From 8f54f7f0d273d928c69c8c87cc062b1bb3ffd7ca Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 3 Oct 2025 14:59:06 +0200 Subject: [PATCH 001/112] added dibi & texy --- contributing/bg/syntax.texy | 2 +- contributing/cs/syntax.texy | 2 +- contributing/de/syntax.texy | 2 +- contributing/el/syntax.texy | 2 +- contributing/en/syntax.texy | 2 +- contributing/es/syntax.texy | 2 +- contributing/fr/syntax.texy | 2 +- contributing/hu/syntax.texy | 2 +- contributing/it/syntax.texy | 2 +- contributing/ja/syntax.texy | 2 +- contributing/pl/syntax.texy | 2 +- contributing/pt/syntax.texy | 2 +- contributing/ro/syntax.texy | 2 +- contributing/ru/syntax.texy | 2 +- contributing/sl/syntax.texy | 2 +- contributing/tr/syntax.texy | 2 +- contributing/uk/syntax.texy | 2 +- dibi/cs/@home.texy | 725 +++++++++++++++++++++++ dibi/cs/@menu.texy | 4 + dibi/cs/@meta.texy | 1 + dibi/en/@home.texy | 725 +++++++++++++++++++++++ dibi/en/@menu.texy | 3 + dibi/en/@meta.texy | 1 + dibi/meta.json | 5 + texy/cs/@home.texy | 32 ++ texy/cs/@menu.texy | 8 + texy/cs/@meta.texy | 1 + texy/cs/@try.texy | 15 + texy/cs/api-block-module.texy | 11 + texy/cs/api-blockquote-module.texy | 8 + texy/cs/api-emoticon-module.texy | 47 ++ texy/cs/api-figure-module.texy | 33 ++ texy/cs/api-heading-module.texy | 42 ++ texy/cs/api-horizline-module.texy | 8 + texy/cs/api-html-module.texy | 21 + texy/cs/api-htmloutput-module.texy | 17 + texy/cs/api-image-module.texy | 35 ++ texy/cs/api-link-module.texy | 42 ++ texy/cs/api-list-module.texy | 12 + texy/cs/api-longwords-module.texy | 19 + texy/cs/api-paragraph-module.texy | 4 + texy/cs/api-phrase-module.texy | 22 + texy/cs/api-script-module.texy | 23 + texy/cs/api-table-module.texy | 18 + texy/cs/api-texy.texy | 69 +++ texy/cs/api-typography-module.texy | 21 + texy/cs/api-zaklady.texy | 25 + texy/cs/api.texy | 28 + texy/cs/napsali-o-texy.texy | 160 ++++++ texy/cs/priklady-vyuziti.texy | 26 + texy/cs/syntax-podrobne.texy | 889 +++++++++++++++++++++++++++++ texy/cs/syntax.texy | 464 +++++++++++++++ texy/cs/texy-vs-wysiwyg.texy | 23 + texy/cs/try-settings.texy | 55 ++ texy/en/@home.texy | 31 + texy/en/@menu.texy | 6 + texy/en/@meta.texy | 1 + texy/en/@try.texy | 15 + texy/en/syntax-full.texy | 889 +++++++++++++++++++++++++++++ texy/en/syntax.texy | 463 +++++++++++++++ texy/en/try-settings.texy | 55 ++ texy/files/image.gif | Bin 0 -> 942 bytes texy/meta.json | 5 + 63 files changed, 5124 insertions(+), 17 deletions(-) create mode 100644 dibi/cs/@home.texy create mode 100644 dibi/cs/@menu.texy create mode 100644 dibi/cs/@meta.texy create mode 100644 dibi/en/@home.texy create mode 100644 dibi/en/@menu.texy create mode 100644 dibi/en/@meta.texy create mode 100644 dibi/meta.json create mode 100644 texy/cs/@home.texy create mode 100644 texy/cs/@menu.texy create mode 100644 texy/cs/@meta.texy create mode 100644 texy/cs/@try.texy create mode 100644 texy/cs/api-block-module.texy create mode 100644 texy/cs/api-blockquote-module.texy create mode 100644 texy/cs/api-emoticon-module.texy create mode 100644 texy/cs/api-figure-module.texy create mode 100644 texy/cs/api-heading-module.texy create mode 100644 texy/cs/api-horizline-module.texy create mode 100644 texy/cs/api-html-module.texy create mode 100644 texy/cs/api-htmloutput-module.texy create mode 100644 texy/cs/api-image-module.texy create mode 100644 texy/cs/api-link-module.texy create mode 100644 texy/cs/api-list-module.texy create mode 100644 texy/cs/api-longwords-module.texy create mode 100644 texy/cs/api-paragraph-module.texy create mode 100644 texy/cs/api-phrase-module.texy create mode 100644 texy/cs/api-script-module.texy create mode 100644 texy/cs/api-table-module.texy create mode 100644 texy/cs/api-texy.texy create mode 100644 texy/cs/api-typography-module.texy create mode 100644 texy/cs/api-zaklady.texy create mode 100644 texy/cs/api.texy create mode 100644 texy/cs/napsali-o-texy.texy create mode 100644 texy/cs/priklady-vyuziti.texy create mode 100644 texy/cs/syntax-podrobne.texy create mode 100644 texy/cs/syntax.texy create mode 100644 texy/cs/texy-vs-wysiwyg.texy create mode 100644 texy/cs/try-settings.texy create mode 100644 texy/en/@home.texy create mode 100644 texy/en/@menu.texy create mode 100644 texy/en/@meta.texy create mode 100644 texy/en/@try.texy create mode 100644 texy/en/syntax-full.texy create mode 100644 texy/en/syntax.texy create mode 100644 texy/en/try-settings.texy create mode 100644 texy/files/image.gif create mode 100644 texy/meta.json diff --git a/contributing/bg/syntax.texy b/contributing/bg/syntax.texy index 23db082cf4..94bbb02a7a 100644 --- a/contributing/bg/syntax.texy +++ b/contributing/bg/syntax.texy @@ -1,7 +1,7 @@ Синтаксис на документацията *************************** -Документацията използва Markdown & [синтаксис на Texy |https://texy.info/cs/syntax] с някои разширения. +Документацията използва Markdown & [синтаксис на Texy |https://texy.nette.org/syntax] с някои разширения. Връзки diff --git a/contributing/cs/syntax.texy b/contributing/cs/syntax.texy index ad8f3ea4c1..039f8c32ac 100644 --- a/contributing/cs/syntax.texy +++ b/contributing/cs/syntax.texy @@ -1,7 +1,7 @@ Dokumentační syntax ******************* -Dokumentace používá Markdown & [Texy syntaxi |https://texy.info/cs/syntax] s některými rozšířeními. +Dokumentace používá Markdown & [Texy syntaxi |https://texy.nette.org/syntax] s některými rozšířeními. Odkazy diff --git a/contributing/de/syntax.texy b/contributing/de/syntax.texy index 64257e971f..02b8af3554 100644 --- a/contributing/de/syntax.texy +++ b/contributing/de/syntax.texy @@ -1,7 +1,7 @@ Dokumentationssyntax ******************** -Die Dokumentation verwendet Markdown & [Texy-Syntax |https://texy.info/de/syntax] mit einigen Erweiterungen. +Die Dokumentation verwendet Markdown & [Texy-Syntax |https://texy.nette.org/syntax] mit einigen Erweiterungen. Links diff --git a/contributing/el/syntax.texy b/contributing/el/syntax.texy index 8727317430..2f46730e28 100644 --- a/contributing/el/syntax.texy +++ b/contributing/el/syntax.texy @@ -1,7 +1,7 @@ Σύνταξη Τεκμηρίωσης ******************* -Η τεκμηρίωση χρησιμοποιεί Markdown & [σύνταξη Texy |https://texy.info/cs/syntax] με ορισμένες επεκτάσεις. +Η τεκμηρίωση χρησιμοποιεί Markdown & [σύνταξη Texy |https://texy.nette.org/syntax] με ορισμένες επεκτάσεις. Σύνδεσμοι diff --git a/contributing/en/syntax.texy b/contributing/en/syntax.texy index a04894a8b9..c7d81ac54f 100644 --- a/contributing/en/syntax.texy +++ b/contributing/en/syntax.texy @@ -1,7 +1,7 @@ Documentation Syntax ******************** -Documentation uses Markdown & [Texy syntax |https://texy.info/en/syntax] with several enhancements. +Documentation uses Markdown & [Texy syntax |https://texy.nette.org/syntax] with several enhancements. Links diff --git a/contributing/es/syntax.texy b/contributing/es/syntax.texy index 97d8316c1c..559dc80217 100644 --- a/contributing/es/syntax.texy +++ b/contributing/es/syntax.texy @@ -1,7 +1,7 @@ Sintaxis de la documentación **************************** -La documentación utiliza Markdown y la [sintaxis Texy |https://texy.info/cs/syntax] con algunas extensiones. +La documentación utiliza Markdown y la [sintaxis Texy |https://texy.nette.org/syntax] con algunas extensiones. Enlaces diff --git a/contributing/fr/syntax.texy b/contributing/fr/syntax.texy index 29c724851e..9b6df676f7 100644 --- a/contributing/fr/syntax.texy +++ b/contributing/fr/syntax.texy @@ -1,7 +1,7 @@ Syntaxe de la documentation *************************** -La documentation utilise Markdown & la [syntaxe Texy |https://texy.info/cs/syntax] avec quelques extensions. +La documentation utilise Markdown & la [syntaxe Texy |https://texy.nette.org/syntax] avec quelques extensions. Liens diff --git a/contributing/hu/syntax.texy b/contributing/hu/syntax.texy index 66b8b50260..11e46f6417 100644 --- a/contributing/hu/syntax.texy +++ b/contributing/hu/syntax.texy @@ -1,7 +1,7 @@ Dokumentációs szintaxis *********************** -A dokumentáció Markdown & [Texy szintaxist |https://texy.info/cs/syntax] használ néhány kiterjesztéssel. +A dokumentáció Markdown & [Texy szintaxist |https://texy.nette.org/syntax] használ néhány kiterjesztéssel. Linkek diff --git a/contributing/it/syntax.texy b/contributing/it/syntax.texy index ef704f10e4..c60a00fb10 100644 --- a/contributing/it/syntax.texy +++ b/contributing/it/syntax.texy @@ -1,7 +1,7 @@ Sintassi della documentazione ***************************** -La documentazione utilizza Markdown e la [sintassi Texy |https://texy.info/cs/syntax] con alcune estensioni. +La documentazione utilizza Markdown e la [sintassi Texy |https://texy.nette.org/syntax] con alcune estensioni. Link diff --git a/contributing/ja/syntax.texy b/contributing/ja/syntax.texy index 4ee286ef64..2fe4e0c515 100644 --- a/contributing/ja/syntax.texy +++ b/contributing/ja/syntax.texy @@ -1,7 +1,7 @@ ドキュメント構文 ******** -ドキュメントはMarkdownと [Texy構文 |https://texy.info/cs/syntax] を使用し、いくつかの拡張機能があります。 +ドキュメントはMarkdownと [Texy構文 |https://texy.nette.org/syntax] を使用し、いくつかの拡張機能があります。 リンク diff --git a/contributing/pl/syntax.texy b/contributing/pl/syntax.texy index 0090da8000..08b4227f42 100644 --- a/contributing/pl/syntax.texy +++ b/contributing/pl/syntax.texy @@ -1,7 +1,7 @@ Składnia dokumentacji ********************* -Dokumentacja używa składni Markdown & [składni Texy |https://texy.info/cs/syntax] z niektórymi rozszerzeniami. +Dokumentacja używa składni Markdown & [składni Texy |https://texy.nette.org/syntax] z niektórymi rozszerzeniami. Linki diff --git a/contributing/pt/syntax.texy b/contributing/pt/syntax.texy index c820a98389..e810926f67 100644 --- a/contributing/pt/syntax.texy +++ b/contributing/pt/syntax.texy @@ -1,7 +1,7 @@ Sintaxe da Documentação *********************** -A documentação usa Markdown e a [sintaxe Texy |https://texy.info/cs/syntax] com algumas extensões. +A documentação usa Markdown e a [sintaxe Texy |https://texy.nette.org/syntax] com algumas extensões. Links diff --git a/contributing/ro/syntax.texy b/contributing/ro/syntax.texy index dd1a95cf97..d756e4e2b0 100644 --- a/contributing/ro/syntax.texy +++ b/contributing/ro/syntax.texy @@ -1,7 +1,7 @@ Sintaxa documentației ********************* -Documentația utilizează Markdown & [sintaxa Texy |https://texy.info/en/syntax] cu unele extensii. +Documentația utilizează Markdown & [sintaxa Texy |https://texy.nette.org/syntax] cu unele extensii. Linkuri diff --git a/contributing/ru/syntax.texy b/contributing/ru/syntax.texy index ad1bbc8511..af9411a740 100644 --- a/contributing/ru/syntax.texy +++ b/contributing/ru/syntax.texy @@ -1,7 +1,7 @@ Синтаксис документации ********************** -Документация использует синтаксис Markdown и [синтаксис Texy |https://texy.info/cs/syntax] с некоторыми расширениями. +Документация использует синтаксис Markdown и [синтаксис Texy |https://texy.nette.org/syntax] с некоторыми расширениями. Ссылки diff --git a/contributing/sl/syntax.texy b/contributing/sl/syntax.texy index 8486f3ea0e..55aed5ac18 100644 --- a/contributing/sl/syntax.texy +++ b/contributing/sl/syntax.texy @@ -1,7 +1,7 @@ Sintaksa dokumentacije ********************** -Dokumentacija uporablja Markdown & [sintakso Texy |https://texy.info/sl/syntax] z nekaterimi razširitvami. +Dokumentacija uporablja Markdown & [sintakso Texy |https://texy.nette.org/syntax] z nekaterimi razširitvami. Povezave diff --git a/contributing/tr/syntax.texy b/contributing/tr/syntax.texy index 650cfe37eb..41bbf226ef 100644 --- a/contributing/tr/syntax.texy +++ b/contributing/tr/syntax.texy @@ -1,7 +1,7 @@ Dokümantasyon sözdizimi *********************** -Dokümantasyon, bazı uzantılarla birlikte Markdown & [Texy sözdizimini |https://texy.info/cs/syntax] kullanır. +Dokümantasyon, bazı uzantılarla birlikte Markdown & [Texy sözdizimini |https://texy.nette.org/syntax] kullanır. Bağlantılar diff --git a/contributing/uk/syntax.texy b/contributing/uk/syntax.texy index de3c97be92..c9d1b66d60 100644 --- a/contributing/uk/syntax.texy +++ b/contributing/uk/syntax.texy @@ -1,7 +1,7 @@ Синтаксис документації ********************** -Документація використовує Markdown та [синтаксис Texy |https://texy.info/cs/syntax] з деякими розширеннями. +Документація використовує Markdown та [синтаксис Texy |https://texy.nette.org/syntax] з деякими розширеннями. Посилання diff --git a/dibi/cs/@home.texy b/dibi/cs/@home.texy new file mode 100644 index 0000000000..dba6f4cc20 --- /dev/null +++ b/dibi/cs/@home.texy @@ -0,0 +1,725 @@ +Dibi: Šikovná Database Abstraction Library pro PHP +************************************************** + +Nejnovější stabilní verzi Dibi instalujte pomocí [Composer|best-practices:composer] příkazem: + +``` +composer require dibi/dibi +``` + +Přehled verzí najdete na stránce [Releases | https://github.com/dibi/dibi/releases]. + +Vyžaduje PHP 8.2 nebo vyšší. + + +Připojení k databázi +==================== + +Databázové spojení je reprezentováno objektem [Dibi\Connection|api:]: + +```php +$database = new Dibi\Connection([ + 'driver' => 'mysqli', + 'host' => 'localhost', + 'username' => 'root', + 'password' => '***', + 'database' => 'table', +]); + +$result = $database->query('SELECT * FROM users'); +``` + +Alternativně můžete používat statický registr `dibi`, který udržuje v globálně dostupném úložišti objekt spojení a nad ním volá všechny funkce: + +```php +dibi::connect([ + 'driver' => 'mysqli', + 'host' => 'localhost', + 'username' => 'root', + 'password' => '***', + 'database' => 'test', + 'charset' => 'utf8', +]); + +$result = dibi::query('SELECT * FROM users'); +``` + +V případě chyby připojení se vyhodí `Dibi\Exception`. + + +Dotazy +====== + +Databázové dotazy pokládáme metodou `query()`, která vrací [Dibi\Result |api:Dibi\Result]. Řádky jako objekty [Dibi\Row |api:Dibi\Row]. + +Všechny příklady si můžete zkoušet [online na hřišti |https://repl.it/@DavidGrudl/dibi-playground]. + +```php +$result = $database->query('SELECT * FROM users'); + +foreach ($result as $row) { + echo $row->id; + echo $row->name; +} + +// pole všech řádků +$all = $result->fetchAll(); + +// pole všech řádků, klíčem je 'id' +$all = $result->fetchAssoc('id'); + +// asociativní pole id => name +$pairs = $result->fetchPairs('id', 'name'); + +// počet řádků výsledku, pokud je znám, nebo počet ovlivněných řádků +$count = $result->getRowCount(); +``` + +Metoda fetchAssoc() umí vracet i [složitější asociativní pole |#Výsledek jako asociativní pole]. + +Do dotazu lze velmi snadno přidávat i parametry, všimněte si otazníku: + +```php +$result = $database->query('SELECT * FROM users WHERE name = ? AND active = ?', $name, $active); + +// nebo +$result = $database->query('SELECT * FROM users WHERE name = ?', $name, 'AND active = ?', $active); + +$ids = [10, 20, 30]; +$result = $database->query('SELECT * FROM users WHERE id IN (?)', $ids); +``` + +
+**POZOR, nikdy dotazy neskládejte jako řetězce, vznikla by zranitelnost [SQL injection |https://cs.wikipedia.org/wiki/SQL_injection]** +/-- +$database->query('SELECT * FROM users WHERE id = ' . $id); // ŠPATNĚ!!! +\-- +
+ +Místo otazníku lze používat i tzv. [#modifikátory]. + +```php +$result = $database->query('SELECT * FROM users WHERE name = %s', $name); +``` + +V případě selhání `query()` vyhodí buď `Dibi\Exception`, nebo některého z potomků: + +- [ConstraintViolationException |api:Dibi\ConstraintViolationException] - porušení nějakého omezení pro tabulku +- [ForeignKeyConstraintViolationException |api:Dibi\ForeignKeyConstraintViolationException] - neplatný cizí klíč +- [NotNullConstraintViolationException |api:Dibi\NotNullConstraintViolationException] - porušení podmínky NOT NULL +- [UniqueConstraintViolationException |api:Dibi\UniqueConstraintViolationException] - koliduje unikátní index + +Dotazy lze pokládat také pomocí zkratek: + +```php +// vrátí asociativní pole id => name, zkratka pro query(...)->fetchPairs() +$pairs = $database->fetchPairs('SELECT id, name FROM users'); + +// vrátí pole všech řádků, zkratka pro query(...)->fetchAll() +$rows = $database->fetchAll('SELECT * FROM users'); + +// vrátí řádek, zkratka pro query(...)->fetch() +$row = $database->fetch('SELECT * FROM users WHERE id = ?', $id); + +// vrátí buňku, zkratka pro query(...)->fetchSingle() +$name = $database->fetchSingle('SELECT name FROM users WHERE id = ?', $id); +``` + + +Modifikátory +============ + +Kromě zástupného symbolu `?` můžeme používat i modifikátory: + +| %s | string +| %sN | string, ale '' se přeloží jako NULL +| %bin | binární data +| %b | boolean +| %i | integer +| %iN | integer, ale 0 se přeloží jako NULL +| %f | float +| %d | datum (očekává DateTime, string nebo UNIX timestamp) +| %dt | datum & čas (očekává DateTime, string nebo UNIX timestamp) +| %n | identifikátor, tedy název tabulky či sloupce +| %N | identifikátor, považuje tečku za běžný znak +| %SQL | SQL - přímo vloží do SQL (alternativou je Dibi\Literal) +| %ex | expanduje pole +| %lmt | speciální - doplní do dotazu LIMIT +| %ofs | speciální - doplní do dotazu OFFSET + +Příklad: + +```php +$result = $database->query('SELECT * FROM users WHERE name = %s', $name); +``` + +Pokud je `$name` `null`, vloží se do SQL příkazu `NULL`. + +Pokud je proměnná pole, modifikátor se aplikuje na všechny jeho prvky a ty se vloží do SQL oddělené čárkami: + +```php +$ids = [10, '20', 30]; +$result = $database->query('SELECT * FROM users WHERE id IN (%i)', $ids); +// SELECT * FROM users WHERE id IN (10, 20, 30) +``` + +Modifikátor `%n` 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'; +$result = $database->query('SELECT * FROM %n WHERE %n = ?', $table, $column, $value); +// SELECT * FROM `blog`.`users` WHERE `name` = 'Jim' +``` + +Pro operátor LIKE jsou k dispozici čtyři speciální modifikátory: + +| %like~ | výraz začíná řetězcem +| %~like | výraz končí řetězcem +| %~like~ | výraz obsahuje řetězec +| `%like` | výraz je řetězec + +Hledej jména začínající na určitý řetězec: + +```php +$result = $database->query('SELECT * FROM table WHERE name LIKE %like~', $query); +``` + + +Modifikátory polí +================= + +Parametrem vkládaným do SQL dotazu může být i pole. Tyto modifikátory určují, jak z něj sestavit SQL příkaz: + +| %and | | `key1 = value1 AND key2 = value2 AND ...` +| %or | | `key1 = value1 OR key2 = value2 OR ...` +| %a | assoc | `key1 = value1, key2 = value2, ...` +| %l %in | list | `(val1, val2, ...)` +| %v | values | `(key1, key2, ...) VALUES (value1, value2, ...)` +| %m | multi | `(key1, key2, ...) VALUES (value1, value2, ...), (value1, value2, ...), ...` +| %by | řazení | `key1 ASC, key2 DESC ...` +| %n | názvy | `key1, key2 AS alias, ...` + +Příklad: + +```php +$arr = [ + 'a' => 'hello', + 'b' => true, +]; + +$database->query('INSERT INTO table %v', $arr); +// INSERT INTO `table` (`a`, `b`) VALUES ('hello', 1) + +$database->query('UPDATE `table` SET %a', $arr); +// UPDATE `table` SET `a`='hello', `b`=1 +``` + +V klauzuli WHERE lze použít modifikátory `%and` nebo `%or`: + +```php +$result = $database->query('SELECT * FROM users WHERE %and', [ + 'name' => $name, + 'year' => $year, +]); +// SELECT * FROM users WHERE `name` = 'Jim' AND `year` = 1978 +``` + +Viz také [#Složitější dotazy]. + +Modifikátor `%by` slouží k řazení, v klíčích uvedeme sloupce a hodnotou bude boolean určující, zda řadit vzestupně: + +```php +$result = $database->query('SELECT id FROM author ORDER BY %by', [ + 'id' => true, // vzestupně + 'name' => false, // sestupně +]); +// SELECT id FROM author ORDER BY `id`, `name` DESC +``` + + +Insert, Update & Delete +======================= + +Data vkládáme do SQL dotazu jako asociativní pole. Modifikátory ani zástupný znak `?` není nutné v těchto případech uvádět. + +```php +$database->query('INSERT INTO users', [ + '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) +``` + +Mazání: + +```php +$database->query('DELETE FROM users WHERE id = ?', $id); + +// vrací počet smazaných řádků +$affectedRows = $database->getAffectedRows(); +``` + +Úprava záznamů: + +```php +$database->query('UPDATE users SET', [ + 'name' => $name, + 'year' => $year, +], 'WHERE id = ?', $id); +// UPDATE users SET `name` = 'Jim', `year` = 1978 WHERE id = 123 + +// vrací počet změněných řádků +$affectedRows = $database->getAffectedRows(); +``` + +Vložení záznamu, nebo úprava, pokud již existuje: + +```php +$database->query('INSERT INTO users', [ + 'id' => $id, + 'name' => $name, + 'year' => $year, +], 'ON DUPLICATE KEY UPDATE %a', [ // tady už modifikátor %a uvést musíme + 'name' => $name, + 'year' => $year, +]); +// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) +// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 +``` + + +Fluent +====== + +Kromě ručního psaní SQL umí Dibi dotazy skládat přes fluent interface, kdy příkaz sestavujete řetězením metod. Začínáte metodami `select()`, `insert()`, `update()`, `delete()` nebo obecnou `command()` nad spojením: + +```php +$rows = $database->select('id, name') + ->from('users') + ->where('age > ?', $age) + ->orderBy('name') + ->fetchAll(); +// SELECT id, name FROM users WHERE age > 18 ORDER BY name +``` + +Názvy tabulek a sloupců se automaticky escapují, zástupný znak `?` i [modifikátory |#modifikátory] fungují stejně jako v běžném `query()`. + +Vložení záznamu a získání jeho ID: + +```php +$id = $database->insert('users', [ + 'name' => $name, + 'year' => $year, +])->execute(Dibi\Fluent::Identifier); +``` + +Úprava a mazání fungují stejně; předáním `Dibi\Fluent::AffectedRows` metodě `execute()` získáte počet ovlivněných řádků: + +```php +$affected = $database->update('users', ['name' => $name]) + ->where('id = ?', $id) + ->execute(Dibi\Fluent::AffectedRows); + +$database->delete('users') + ->where('id = ?', $id) + ->execute(); +``` + +Pomocí `setFlag()` lze přidat SQL příznak, například pro sestavení `INSERT IGNORE`: + +```php +$database->insert('users', $record) + ->setFlag('IGNORE') + ->execute(); +``` + +Řádky se načítají stejnými metodami jako u [Dibi\Result |#dotazy] - `fetch()`, `fetchSingle()`, `fetchAll()`, `fetchPairs()`, `fetchAssoc()` - nebo iterací přes `foreach`. Chcete-li místo vykonání dotazu jen vypsat vygenerované SQL, zavolejte `test()`. + + +DataSource +========== + +[Dibi\DataSource |api:] obalí tabulku nebo SQL dotaz do objektu, který můžete před samotným vykonáním dále filtrovat, řadit a stránkovat - hodí se pro komponenty typu datagrid. Vytvoříte jej metodou `dataSource()` z názvu tabulky nebo z celého dotazu: + +```php +$ds = $database->dataSource('SELECT id, name, age FROM users'); +``` + +Poté data upřesníte a načtete. Metoda `count()` vrací počet řádků odpovídajících aktuálním podmínkám (a limitu, je-li nastaven), zatímco `getTotalCount()` vrací celkový počet bez ohledu na podmínky a limit: + +```php +$ds->where('age > ?', $age) + ->orderBy('name'); + +$count = $ds->count(); // počet řádků odpovídajících filtru +$rows = $ds->applyLimit(10) // jedna stránka výsledků + ->fetchAll(); +``` + + +Transakce +========= + +Pro práci s transakcemi slouží čtveřice metod: + +```php +$database->beginTransaction(); // zahájení transakce + +$database->commit(); // potvrzení + +$database->rollback(); // vrácení zpět + +$database->transaction(function () { + // nejaka akce +}); +``` + + +Testování +========= + +Abyste si mohli trošku s Dibi hrát, je tu připravena metoda `test()`, které předáte parametry stejně jako `query()`, ovšem místo provedení SQL příkazu se tento barevně vypíše na obrazovku. + +Výsledky dotazu je možné vypsat jako tabulku pomocí `$result->dump()`. + +K dispozici jsou dále proměnné: + +```php +dibi::$sql; // poslední SQL příkaz +dibi::$elapsedTime; // jeho doba trvání v sekundách +dibi::$numOfQueries; // celkem SQL příkazů +dibi::$totalTime; // celkový čas v sekundách +``` + + +Složitější dotazy +================= + +Parametrem může být také objekt `DateTime`. + +```php +$result = $database->query('SELECT * FROM users WHERE created < ?', new DateTime); + +$database->query('INSERT INTO users', [ + 'created' => new DateTime, +]); +``` + +Nebo SQL literál: + +```php +$database->query('UPDATE table SET', [ + 'date' => $database->literal('NOW()'), +]); +// UPDATE table SET `date` = NOW() +``` + +Nebo výraz, ve kterém lze používat zástupné znaky `?` nebo modifikátory: + +```php +$database->query('UPDATE `table` SET', [ + 'title' => $database::expression('SHA1(?)', 'tajne'), +]); +// UPDATE `table` SET `title` = SHA1('tajne') +``` + +Při update lze modifikátory uvádět přímo v klíčích: + +```php +$database->query('UPDATE table SET', [ + 'date%SQL' => 'NOW()', // %SQL znamená SQL ;) +]); +// UPDATE table SET `date` = NOW() +``` + +V podmínkách (tj. u modifikátorů `%and` a `%or`) není nutné uvádět klíče: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %and', [ + 'number > 10', + 'number < 100', +]); +// SELECT * FROM `table` WHERE (number > 10) AND (number < 100) +``` + +V položkách lze používat i modifikátory nebo zástupné znaky: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %and', [ + ['number > ?', 10], // nebo $database::expression('number > ?', 10) + ['number < ?', 100], + ['%or', [ + 'left' => 1, + 'top' => 2, + ]], +]); +// SELECT * FROM `table` WHERE (number > 10) AND (number < 100) AND (`left` = 1 OR `top` = 2) +``` + +Modifikátor `%ex` vloží do SQL všechny prvky pole: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %ex', [ + $database::expression('left = ?', 1), + 'AND', + 'top IS NULL', +]); +// SELECT * FROM `table` WHERE left = 1 AND top IS NULL +``` + + +Podmínky v SQL příkazu +====================== + +Podmíněné SQL příkazy se ovládají pomocí tří modifikátorů `%if`, `%else` a `%end`. První z nich `%if` se musí nacházet zcela na konci řetězce představujícího SQL a za ním následuje proměnná: + +```php +// $user = ???; + +$result = $database->query(' + SELECT * + FROM table + %if', isset($user), 'WHERE user=%s', $user, '%end + ORDER BY name +'); +``` + +Podmínku lze doplnit o část `%else`: + +```php +$result = $database->query(' + SELECT * + FROM %if', $cond, 'one_table %else second_table +'); +``` + +Podmínky můžete zanořovat do sebe. + + +Identifikátory a řetězce v SQL +============================== + +Samotné SQL prochází zpracováním, aby vyhovovalo konvencím dané databáze. Identifikátory (jména tabulek a sloupců) lze uvozovat do hranatých závorek nebo zpětných uvozovek, dále řetězce jednoduchými či dvojitými uvozovkami, nicméně na server se pošle vždy to, co databáze žádá. Příklad: + +```php +$database->query("UPDATE `table` SET [status]='I''m fine'"); +// MySQL: UPDATE `table` SET `status`='I\'m fine' +// ODBC: UPDATE [table] SET [status]='I''m fine' +``` + +Uvozovka se uvnitř řetězce v SQL zapisuje zdvojením. + + +Výsledek jako asociativní pole +============================== + +Příklad: vrátí výsledky jako asociativní pole, kde klíčem bude hodnota políčka `id`: + +```php +$assoc = $result->fetchAssoc('id'); +``` + +Největší síla funkce `fetchAssoc()` se projeví u SQL dotazu spojujícího několik tabulek s různými typy vazeb. Databáze z toho udělá plochou tabulku, fetchAssoc jí vrátí tvar. + +Příklad: Mějme tabulku zákazníků a objednávek (vazba N:M) a položíme dotaz: + +```php +$result = $database->query(' + SELECT customer_id, customers.name, order_id, orders.number, ... + FROM customers + INNER JOIN orders USING (customer_id) + WHERE ... +'); +``` + +A rádi bychom získali vnořené asociativní pole podle ID zákazníka a poté podle ID objednávky: + +```php +$all = $result->fetchAssoc('customer_id|order_id'); + +// budeme jej procházet takto: +foreach ($all as $customerId => $orders) { + foreach ($orders as $orderId => $order) { + // ... + } +} +``` + +Asociativní deskriptor má obdobnou syntax, jako když pole píšete pomocí přiřazení v PHP. Tedy `'customer_id|order_id'` představuje sérii přiřazení `$all[$customerId][$orderId] = $row;`, postupně pro všechny řádky. + +Někdy by se hodilo, aby se asociovalo podle jména zákazníka namísto jeho ID: + +```php +$all = $result->fetchAssoc('name|order_id'); + +// k prvkům pak přistupujeme třeba takto: +$order = $all['Arnold Rimmer'][$orderId]; +``` + +Co když ale existuje více zákazníků se stejným jménem? Tabulka by měla mít spíš tvar: + +```php +$row = $all['Arnold Rimmer'][0][$orderId]; +$row = $all['Arnold Rimmer'][1][$orderId]; +``` + +Rozlišujeme tedy více možných Rimmerů pomocí klasického pole. Asociativní deskriptor má opět formát podobný přiřazování, s tím, že sekvenční pole představuje `[]`: + +```php +$all = $result->fetchAssoc('name[]order_id'); + +// iterujeme všechny Arnoldy ve výsledcích +foreach ($all['Arnold Rimmer'] as $arnoldOrders) { + foreach ($arnoldOrders as $orderId => $order) { + // ... + } +} +``` + +Vrátíme se k příkladu s deskriptorem `'customer_id|order_id'` a zkusíme vypsat objednávky jednotlivých zákazníků: + +```php +$all = $result->fetchAssoc('customer_id|order_id'); + +foreach ($all as $customerId => $orders) { + echo "Objednávky zákazníka $customerId:"; + + foreach ($orders as $orderId => $order) { + echo "Číslo dokladu: $order->number"; + // jméno zákazníka je v $order->name + } +} +``` + +Bylo by hezké místo ID zákazníka vypsat jeho jméno. Jenže to bychom museli dohledávat v poli `$orders`. Výsledky si proto necháme upravit do takovéhoto tvaru: + +```php +$all[$customerId]->name = 'John Doe'; +$all[$customerId]->order_id[$orderId] = $row; +$all[$customerId]->order_id[$orderId2] = $row2; +``` + +Tedy mezi `$customerId` a `$orderId` vložíme ještě mezičlánek. Tentokrát ne číslované indexy, jaké jsme použili pro odlišení jednotlivých Rimmerů, ale rovnou databázový záznam. Řešení je velmi podobné, jen si stačí zapamatovat, že záznam symbolizuje šipka: + +```php +$all = $result->fetchAssoc('customer_id->order_id'); + +foreach ($all as $customerId => $row) { + echo "Objednávky zákazníka $row->name:"; + + foreach ($row->order_id as $orderId => $order) { + echo "Číslo dokladu: $order->number"; + } +} +``` + + +Prefixy & substituce +==================== + +Názvy tabulek a sloupců mohou obsahovat proměnné části. Ty si nejprve nadefinujeme: + +```php +// vytvoří novou substituci :blog: ==> wp_ +$database->getSubstitutes()->blog = 'wp_'; +``` + +a poté použijeme v SQL. Všimněte si, že v SQL jsou uvozeny dvojtečkami: + +```php +$database->query("UPDATE [:blog:items] SET [text]='Hello World'"); +// UPDATE `wp_items` SET `text`='Hello World' +``` + + +Datové typy buněk +================= + +Dibi automaticky detekuje typy jednotlivých sloupců dotazu a převádí buňky na nativní typy PHP. Typ můžeme určit i manuálně. Možné typy najdete ve třídě [Dibi\Type |api:Dibi\Type]. + +```php +$result->setType('id', Dibi\Type::INTEGER); // id bude integer +$row = $result->fetch(); + +is_int($row->id) // true +``` + + +Logování +======== + +Dibi má v sobě zabudovaný logger, kterým můžete sledovat všechny vykonané SQL příkazy a měřit délku jejich trvání. Aktivace: + +```php +$database->connect([ + 'driver' => 'sqlite', + 'database' => 'sample.sdb', + 'profiler' => [ + 'file' => 'file.log', + ], +]); +``` + +Šikovnější profiler je panel pro Tracy, který se aktivuje při propojení s Nette. + + +Připojení do [Nette |https://nette.org] +======================================= + +V konfiguračním souboru zaregistrujeme DI rozšíření a přidáme sekci `dibi` - tím se vytvoří potřebné objekty a také databázový panel v [Tracy |https://tracy.nette.org] debugger baru. + +```neon +extensions: + dibi: Dibi\Bridges\Nette\DibiExtension3 + +dibi: + host: localhost + username: root + password: *** + database: foo + lazy: true +``` + +Poté objekt spojení [získáme jako službu z DI kontejneru |https://doc.nette.org/di-usage], např.: + +```php +class Model +{ + private $database; + + public function __construct(Dibi\Connection $database) + { + $this->database = $database; + } +} +``` + + +Komunitní rozšíření +=================== + +Nad Dibi staví nejrůznější knihovny, ORM a rozšíření. Celý jejich seznam najdete na "Packagistu":https://packagist.org/packages/dibi/dibi/dependents?order_by=downloads&requires=require. + +{{maintitle: Dibi – Šikovná Database Abstraction Library pro PHP}} diff --git a/dibi/cs/@menu.texy b/dibi/cs/@menu.texy new file mode 100644 index 0000000000..9a7a7c3b48 --- /dev/null +++ b/dibi/cs/@menu.texy @@ -0,0 +1,4 @@ +- [Úvod | @home] +- "Blog .[link-external]":https://phpfashion.com/category/dibi +- "API .[link-external]":https://api.nette.org/dibi/ +- "GitHub .[link-external]":https://github.com/dibi/dibi diff --git a/dibi/cs/@meta.texy b/dibi/cs/@meta.texy new file mode 100644 index 0000000000..49d44d0cfa --- /dev/null +++ b/dibi/cs/@meta.texy @@ -0,0 +1 @@ +{{sitename: Dibi Dokumentace}} diff --git a/dibi/en/@home.texy b/dibi/en/@home.texy new file mode 100644 index 0000000000..5eef65ce88 --- /dev/null +++ b/dibi/en/@home.texy @@ -0,0 +1,725 @@ +Dibi: Smart Database Abstraction Library for PHP +************************************************ + +To install the latest stable Dibi version, use the [Composer|best-practices:composer] command: + +``` +composer require dibi/dibi +``` + +You can find a version overview on the [Releases | https://github.com/dibi/dibi/releases] page. + +Requires PHP 8.2 or newer. + + +Connecting to Database +====================== + +The database connection is represented by the [Dibi\Connection|api:] object: + +```php +$database = new Dibi\Connection([ + 'driver' => 'mysqli', + 'host' => 'localhost', + 'username' => 'root', + 'password' => '***', + 'database' => 'table', +]); + +$result = $database->query('SELECT * FROM users'); +``` + +Alternatively, you can use the `dibi` static registry, which maintains a connection object in globally accessible storage and calls all functions on it: + +```php +dibi::connect([ + 'driver' => 'mysqli', + 'host' => 'localhost', + 'username' => 'root', + 'password' => '***', + 'database' => 'test', + 'charset' => 'utf8', +]); + +$result = dibi::query('SELECT * FROM users'); +``` + +In case of a connection error, it throws `Dibi\Exception`. + + +Queries +======= + +We query the database using the `query()` method, which returns [Dibi\Result |api:Dibi\Result]. Rows are returned as [Dibi\Row |api:Dibi\Row] objects. + +You can try all the examples [online at the playground |https://repl.it/@DavidGrudl/dibi-playground]. + +```php +$result = $database->query('SELECT * FROM users'); + +foreach ($result as $row) { + echo $row->id; + echo $row->name; +} + +// array of all rows +$all = $result->fetchAll(); + +// array of all rows, keyed by 'id' +$all = $result->fetchAssoc('id'); + +// associative pairs id => name +$pairs = $result->fetchPairs('id', 'name'); + +// number of result rows, if known, or number of affected rows +$count = $result->getRowCount(); +``` + +The fetchAssoc() method can return [more complex associative arrays |#Result as associative array]. + +You can easily add parameters to the query - note the question mark: + +```php +$result = $database->query('SELECT * FROM users WHERE name = ? AND active = ?', $name, $active); + +// or +$result = $database->query('SELECT * FROM users WHERE name = ?', $name, 'AND active = ?', $active); + +$ids = [10, 20, 30]; +$result = $database->query('SELECT * FROM users WHERE id IN (?)', $ids); +``` + +
+**WARNING: never concatenate parameters into SQL queries, as this would create an [SQL injection |https://en.wikipedia.org/wiki/SQL_injection] vulnerability** +/-- +$database->query('SELECT * FROM users WHERE id = ' . $id); // BAD!!! +\-- +
+ +Instead of question marks, you can also use so-called [#modifiers]. + +```php +$result = $database->query('SELECT * FROM users WHERE name = %s', $name); +``` + +In case of failure, `query()` throws either `Dibi\Exception` or one of its descendants: + +- [ConstraintViolationException |api:Dibi\ConstraintViolationException] - violation of some table constraint +- [ForeignKeyConstraintViolationException |api:Dibi\ForeignKeyConstraintViolationException] - invalid foreign key +- [NotNullConstraintViolationException |api:Dibi\NotNullConstraintViolationException] - violation of the NOT NULL condition +- [UniqueConstraintViolationException |api:Dibi\UniqueConstraintViolationException] - collision with unique index + +You can also use shortcut methods: + +```php +// returns associative pairs id => name, shortcut for query(...)->fetchPairs() +$pairs = $database->fetchPairs('SELECT id, name FROM users'); + +// returns array of all rows, shortcut for query(...)->fetchAll() +$rows = $database->fetchAll('SELECT * FROM users'); + +// returns row, shortcut for query(...)->fetch() +$row = $database->fetch('SELECT * FROM users WHERE id = ?', $id); + +// returns cell, shortcut for query(...)->fetchSingle() +$name = $database->fetchSingle('SELECT name FROM users WHERE id = ?', $id); +``` + + +Modifiers +========= + +In addition to the `?` placeholder, we can also use modifiers: + +| %s | string +| %sN | string, but '' translates as NULL +| %bin | binary data +| %b | boolean +| %i | integer +| %iN | integer, but 0 translates as NULL +| %f | float +| %d | date (accepts DateTime, string or UNIX timestamp) +| %dt | datetime (accepts DateTime, string or UNIX timestamp) +| %n | identifier, i.e. table or column name +| %N | identifier, treats period as ordinary character +| %SQL | SQL - directly inserts into SQL (alternative is Dibi\Literal) +| %ex | expands array +| %lmt | special - adds LIMIT to the query +| %ofs | special - adds OFFSET to the query + +Example: + +```php +$result = $database->query('SELECT * FROM users WHERE name = %s', $name); +``` + +If `$name` is `null`, `NULL` is inserted into the SQL statement. + +If the variable is an array, the modifier is applied to all of its elements and they are inserted into SQL separated by commas: + +```php +$ids = [10, '20', 30]; +$result = $database->query('SELECT * FROM users WHERE id IN (%i)', $ids); +// SELECT * FROM users WHERE id IN (10, 20, 30) +``` + +The `%n` modifier is used when the table 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'; +$result = $database->query('SELECT * FROM %n WHERE %n = ?', $table, $column, $value); +// SELECT * FROM `blog`.`users` WHERE `name` = 'Jim' +``` + +Four special modifiers are available for the LIKE operator: + +| %like~ | expression starts with string +| %~like | expression ends with string +| %~like~ | expression contains string +| `%like` | expression matches string + +Search for names starting with a certain string: + +```php +$result = $database->query('SELECT * FROM table WHERE name LIKE %like~', $query); +``` + + +Array Modifiers +=============== + +The parameter inserted into an SQL query can also be an array. These modifiers determine how to construct the SQL statement from it: + +| %and | | `key1 = value1 AND key2 = value2 AND ...` +| %or | | `key1 = value1 OR key2 = value2 OR ...` +| %a | assoc | `key1 = value1, key2 = value2, ...` +| %l %in | list | `(val1, val2, ...)` +| %v | values | `(key1, key2, ...) VALUES (value1, value2, ...)` +| %m | multi | `(key1, key2, ...) VALUES (value1, value2, ...), (value1, value2, ...), ...` +| %by | ordering | `key1 ASC, key2 DESC ...` +| %n | names | `key1, key2 AS alias, ...` + +Example: + +```php +$arr = [ + 'a' => 'hello', + 'b' => true, +]; + +$database->query('INSERT INTO table %v', $arr); +// INSERT INTO `table` (`a`, `b`) VALUES ('hello', 1) + +$database->query('UPDATE `table` SET %a', $arr); +// UPDATE `table` SET `a`='hello', `b`=1 +``` + +In the WHERE clause, you can use `%and` or `%or` modifiers: + +```php +$result = $database->query('SELECT * FROM users WHERE %and', [ + 'name' => $name, + 'year' => $year, +]); +// SELECT * FROM users WHERE `name` = 'Jim' AND `year` = 1978 +``` + +See also [#Complex queries]. + +The `%by` modifier is used for sorting - keys specify the columns, and the boolean value determines whether to sort in ascending order: + +```php +$result = $database->query('SELECT id FROM author ORDER BY %by', [ + 'id' => true, // ascending + 'name' => false, // descending +]); +// SELECT id FROM author ORDER BY `id`, `name` DESC +``` + + +Insert, Update & Delete +======================= + +We insert data into SQL queries as associative arrays. Modifiers and the `?` placeholder are not necessary in these cases. + +```php +$database->query('INSERT INTO users', [ + 'name' => $name, + 'year' => $year, +]); +// INSERT INTO users (`name`, `year`) VALUES ('Jim', 1978) + +$id = $database->getInsertId(); // returns the auto-increment of the inserted record + +$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) +``` + +Deleting: + +```php +$database->query('DELETE FROM users WHERE id = ?', $id); + +// returns number of deleted rows +$affectedRows = $database->getAffectedRows(); +``` + +Updating records: + +```php +$database->query('UPDATE users SET', [ + 'name' => $name, + 'year' => $year, +], 'WHERE id = ?', $id); +// UPDATE users SET `name` = 'Jim', `year` = 1978 WHERE id = 123 + +// returns the number of updated rows +$affectedRows = $database->getAffectedRows(); +``` + +Substitute any identifier: + +```php +$database->query('INSERT INTO users', [ + 'id' => $id, + 'name' => $name, + 'year' => $year, +], 'ON DUPLICATE KEY UPDATE %a', [ // here the modifier %a must be used + 'name' => $name, + 'year' => $year, +]); +// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978) +// ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978 +``` + + +Fluent +====== + +Besides writing SQL by hand, Dibi can build queries through a fluent interface, where you assemble the statement by chaining methods. You start with the `select()`, `insert()`, `update()`, `delete()`, or the generic `command()` methods on the connection: + +```php +$rows = $database->select('id, name') + ->from('users') + ->where('age > ?', $age) + ->orderBy('name') + ->fetchAll(); +// SELECT id, name FROM users WHERE age > 18 ORDER BY name +``` + +Table and column names are escaped automatically, and the `?` placeholder and [modifiers |#modifiers] work just like in a regular `query()`. + +Inserting a record and getting its ID: + +```php +$id = $database->insert('users', [ + 'name' => $name, + 'year' => $year, +])->execute(Dibi\Fluent::Identifier); +``` + +Updating and deleting work the same way; pass `Dibi\Fluent::AffectedRows` to `execute()` to get the number of affected rows: + +```php +$affected = $database->update('users', ['name' => $name]) + ->where('id = ?', $id) + ->execute(Dibi\Fluent::AffectedRows); + +$database->delete('users') + ->where('id = ?', $id) + ->execute(); +``` + +You can add an SQL flag with `setFlag()`, for example to build `INSERT IGNORE`: + +```php +$database->insert('users', $record) + ->setFlag('IGNORE') + ->execute(); +``` + +The rows are fetched with the same methods as [Dibi\Result |#queries] - `fetch()`, `fetchSingle()`, `fetchAll()`, `fetchPairs()`, `fetchAssoc()` - or by iterating with `foreach`. To only print the generated SQL instead of running the query, call `test()`. + + +DataSource +========== + +[Dibi\DataSource |api:] wraps a table or an SQL query in an object that you can further filter, sort, and paginate before it is actually executed - handy for components such as data grids. You create it with `dataSource()` from a table name or a whole query: + +```php +$ds = $database->dataSource('SELECT id, name, age FROM users'); +``` + +Then you refine the data and read it. The `count()` method returns the number of rows matching the current conditions (and limit, if set), while `getTotalCount()` returns the total number ignoring conditions and limit: + +```php +$ds->where('age > ?', $age) + ->orderBy('name'); + +$count = $ds->count(); // number of rows matching the filter +$rows = $ds->applyLimit(10) // one page of results + ->fetchAll(); +``` + + +Transaction +=========== + +There are four methods for dealing with transactions: + +```php +$database->beginTransaction(); + +$database->commit(); + +$database->rollback(); + +$database->transaction(function () { + // some action +}); +``` + + +Testing +======= + +In order to play with Dibi a little, there is a `test()` method that takes the same parameters as `query()`, but instead of executing the SQL statement, it echoes it on the screen. + +The query results can be echoed as a table using `$result->dump()`. + +These variables are also available: + +```php +dibi::$sql; // the latest SQL query +dibi::$elapsedTime; // its duration in sec +dibi::$numOfQueries; +dibi::$totalTime; +``` + + +Complex Queries +=============== + +The parameter may also be a `DateTime` object. + +```php +$result = $database->query('SELECT * FROM users WHERE created < ?', new DateTime); + +$database->query('INSERT INTO users', [ + 'created' => new DateTime, +]); +``` + +Or an SQL literal: + +```php +$database->query('UPDATE table SET', [ + 'date' => $database->literal('NOW()'), +]); +// UPDATE table SET `date` = NOW() +``` + +Or an expression in which you can use `?` or modifiers: + +```php +$database->query('UPDATE `table` SET', [ + 'title' => $database::expression('SHA1(?)', 'secret'), +]); +// UPDATE `table` SET `title` = SHA1('secret') +``` + +When updating, modifiers can be placed directly in the keys: + +```php +$database->query('UPDATE table SET', [ + 'date%SQL' => 'NOW()', // %SQL means SQL ;) +]); +// UPDATE table SET `date` = NOW() +``` + +In conditions (i.e., for the `%and` and `%or` modifiers), it is not necessary to specify the keys: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %and', [ + 'number > 10', + 'number < 100', +]); +// SELECT * FROM `table` WHERE (number > 10) AND (number < 100) +``` + +Modifiers or placeholders can also be used in expressions: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %and', [ + ['number > ?', 10], // or $database::expression('number > ?', 10) + ['number < ?', 100], + ['%or', [ + 'left' => 1, + 'top' => 2, + ]], +]); +// SELECT * FROM `table` WHERE (number > 10) AND (number < 100) AND (`left` = 1 OR `top` = 2) +``` + +The `%ex` modifier inserts all items of the array into SQL: + +```php +$result = $database->query('SELECT * FROM `table` WHERE %ex', [ + $database::expression('left = ?', 1), + 'AND', + 'top IS NULL', +]); +// SELECT * FROM `table` WHERE left = 1 AND top IS NULL +``` + + +Conditions in SQL Statements +============================ + +Conditional SQL statements are controlled by three modifiers: `%if`, `%else`, and `%end`. The `%if` must be at the end of the string representing SQL and is followed by a variable: + +```php +// $user = ???; + +$result = $database->query(' + SELECT * + FROM table + %if', isset($user), 'WHERE user=%s', $user, '%end + ORDER BY name +'); +``` + +The condition can be supplemented with an `%else` section: + +```php +$result = $database->query(' + SELECT * + FROM %if', $cond, 'one_table %else second_table +'); +``` + +Conditions can be nested within each other. + + +Identifiers and Strings in SQL +============================== + +SQL itself goes through processing to meet the conventions of the given database. Identifiers (table and column names) can be enclosed in square brackets or backticks, and strings in single or double quotes, but the server is always sent what the database requires. Example: + +```php +$database->query("UPDATE `table` SET [status]='I''m fine'"); +// MySQL: UPDATE `table` SET `status`='I\'m fine' +// ODBC: UPDATE [table] SET [status]='I''m fine' +``` + +Quotes inside strings in SQL are written by doubling them. + + +Result as Associative Array +=========================== + +Example: returns results as an associative array where the key will be the value of the `id` field: + +```php +$assoc = $result->fetchAssoc('id'); +``` + +The greatest power of `fetchAssoc()` is demonstrated in SQL queries joining several tables with different types of relationships. The database creates a flat table; fetchAssoc restores the shape. + +Example: Let's have a customer and order table (N:M relationship) and query: + +```php +$result = $database->query(' + SELECT customer_id, customers.name, order_id, orders.number, ... + FROM customers + INNER JOIN orders USING (customer_id) + WHERE ... +'); +``` + +And we'd like to get a nested associative array by Customer ID and then by Order ID: + +```php +$all = $result->fetchAssoc('customer_id|order_id'); + +// we will iterate like this: +foreach ($all as $customerId => $orders) { + foreach ($orders as $orderId => $order) { + // ... + } +} +``` + +The associative descriptor has a similar syntax to when you write arrays using assignment in PHP. Thus `'customer_id|order_id'` represents the assignment series `$all[$customerId][$orderId] = $row;` sequentially for all rows. + +Sometimes it would be useful to associate by the customer's name instead of their ID: + +```php +$all = $result->fetchAssoc('name|order_id'); + +// elements are then accessed like this: +$order = $all['Arnold Rimmer'][$orderId]; +``` + +But what if there are multiple customers with the same name? The table should have the form: + +```php +$row = $all['Arnold Rimmer'][0][$orderId]; +$row = $all['Arnold Rimmer'][1][$orderId]; +``` + +So we distinguish multiple possible Rimmers using a regular array. The associative descriptor again has a format similar to assignment, with sequential arrays represented by `[]`: + +```php +$all = $result->fetchAssoc('name[]order_id'); + +// we iterate all Arnolds in the results +foreach ($all['Arnold Rimmer'] as $arnoldOrders) { + foreach ($arnoldOrders as $orderId => $order) { + // ... + } +} +``` + +Returning to the example with the `customer_id|order_id` descriptor, let's try to list orders for each customer: + +```php +$all = $result->fetchAssoc('customer_id|order_id'); + +foreach ($all as $customerId => $orders) { + echo "Orders for customer $customerId:"; + + foreach ($orders as $orderId => $order) { + echo "Document number: $order->number"; + // customer name is in $order->name + } +} +``` + +It would be nice to display the customer name instead of ID. But we would have to look it up in the `$orders` array. So let's modify the results to have this shape: + +```php +$all[$customerId]->name = 'John Doe'; +$all[$customerId]->order_id[$orderId] = $row; +$all[$customerId]->order_id[$orderId2] = $row2; +``` + +So, between `$customerId` and `$orderId`, we insert an intermediate element. This time not the numbered indexes we used to distinguish individual Rimmers, but a database record directly. The solution is very similar - just remember that a record is symbolized by an arrow: + +```php +$all = $result->fetchAssoc('customer_id->order_id'); + +foreach ($all as $customerId => $row) { + echo "Orders for customer $row->name:"; + + foreach ($row->order_id as $orderId => $order) { + echo "Document number: $order->number"; + } +} +``` + + +Prefixes & Substitutions +======================== + +Table and column names can contain variable parts. You will first define them: + +```php +// create new substitution :blog: ==> wp_ +$database->getSubstitutes()->blog = 'wp_'; +``` + +and then use them in SQL. Note that in SQL they are enclosed in colons: + +```php +$database->query("UPDATE [:blog:items] SET [text]='Hello World'"); +// UPDATE `wp_items` SET `text`='Hello World' +``` + + +Field Data Types +================ + +Dibi automatically detects the types of individual query columns and converts cells to native PHP types. We can also specify the type manually. Possible types can be found in the [Dibi\Type |api:Dibi\Type] class. + +```php +$result->setType('id', Dibi\Type::INTEGER); // id will be integer +$row = $result->fetch(); + +is_int($row->id) // true +``` + + +Logging +======= + +Dibi has a built-in logger that lets you track all executed SQL statements and measure the duration of their execution. Activation: + +```php +$database->connect([ + 'driver' => 'sqlite', + 'database' => 'sample.sdb', + 'profiler' => [ + 'file' => 'file.log', + ], +]); +``` + +A more versatile profiler is the Tracy panel, which is activated when connecting to Nette. + + +Connect to [Nette |https://nette.org] +===================================== + +In the configuration file, we register the DI extension and add the `dibi` section - this creates the required objects and also the database panel in the [Tracy |https://tracy.nette.org] debugger bar. + +```neon +extensions: + dibi: Dibi\Bridges\Nette\DibiExtension3 + +dibi: + host: localhost + username: root + password: *** + database: foo + lazy: true +``` + +Then the connection object can be [obtained as a service from the DI container |https://doc.nette.org/di-usage], e.g.: + +```php +class Model +{ + private $database; + + public function __construct(Dibi\Connection $database) + { + $this->database = $database; + } +} +``` + + +Community Extensions +==================== + +Various libraries, ORMs and extensions are built on top of Dibi. You can find a complete list of them on "Packagist":https://packagist.org/packages/dibi/dibi/dependents?order_by=downloads&requires=require. + +{{maintitle: Dibi – Smart Database Abstraction Library for PHP}} diff --git a/dibi/en/@menu.texy b/dibi/en/@menu.texy new file mode 100644 index 0000000000..ba2e4587aa --- /dev/null +++ b/dibi/en/@menu.texy @@ -0,0 +1,3 @@ +- [Home | @home] +- "API .[link-external]":https://api.nette.org/dibi/ +- "GitHub .[link-external]":https://github.com/dibi/dibi diff --git a/dibi/en/@meta.texy b/dibi/en/@meta.texy new file mode 100644 index 0000000000..b9ca163d2f --- /dev/null +++ b/dibi/en/@meta.texy @@ -0,0 +1 @@ +{{sitename: Dibi Documentation}} diff --git a/dibi/meta.json b/dibi/meta.json new file mode 100644 index 0000000000..519fa582cd --- /dev/null +++ b/dibi/meta.json @@ -0,0 +1,5 @@ +{ + "version": "5.x", + "repo": "dibi/dibi", + "composer": "dibi/dibi" +} diff --git a/texy/cs/@home.texy b/texy/cs/@home.texy new file mode 100644 index 0000000000..3a23512250 --- /dev/null +++ b/texy/cs/@home.texy @@ -0,0 +1,32 @@ +Texy! je sexy! +============== + +Texy je program, díky kterému můžete snadno, bez odborných znalostí, psát texty na webové stránky. + +Chcete zvýraznit písmo? Vytvořit nadpis či odrážky? Přidat obrázek nebo tabulku? Nemusíte zápasit se složitým textovým editorem. Stačí psát prostý text a Texy už úpravu zvládne za vás. Výsledkem bude hezky zformátovaná stránka. + +--> [Vyzkoušejte si to | https://fiddle.nette.org/texy/] + +Texy dnes používají [tisíce spokojených uživatelů | napsali o Texy]. + + +Co všechno umí? +--------------- + +- vytvářet odkazy, odrážky, tabulky,... +- vkládat do textu obrázky +- zná českou typografii +- a navíc je **zdarma!** (pod licencí BSD a GPL) +- generuje vždy validní HTML kód +- vkládá pevné mezery za jednopísmenné předložky +- je dokonale konfigurovatelné a přizpůsobitelné + + +Objevte Texy! +------------- + +- Srovnání [Texy versus WYSIWYG editory | texy-vs-wysiwyg] +- [Příklady využití | priklady-vyuziti] +- [Základy syntaxe | syntax] + +{{maintitle: Texy – formátovač textů pro PHP}} diff --git a/texy/cs/@menu.texy b/texy/cs/@menu.texy new file mode 100644 index 0000000000..eb7458d858 --- /dev/null +++ b/texy/cs/@menu.texy @@ -0,0 +1,8 @@ +- [úvodní stránka | @home] +- [syntax stručně | syntax] +- [syntax podrobně | syntax-podrobne] +- [fiddle | https://fiddle.nette.org/texy/] +- [manuál | api] +- [blog | https://phpfashion.com/category/texy] +- [API | https://api.nette.org/texy/] +- [GitHub | https://github.com/dg/texy] diff --git a/texy/cs/@meta.texy b/texy/cs/@meta.texy new file mode 100644 index 0000000000..41e2da6970 --- /dev/null +++ b/texy/cs/@meta.texy @@ -0,0 +1 @@ +{{sitename: Texy Dokumentace}} diff --git a/texy/cs/@try.texy b/texy/cs/@try.texy new file mode 100644 index 0000000000..9c443df131 --- /dev/null +++ b/texy/cs/@try.texy @@ -0,0 +1,15 @@ +Vítejte! +-------- + +Můžete používat syntax Texy!, pokud Vám vyhovuje: +- třeba **tučné** písmo nebo *kurzíva* +- a takto se dělá "odkaz":https://texy.info +- více najdete na stránce "syntax":[syntax] + + +Ale také můžete zůstat u HTML: +- takto HTML +- nebo i úplně hloupě, Texy! to pořeší + + +[syntax]: /cs/syntax diff --git a/texy/cs/api-block-module.texy b/texy/cs/api-block-module.texy new file mode 100644 index 0000000000..aae50f9b19 --- /dev/null +++ b/texy/cs/api-block-module.texy @@ -0,0 +1,11 @@ +Třída Texy\Modules\BlockModule +****************************** + +Má na starosti zpracování bloků `/-- xxx`. Modul vypneme zakázáním syntaxe `blocks`: + +/--code php +$texy->allowed['blocks'] = false; + +// nebo pro jednotlivé typy bloků +$texy->allowed['block/code'] = false; +\-- diff --git a/texy/cs/api-blockquote-module.texy b/texy/cs/api-blockquote-module.texy new file mode 100644 index 0000000000..b9fd28e72a --- /dev/null +++ b/texy/cs/api-blockquote-module.texy @@ -0,0 +1,8 @@ +Třída Texy\Modules\BlockQuoteModule +*********************************** + +Má na starosti blokové citace. Modul vypneme zakázáním syntaxe `blockquote`: + +/--code php +$texy->allowed['blockquote'] = false; +\-- diff --git a/texy/cs/api-emoticon-module.texy b/texy/cs/api-emoticon-module.texy new file mode 100644 index 0000000000..8f0f8fa475 --- /dev/null +++ b/texy/cs/api-emoticon-module.texy @@ -0,0 +1,47 @@ +Třída Texy\Modules\EmoticonModule +********************************* + +Má na starosti nahrazování smajlíků za obrázky. Ve výchozím nastavení je modul vypnutý, zapneme jej povolením syntaxe `emoticon`: + +/--code php +$texy->allowed['emoticon'] = true; +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $icons | array | | tabulka všech smajlíků a odpovídajících obrázků +| $class | string | null | CSS třída +| $root | string | | kořenový adresář obrázků na webu +| $fileRoot | string | | kořenový adresář obrázků na disku + +Pokud nenastavíte hodnoty pro `$root` a `$fileRoot`, použijí se ty z modulu [Texy\Modules\ImageModule|api-image-module]. Výchozí seznam smajlíků je tento: + +/--code php +$icons = [ + ':-)' => 'smile.gif', + ':-(' => 'sad.gif', + ';-)' => 'wink.gif', + ':-D' => 'biggrin.gif', + '8-O' => 'eek.gif', + '8-)' => 'cool.gif', + ':-?' => 'confused.gif', + ':-x' => 'mad.gif', + ':-P' => 'razz.gif', + ':-|' => 'neutral.gif', +]; +\-- + +Můžete jej pozměnit přímo zásahem do pole: + +/--code php +$texy->emoticonModule->icons[':-)'] = 'smile.png'; + +unset($texy->emoticonModule->icons[':-P']); +\-- + +Poslední znak smajlíku se může opakovat, tedy klíč. `:-)` akceptuje i smajlík v podobě `:-)))`. diff --git a/texy/cs/api-figure-module.texy b/texy/cs/api-figure-module.texy new file mode 100644 index 0000000000..db0d483886 --- /dev/null +++ b/texy/cs/api-figure-module.texy @@ -0,0 +1,33 @@ +Třída Texy\Modules\FigureModule +******************************* + +Má na starosti obrázky s popiskou. Module vypneme zakázáním syntaxe `figure`: + +/--code php +$texy->allowed['figure'] = false; +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $class | string | `'figure'` | třída neplovoucího kontejneru `
` +| $leftClass | string | null | třída `
` plovoucího vlevo +| $rightClass | string | null | třída `
` plovoucího vpravo +| $widthDelta | int | 10 | offset pro výpočet šířky + + +Neplovoucím kontejnerům `
` bude přiřazena třída `$class`, plovoucím `$leftClass` nebo `$rightClass`. Pokud není specifikována třída u plovoucích kontejnerů, Texy ji sestaví takto: + +`$texy->figureModule->class . '-' . $texy->alignClasses['left'] resp. 'right'` + +Není-li nastavená třída ani v poli `$alignClasses`, Texy kontejner zarovnává CSS vlastností `float`. + +U plovoucích kontejnerů Texy počítá a nastavuje šířků podle vzorce: + +`šířka
= šířka obrázku + $widthDelta` + +Je tedy nutné zjistit šířku obrázku. K tomu je potřeba korektně nastavit cestu `$texy->imageModule->fileRoot`, viz [Texy\Modules\ImageModule | api-image-module]. diff --git a/texy/cs/api-heading-module.texy b/texy/cs/api-heading-module.texy new file mode 100644 index 0000000000..701de25493 --- /dev/null +++ b/texy/cs/api-heading-module.texy @@ -0,0 +1,42 @@ +Třída Texy\Modules\HeadingModule +******************************** + +Má na starosti nadpisy a titulky. Module (de)aktivujeme zakázáním syntaxe: + +/--code php +// podtržené titulky +$texy->allowed['heading/underlined'] = false; + +// ohraničené titulky +$texy->allowed['heading/surrounded'] = false; +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $title | string | null | nejvyšší titulek +| $TOC | array | | zde se vygeneruje obsah +| $generateID | boolean| false | generovat titulkům ID? +| $idPrefix | string | `'toc-'` | prefix pro generované ID +| $top | int | 1 | strop, úroveň nejvyššího titulku +| $moreMeansHigher | boolean | true | více znaků znamená vyšší titulek +| $balancing | int | *dynamic* | způsob vážení titulků +| $levels | array | | tabulka vážení titulků + +Po zpracování textu bude první titulek uložen do proměnné $title (bez HTML kódování, tedy vhodné pro použití v ``). + +Způsob vážení titulků určuje $balancing, které může nabývat hodnot `Texy\Modules\HeadingModule::DYNAMIC` (výchozí) nebo `Texy\Modules\HeadingModule::FIXED`. Více informací [ve fóru | https://forum.texy.info/cs/viewtopic.php?pid=22]. + + +Příklady +-------- + +Nejvyšší titulek bude mít úroveň `<h2>` + +/--code php +$texy->headingModule->top = 2; +\-- diff --git a/texy/cs/api-horizline-module.texy b/texy/cs/api-horizline-module.texy new file mode 100644 index 0000000000..ebbd47da44 --- /dev/null +++ b/texy/cs/api-horizline-module.texy @@ -0,0 +1,8 @@ +Třída Texy\Modules\HorizLineModule +********************************** + +Má na starosti horizontální čáry. Modul vypneme zakázáním syntaxe `horizline`: + +/--code php +$texy->allowed['horizline'] = false; +\-- diff --git a/texy/cs/api-html-module.texy b/texy/cs/api-html-module.texy new file mode 100644 index 0000000000..f925f2ade8 --- /dev/null +++ b/texy/cs/api-html-module.texy @@ -0,0 +1,21 @@ +Třída Texy\Modules\HtmlModule +***************************** + +Má na starosti HTML značky a komentáře na vstupu. Modul vypneme zakázáním syntaxe: + +/--code php +// vypneme HTML značky +$texy->allowed['html/tag'] = false; + +// vypneme HTML komentáře +$texy->allowed['html/comment'] = false; +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $passComment | boolean | true | zobrazit HTML komentáře na výstupu? diff --git a/texy/cs/api-htmloutput-module.texy b/texy/cs/api-htmloutput-module.texy new file mode 100644 index 0000000000..ec049261fa --- /dev/null +++ b/texy/cs/api-htmloutput-module.texy @@ -0,0 +1,17 @@ +Třída Texy\Modules\HtmlOutputModule +*********************************** + +Má na starosti formátování výstupního XHTML / HTML. Formátuje výsledné HTML a zároveň hlídá a koriguje "wellformed" zápis. + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $indent | boolean | true | formátovat výstup do úhledné podoby? +| $baseIndent | int | 0 | minimální odsazení každého řádku +| $lineWrap | int | 80 | maximální šířka řádku + +Volitelné koncové značky se odstraňují pouze v režimu HTML. Zalamování řádků lze vypnout nastavením `$texy->htmlOutputModule->lineWrap = false`. diff --git a/texy/cs/api-image-module.texy b/texy/cs/api-image-module.texy new file mode 100644 index 0000000000..bebde249b3 --- /dev/null +++ b/texy/cs/api-image-module.texy @@ -0,0 +1,35 @@ +Třída Texy\Modules\ImageModule +****************************** + +Má na starosti obrázky. Tzv. obrázky s popiskou zpracovává [Texy\Modules\FigureModule|api-figure-module]. Modul vypneme zakázáním syntaxe: + +/--code php +// zákaz obrázků +$texy->allowed['image'] = false; + +// zákaz referencí +$texy->allowed['image/definition'] = false; +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $root | string | `'images/'` | kořenový adresář obrázků na webu +| $linkedRoot | string | `'images/'` | kořenový adresář obrázků v pozici odkazu +| $fileRoot | string | | kořenový adresář obrázků na disku +| $leftClass | string | null | třída obrázku plovoucího vlevo +| $rightClass | string | null | třída obrázku plovoucího vpravo +| $defaultAlt | string | `''` | výchozí hodnota atributu `alt` + + +Fyzická cesta k obrázkům `$fileRoot` se používá ke zjištění jich rozměrů, které se pak uvedou v HTML výstupu. Modul se cestu pokusí detekovat z prostředí serveru, nicméně je vhodnější ji nastavit manuálně. + +Proměnná `$linkedRoot` určuje kořenový adresář obrázků, které jsou použity jako cíl odkazu, tedy `[odkaz | * img.jpg *]`. + +Plovoucím obrázkům je nastavena třída `$leftClass` resp. `$rightClass`, nebo `$texy->alignClasses['left']` resp. `'right'`. Pokud není ani jedno specifikováno, Texy obrázky zarovnává CSS vlastností `float`. + +Protože Texy umí překlápět obrázky při přejetí myškou (onmouseover efekt), je nutné zajistit včasné načtení těchto obrázků (preload). K tomu slouží děsně fikaný skript, který se aktivuje událostí `onload` u jednotlivých obrázků. Skript je uložen v proměnné `$onLoad`. diff --git a/texy/cs/api-link-module.texy b/texy/cs/api-link-module.texy new file mode 100644 index 0000000000..f9fc26e49a --- /dev/null +++ b/texy/cs/api-link-module.texy @@ -0,0 +1,42 @@ +Třída Texy\Modules\LinkModule +***************************** + +Má na starosti definice, reference, odkazy. Modul vypneme zakázáním syntaxe: + +/--code php +// vypnout zpracování definicí [ref]: www.texy.info +$texy->allowed['link/definition'] = false; + +// vypnout zpracování referencí [ref] +$texy->allowed['link/reference'] = false; + +// vypnout dělání www adres klikatelnými +$texy->allowed['link/url'] = false; + +// vypnout dělání emailových adres klikatelnými +$texy->allowed['link/email'] = false; +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $root | string | `''` | kořenový adresář relativních odkazů +| $imageClass | string | | třída pro odkazy vedoucí na obrázky (od Texy 2.2) +| $forceNoFollow | boolean | false | doplňovat `rel="nofollow"`? +| $shorten | boolean | true | zkracovat URL? + +Texy automaticky doplňuje atribut nofollow odkazům, které mají pseudotřídu `nofollow`, např. `"odkaz .[nofollow]":www.texy.info`. + + +Příklad +------- + +Chceme přidávat nofollow ke všem odkazům + +/--code php +$texy->linkModule->forceNoFollow = true; +\-- diff --git a/texy/cs/api-list-module.texy b/texy/cs/api-list-module.texy new file mode 100644 index 0000000000..222f9d94e4 --- /dev/null +++ b/texy/cs/api-list-module.texy @@ -0,0 +1,12 @@ +Třída Texy\Modules\ListModule +***************************** + +Má na starosti číslované, nečíslované a definiční seznamy. Modul vypneme zakázáním syntaxe: + +/--code php +// číslované a nečíslované seznamy +$texy->allowed['list'] = false; + +// definiční listy +$texy->allowed['list/definition'] = false; +\-- diff --git a/texy/cs/api-longwords-module.texy b/texy/cs/api-longwords-module.texy new file mode 100644 index 0000000000..00c5ca765a --- /dev/null +++ b/texy/cs/api-longwords-module.texy @@ -0,0 +1,19 @@ +Třída Texy\Modules\LongWordsModule +********************************** + +Má na starosti rozdělení dlouhých slov. Modul vypneme zakázáním syntaxe `longwords`: + +/--code php +$texy->allowed['longwords'] = false; +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $wordLimit | int | 20 | maximální délka slova + +Texy vkládá do příliš dlouhých slov (nad $wordLimit) značku volitelného zalomení `­`. Snaží se přitom respektovat specifika dělení slov na slabiky. diff --git a/texy/cs/api-paragraph-module.texy b/texy/cs/api-paragraph-module.texy new file mode 100644 index 0000000000..28b10e479e --- /dev/null +++ b/texy/cs/api-paragraph-module.texy @@ -0,0 +1,4 @@ +Třída Texy\Modules\ParagraphModule +********************************** + +Má na starosti jednotlivé odstavce textu. diff --git a/texy/cs/api-phrase-module.texy b/texy/cs/api-phrase-module.texy new file mode 100644 index 0000000000..c978f68adc --- /dev/null +++ b/texy/cs/api-phrase-module.texy @@ -0,0 +1,22 @@ +Třída Texy\Modules\PhraseModule +******************************* + +Má na starosti tzv. fráze, tedy úseky textu (tučný text, odkaz, ...). Můžeme vypínat a zapínat jednotlivé syntaxe: + +/--code php +$texy->allowed['phrase/strong'] = false; +... +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $tags | array | | HTML značky pro jednotlivé fráze +| $linksAllowed | boolean | true | je možné k frázi přidat odkaz? + + +Pod pojmem fráze se rozumí jakákoliv řádková syntax jako `**tučné**`, `//kurzíva//`, `subskript_2`, `[odkaz | www.texy.info`]. diff --git a/texy/cs/api-script-module.texy b/texy/cs/api-script-module.texy new file mode 100644 index 0000000000..0e3ace4d82 --- /dev/null +++ b/texy/cs/api-script-module.texy @@ -0,0 +1,23 @@ +Třída Texy\Modules\ScriptModule +******************************* + +Má na starosti volání uživatelských funkcí a vkládání externích dat. Modul vypneme zakázáním syntaxe `script`: + +/--code php +$texy->allowed['script'] = false; +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $separator | string | `';'` | oddělovat argumentů + + +Příklady +-------- + +Najdete ve [fóru | https://forum.texy.info/cs/viewtopic.php?pid=1954]. diff --git a/texy/cs/api-table-module.texy b/texy/cs/api-table-module.texy new file mode 100644 index 0000000000..6114f24229 --- /dev/null +++ b/texy/cs/api-table-module.texy @@ -0,0 +1,18 @@ +Třída Texy\Modules\TableModule +****************************** + +Má na starosti tabulky. Modul vypneme zakázáním syntaxe `table`: + +/--code php +$texy->allowed['table'] = false; +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $oddClass | string | null | CSS třída přiřazená lichým řádkům tabulky +| $evenClass | string | null | CSS třída přiřazená sudým řádkům tabulky diff --git a/texy/cs/api-texy.texy b/texy/cs/api-texy.texy new file mode 100644 index 0000000000..6eba43a35c --- /dev/null +++ b/texy/cs/api-texy.texy @@ -0,0 +1,69 @@ +Třída Texy\Texy +*************** + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| `$allowed` | mixed | | povolené "Texy syntaxe" +| `$allowedTags` | mixed | *validní značky* | povolené HTML značky +| `$allowedClasses` | mixed | Texy::ALL | povolené CSS třídy a identifikátory +| `$allowedStyles` | mixed | Texy::ALL | povolené CSS styly +| `$alignClasses` | array | *vynulované* | CSS třídy pro zarovnání textu a obrázků +| `$mergeLines` | boolean | true | spojovat řádky? +| `$tabWidth` | int | 8 | šířka tabulátorů kvůli převední na mezery +| `$obfuscateEmail` | boolean| true | maskovat emailové adresy před roboty? +| `$urlSchemeFilters` | array| null | + + +Proměnné `$allowedTags`, `$allowedClasses` a `$allowedStyles` mohou nabývat hodnot: + +- `Texy::ALL` - jsou povoleny všechny HTML značky resp. styly resp. třídy +- `Texy::NONE` - naopak jsou všechny zakázány +- *array* - výčet povolených hodnot + - `$allowedTags` - povolené značky tvoří klíče, viz příklady níže + - `$allowedClasses` - seznam tříd a ID, přičemž ID začínají prefixem # + - `$allowedStyles` - seznam CSS vlastností + +Pole `$alignClasses` definuje CSS třídy pro zarovnání textu a obrázků. Blíže popsáno [ve fóru | https://forum.texy.info/cs/viewtopic.php?id=532]. + +Texy spojuje řádky následující za sebou do odstavců. Toto chování je možné vypnout nastavením vlastnosti `$texy->mergeLines = false`. + + +Příklady +-------- + +Povolíme pouze HTML elementy `<strong>, <div>, <a>` a některé jejich atributy: + +/--code php +$texy->allowedTags = [ + 'strong' => Texy::NONE, // <strong> nesmí mít žádné attributy + 'div' => Texy::ALL, // <div> může mít jakékoliv atributy + 'a' => ['href', 'lang', 'target'], // <a> může mít jen tyto atributy +]; +\-- + +Všechny HTML značky zakážeme: + +/--code php +$texy->allowedTags = Texy::NONE; +\-- + +Povolíme pouze CSS třídy `class1, class2` a CSS identifikátory `id1, id2` + +/--code php +$texy->allowedClasses = ['class1', 'class2', '#id1', '#id2']; +\-- + +Povolíme pouze CSS vlastnosti `font-size, color, width` + +/--code php +$texy->allowedStyles = ['font-size', 'color', 'width']; +\-- + +Místo přímých stylů `style="text-align:left"` apod. používej třídy `class="left"` apod. + +/--code php +$texy->alignClasses['left'] = 'left'; +$texy->alignClasses['right'] = 'right'; +... // dále možno definovat: center, justify, top, bottom, middle +\-- diff --git a/texy/cs/api-typography-module.texy b/texy/cs/api-typography-module.texy new file mode 100644 index 0000000000..a66748609f --- /dev/null +++ b/texy/cs/api-typography-module.texy @@ -0,0 +1,21 @@ +Třída Texy\Modules\TypographyModule +*********************************** + +Má na starosti typografické úpravy výsledného textu. Modul vypneme zakázáním syntaxe `typography`: + +/--code php +$texy->allowed['typography'] = false; +\-- + + +Konfigurace +----------- + +|---------------- +| proměnná | typ | výchozí | popis | +|---------------- +| $locale | string | `'cs'` | určení národních specifik +| static $locales | array | | předvolená specifika + + +Vlastnost $locale může nabývat hodnot: cs, en, fr, de, pl. Další specifika lze doplnit do statické proměnné $locales. diff --git a/texy/cs/api-zaklady.texy b/texy/cs/api-zaklady.texy new file mode 100644 index 0000000000..3672e3ba65 --- /dev/null +++ b/texy/cs/api-zaklady.texy @@ -0,0 +1,25 @@ +Základní dovednosti +******************* + +Jak přeformátovat text do HTML? Stačí do kódu začlenit knihovnu Texy, například pomocí Composeru: + +/-- +composer require texy/texy +\-- + +A vytvořit objekt `$texy = new Texy\Texy;`. Celý převod obstará metoda `$html = $texy->process($text)`, kde proměnná `$text` obsahuje vstupní text a vrací se zformátovaný HTML výstup. + +/--php +// vytvoříme objekt +$texy = new Texy\Texy; + +// můžeme jej nakonfigurovat +$texy->imageModule->root = 'images/'; + +// a zpracujeme vstupní $text +$html = $texy->process($text); +\-- + +Pokud potřebujete formátovat jednořádkový text (tedy bez blokových elementů), použijte `$html = $texy->processLine($text)`. + +V ukázce vidíte, jak je možné objekt `$texy` konfigurovat. Protože parametrů je hodně, jsou rozčleněny do logických celků označovaných jako moduly. Instance každého modulu je uložena v atributu `$texy->blockModule`, `$texy->emoticonModule` atd. (viz [jednotlivé moduly | api#moduly]). diff --git a/texy/cs/api.texy b/texy/cs/api.texy new file mode 100644 index 0000000000..e0d5c9e4db --- /dev/null +++ b/texy/cs/api.texy @@ -0,0 +1,28 @@ +Programátorský manuál +********************* + +Texy je napsané v objektovém PHP. Minimální požadovaná verze pro Texy 3.0 je PHP 7.1. + +--> [Základní dovednosti | api-zaklady] + +--> [API dokumentace | https://api.nette.org/texy/] + +--> Jednotlivé moduly: .[#moduly] +- [Texy\Texy | api-texy] - jádro Texy +- [Texy\Modules\BlockModule | api-block-module] - zpracování bloků `/-- xxx` +- [Texy\Modules\BlockQuoteModule | api-blockquote-module] - blokové citace +- [Texy\Modules\EmoticonModule | api-emoticon-module] - nahrazování smajlíků za obrázky +- [Texy\Modules\FigureModule | api-figure-module] - obrázky s popiskou +- [Texy\Modules\HeadingModule | api-heading-module] - nadpisy, titulky +- [Texy\Modules\HorizLineModule | api-horizline-module] - horizontální čáry +- [Texy\Modules\HtmlModule | api-html-module] - HTML značky a komentáře na vstupu +- [Texy\Modules\HtmlOutputModule | api-htmloutput-module] - formátování výstupního HTML +- [Texy\Modules\ImageModule | api-image-module] - obrázky +- [Texy\Modules\LinkModule | api-link-module] - odkazy a reference +- [Texy\Modules\ListModule | api-list-module] - číslované, nečíslované a definiční seznamy +- [Texy\Modules\LongWordsModule | api-longwords-module] - rozdělení dlouhých slov +- [Texy\Modules\ParagraphModule | api-paragraph-module] - jednotlivé odstavce textu +- [Texy\Modules\PhraseModule | api-phrase-module] - fráze, tedy úseky textu (tučný text, odkaz, ...) +- [Texy\Modules\ScriptModule | api-script-module] - volání uživatelských funkcí +- [Texy\Modules\TableModule | api-table-module] - tabulky +- [Texy\Modules\TypographyModule | api-typography-module] - typografické úpravy výsledného textu diff --git a/texy/cs/napsali-o-texy.texy b/texy/cs/napsali-o-texy.texy new file mode 100644 index 0000000000..dc3caef0dd --- /dev/null +++ b/texy/cs/napsali-o-texy.texy @@ -0,0 +1,160 @@ +Napsali o Texy +************** + +{{nofollow:yes}} + +> Při volbě formátovače textu pro poslední dva weby jsme zkusili oproti dříve používaným WYSIWYG editorům implementovat systém Texy Davida Grudla, a nestačili jsme se divit. Neuvěřitelně komplexní formátovací možnosti, úžasná podpora české typografie a předem připravené instalační balíčky dělají Texy vynikajícím publikačním doplňkem. +> +> [Jan Brašna Alphanumeric | http://www.alphanumeric.cz] (3. 5. 2005) + + +> Dobrý den, s Texy jsem neuvěřitelně spokojen. Mnohokráte děkuji. +> +> Využíváme Texy jak na firemních stránkách www.logio.cz v sekci Novinky tak na našem novém projektu www.skladuj.cz. +> +> Dokonce si někteří kolegové navykli posílat příspěvky již předformátované v emailu. Což je neuvěřitelné. +> +> [Tomáš Formánek | http://www.skladuj.cz] (10. 4. 2006) + + +> Zdravím. Texy jsem začal používat na nokturno.net - je to luxusní věcička, hlavně dá minimálně práce zakomponovat jej do systému. +> +> [Jiří Reiter | http://www.nokturno.net] (5. 3. 2006) + + +> Použil jsem jej pro formátování aktuálních zpráviček na našem firemním webu. Celé zahrnutí trvalo cca hodinu, včetně pochopení o co jde a zavedení příznaku pro přepínání pro starší zprávy v HTML. Zadávání je teď výrazně intuitivnější a je menší riziko "rozhození" formátování stránek při ev. chybě v textu zprávy. +> +> Brilantní kus kódu, jak návrh, tak realizace. +> +> [Ing. Zdeněk Trojánek | http://www.daisy.cz] (3. 3. 2006) + + +> Nejprve jsem chtěl do svého implementovat nějaký wysiwyg editor, ale našel jsem Texy. +> +> Něco podobného jsem zatím nevidel, uplně nadchnul. Dokud nevyzkoušíte neuvěříte. +> +> [Petr Čada | http://error414.php5.cz/] (24. 11. 2005) + + + +> Texy je velmi šikovná a praktická věc, která dokáže i překvapit... spokojenost je určitě na místě. +> +> [Petr Vlček | http://saabinfo.net] (4. 10. 2005) + + +> Na Texy ma prekvapila jeho komplexnosť a sila. Umožňuje formátovať text akokoľvek len chcete, pritom dbá aj na správne postavenie predložiek a rozdelenie dlhých slov atď. Rozhodne Texy vyskúšajte. +> +> [Michal Poppe | http://www.mipo.ssag.sk/zapisnik/webowiny/2005-02-08-texy-konecne-vonku.html] (8. 2. 2005) + + +> Texy jsem implementoval do nekomerční obrázkové encyklopedie. Texy se mi stará o doprovodné texty k tématickým sekcím a já mám z toho, jak to dělá (dělá to hezky sexy ;-) ), **velikou radost**. Texy mi ušetří čas, a tak mám Texy rád a autorovi za něj děkuji tímto a ikonkou. +> +> Hlavně oceňuji logickou a jenoduchou syntaxi, jednoduchou implementaci do jiných php systémů, komplexnost. :) +> +> [Robert Nový | http://www.jablicko.cz] (18. 4. 2005) + + +> Texy používám už delší dobu jako svou hlavní pomůcku pro převod textů do HTML, což dělám v práci vlastně denně. +> +> [Jirka Chomát | http://www.chomat.net/articles/trublog] (5. 3. 2005) + + +> Ano je to tak. Skutečně používáme tento skvělý "převaděč textu do formátovaného HTML kódu". +> +> [WordPress CZ | http://wordpress.cz] (24. 2. 2005) + + +> **Elegance v phpRS .. to je Texy** Osobně mi vůbec nevadí psát příspěvky včetně TAGů, mám potom vše pod kontrolou a vím "wo co de !". Proč si ale neušetřit práci použitím formátovače Texy. Přeci jen jsem občas líný tvor a Texy je podle mě opravdu super. Celá operace "zasunutí" Texy do phpRS je velice jednoduchá +> +> [Pykaso | http://pykaso.net/?article=elegance-v-phprs-to-je-texy] (11. 3. 2005) + + +> Texy, skvělý nástroj od Davida Grudla (dgx), který usnadní práci nejen pisálkům, ale i těm, kteří komentují, jsem původně zamýšlel používat pouze k formátování komentářů - ode dneška jej používám i k formátování mnou napsaných článků. +> +> A co mě k tomu vedlo? Pohodlost! Člověk by nevěřil, jak je všechno najednou jednoduché +> +> [Luboš Bretschneider | http://www.bretik.com/?page=article&article=Texy-je-sexy-Texy-je-cool] (2. 3. 2005) + + +> Texy jsem si zaimplementoval do webu spíš jen tak. Chtěl jsem ho vyzkoušet a napadlo mě, že to můžu zkusit rovnou v reálu. Nečekal jsem nic nepřevratného, a to byla chyba. Texy mě úplně vzalo. +> +> Dnes už vůbec neuvažuji nad WYSIWYG editorem. Texy je pro mě jasná volba pro jednoduché, ale i složité formátování textů do XHTML. Což je věc, kterou Texy zvládá na jedničku! +> +> [Lukáš Knop, Knopdesign | http://www.knopdesign.net] (17. 3. 2005) + + +> **Texy je opravdu sexy** Texy využívám i já. Texy totiž není program jen pro neznalé HTML a počítačové analfabety, ale i pro ostřílené webdevelopery. Dnes jsem ho využil k převedení dlouhého textu do kódu pro jedny stránky, na kterých teď usilovně pracuji. Usnadní mi nudnou práci a tím pádem jsem o něco efektivnější... +> +> [Ondřej Kůrka | http://bernardyn.bloguje.cz/109088_item.php/] (1. 2. 2005) + + +> Články se publikují v podstatě sami, protože využívám Texy, jehož stvořitelem je David Grudl. +> +> [Lazy Byte | http://lazybyte.wz.cz/blog/za%c4%8dinam-blogovat] (6. 3. 2005) + + +> Kód je napsán hezky objektově a přehledně, navíc řekl bych i hodně univerzálně, autor s řadou věcí počítá a tak je radost s Texy pracovat. Podpora UTF-8 hned v základu a hlavně dgx vážně nekecal, když psal, že zapracování do kódu bude snadné. +> +> [Vojtěch Schlesinger | http://www.php-weblog.com] (1. 2. 2005) + + +> Budu se ale muset hodně ovládat, abych nepsal s dokonalou Texy syntaxí ... tedy ne že bych ji já psal dokonale, ale že ona je dokonalá. S klidem přiznám, že jsem z Texy už několik dní nepokrytě nadšený. Takový vybroušený kousek php kódu jsem ještě neviděl. +> +> [Juneau | http://reality-show.net/?text=486-a-tak-si-mezi-programovanim-blognu] (26. 2. 2005) + + +> Texy hodnotím výborně, velice mi usnadňuje psaní článků. A plugin pro BLOG:CMS je taky super věc! :-) DĚKUJU ZA TEXY! :-) +> +> [Miroslav Navrátil | http://kanevinternetu.blacksuns.net/] (14. 3. 2005) + + +> ... umožní jednoduché a intuitivní formátování textu bez znalosti HTML, čistě za použití plain textu. ... Implementace systému do existující PHP aplikace už snad ani nemůže být jednodušší, stačí includovat jeden soubor a vytvořit jeden objekt, Texy se postará o zbytek +> +> [Adam Šindelář, Root.cz | http://www.root.cz/clanky/nova-softwarova-sklizen-16-3-2005/] (16. 3. 2005) + + +> Jsem se zas jednou nudil, serfoval po netu a narazil na Texy, mno a to mě tak nadchlo až jsem z toho začal předělávat celej webík. Texy vřele doporučuji všem, kteří jsou aspoň z poloviny tak líní jako já. Je vhodný jak pro laiky tak pro zkušené programátory. Je prostě sexy! +> +> [Rozi.net | http://www.rozi.net/text-32.html] (11. 3. 2005) + + +> Izsak's Weblog používa na formátovanie článkov a komentárov nový, jednoduchý a komplexný systém Texy. +> +> [Jozef Izso | http://www.izsak.net/weblog/47/prechod-na-textpattern] (23. 2. 2005) + + +> Zdravím! Texy využívám v mém blogu, sám bych texy asi nedokázal zasadit do nějakého rs, ale RS2 ho obsahuje a tak ho využívám, jsem naprosto spokejen, texy mi vyhovuje, práce s ním je hned příjemnější. Přeji hodně úspěchů. +> +> [Martin Světlík alias QuickShare | http://blog.msvetlik.com] (20. 4. 2006) + +> Cau, ted sem se dostal k Texy! Zatim pouzivam WYSIWYG editor napsany v JS. Dobry, jen nekdy pomaly a ten kod taky nic moc. Navic JS pouzivam nerad. Cetl sem si jak je to udelany a musim rict ze uvazuju nad tim ze bych to cely nahradil :o) +> +> Kanadsky bod pro tebe...kdyz sem se dival na formatovani tabulek... no musel jsi s tim mit strasnou praci. +> +> Keep on ;) +> +> [Marek | liq@quick.cz] (17. 11. 2005) + + +> Konečně mám (po hokeji) zase jednou důvod být hrdý, že jsem Čech, stejně jako autor Texy ;) Moc pěkný kousek software! Smekám... +> +> [Pavel Beníšek | http://www.3dgrafika.cz] (17. 3. 2005) + + +Dále Texy používá +----------------- + +- síť obchodů [Internet Mall | http://www.mall.cz/] +- [H1.cz | http://www.h1.cz/] +- [Vitalita | http://www.vitalita.cz] +- [Václavák | http://www.vaclavak.net] +- [Rarouš weblog | http://rarous.net/] (běží na ASP.NET) +- [La Trine | https://www.latrine.cz] +- **...a stovky dalších webů** + + +Texy najdete v systémech: +------------------------- + +- [Český TextPattern | http://www.vaclavak.net/weblog/23/textpattern-pro-ceske-prostredi] +- Český WordPress diff --git a/texy/cs/priklady-vyuziti.texy b/texy/cs/priklady-vyuziti.texy new file mode 100644 index 0000000000..f71b9616dc --- /dev/null +++ b/texy/cs/priklady-vyuziti.texy @@ -0,0 +1,26 @@ +Příklady využití +**************** + + +Běžní uživatelé .[#bfu] +----------------------- + +Texy původně vznikl jako nástroj, který umožnil i uživatelům neznalým HTML **snadno editovat obsah** webových stránek. Záměrem bylo vytvořit silnou **alternativu k WYSIWYG** editorům, které se v praxi [ukazují jako neefektivní | texy-vs-wysiwyg]. + +Běžný uživatel se může plně věnovat psaní "čistého textu .(například v Poznámkovém bloku)[about]", který si formátuje velmi [přirozeným způsobem | syntax]. A nemusí se téměř nic nového učit. + + +Zkušení pisatelé .[#experienced] +-------------------------------- + +Zkušení tvůrci internetového obsahu, jakými jsou například bloggeři, často jazyk HTML znají. Ale psát články přímo v něm je nepříjemné a proto si zjednodušují práci používáním chytrých editorů. Jedním z nich je i Texy Je navíc dostupný i přes webové rozhraní a nabízí **neobvykle vysoký konfort**. + +Díky Texy se autor může plně věnovat obsahu dokumentu a nemusí uvažovat nad HTML nebo typografickou úpravou. Texy dokáže psaní výrazně zjednodušit, přitom pokročilého uživatele nijak neomezuje - nabízí mu **vkládat i HTML značky** a CSS formátování. + + +Komentáře a diskuzní fóra .[#comments] +-------------------------------------- + +Texy počítá i s nasazením jako formátovač příspěvků v diskuzních fórech a komentářích. Přispívatelé mohou používat jednotnou a **intuitivní syntaxi** a programátorům **odpadne náročná práce** na vlastním formátovači. + +Tato oblast použití je charakteristická tím, že vyžaduje mnohem **přísnější kontrolu** vstupů. Texy proto obsahuje mechanismy, které zakáží nebo omezí použití určitých HTML značek (a jejich atributů), kaskádových stylů a tříd atd. diff --git a/texy/cs/syntax-podrobne.texy b/texy/cs/syntax-podrobne.texy new file mode 100644 index 0000000000..d10b62068c --- /dev/null +++ b/texy/cs/syntax-podrobne.texy @@ -0,0 +1,889 @@ +Podrobný popis syntaxe +********************** + + +- [#Filozofie] +- [#Odstavce textu] +- [#Titulky] +- [#Horizontální čáry] +- [#Kód] +- [#Vypnutí Texy] +- [#Citace] +- [#Odkazy] +- [#Obrázky] +- [#Fráze] +- [#Přímé HTML] +- [#Seznamy] +- [#Modifikátory] +- [#Typografie] +- [#Rozdělení velmi dlouhých slov] +- [#Tabulky] + + +Filozofie +========= + +Nástroj Texy vznikl proto, aby nezkušeným uživatelům umožnil snadno editovat obsah webových stránek. Proto je i syntaxe maximálně intuitivní. Záměrem je, aby text v čisté (nezformátované) formě byl přehledný a jeho formát tušitelný. + +Dnes Texy výborně slouží i zkušeným znalcům jazyka HTML. Dovoluje volně kombinovat Texy zápis s HTML značkami. Zkušení uživatelé se tedy nemusí učit nový meta-jazyk a plně využít svých znalostí. Texy jim pouze zjednodušuje práci. + +Prvotní logikou syntaxe je **žádnou syntaxi nepoužívat**. Jen psát čistý text. Vkládání rozšířených informací, jako třeba CSS třídy nebo odkazy, nenaruší tok textu. A zapíší se způsobem, který snadno pochopí i netechnicky založení uživatelé. + + +Odstavce textu +============== + +Za odstavec se považuje jeden nebo více bezprostředně za sebou následujících řádků textu. Odstavečky jsou od sebe oddělené prázdným řádkem. + +/--code texy +První odstavec lorem ipsum dolor sit amet. + +Druhý odstavec, který tvoří jeden řádek. +A druhý řádek textu. Texy je spojí. +\-- + +/--texysource +První odstavec lorem ipsum dolor sit amet. + +Druhý odstavec, který tvoří jeden řádek. +A druhý řádek textu. Texy je spojí. +\-- + +*V editačním políčku webové stránky (textarea) není rozdělení odstavce na dva řádky patrné. Proto je i Texy považuje za jeden odstavec.* + +Zalomení řádku v odstavci docílíte vložením jedné mezery vlevo: + +/--code texy +Kdoví jestli + jestli jsou na měsíci vůbec nějaký stopy + a proč kope kolem sebe kdo se topí + jakej sval to Zemí otáčí +\-- + +/--texysource +Kdoví jestli + jestli jsou na měsíci vůbec nějaký stopy + a proč kope kolem sebe kdo se topí + jakej sval to Zemí otáčí +\-- + + +Titulky +======= + +Titulky je možné zapsat hned dvěma způsoby: **podtržením** nebo **předsazením**. + +Každý titulek má svůj stupeň. V případě **podtržení** o důležitosti titulku rozhoduje podtrhávací znak. Od nejvyšší po nejnižší jsou to tyto: `#` `*` `=` `-` + +/--code texy +Hlavní titulek +************** + + +Podtitulek +========== +\-- + +/--texysource +Hlavní titulek +************** + + +Podtitulek +========== +\-- + +U titulků zapsaných **předsazením** určuje úroveň počet předsazených znaků. A ty mohou být `#` nebo `=` + +Platí: čím více znaků, tím důležitější titulek (minimum jsou dva znaky, maximum sedm). + +/--code texy +=== Hlavní titulek === + +## Podtitulek +\-- + +Jak vidíte v případě podtitulku, znaky vpravo je možné vynechat. + +*Stupně titulků jsou vždy jen relativní! Tedy Texy najde nejvyšší použitý titulek a ostatní titulky relativně od něj odstupňuje.* + + +Horizontální čáry +================= + +Texy zná tyto způsoby zápisu: + + +/--code texy +------------ + +******** +\-- + + +/--texysource +------------- + +******** +\-- + + +Kód +=== + +Používá se pro vložení zdrojového kódu. Použitím přídavného modulu lze aktivovat i zvýrazňování syntaxe. + +/--code texy + /---code php + function reImage($matches) { + $content = $matches[1]; + $align = $matches[5]; + $href = $matches[6]; + } + \--- +\-- + +/--texysource + /---code php + function reImage($matches) { + $content = $matches[1]; + $align = $matches[5]; + $href = $matches[6]; + } + \--- +\-- + +*Všimněte si slova `php` pro označení jazyka.* + + +Vypnutí Texy +============ + +Klíčové slovo `html` nebo `text` ovlivňuje, jestli obsah bude chápán jako HTML (včetně značek), nebo prostý text. + +/--code texy + /---html + <em>příklad</em>: **this is not strong** + \--- + + + /---text + <em>příklad</em>: **this is not strong** + \--- +\-- + +Pro inline vypnutí Texy je možné použít dvojitý apostrof `''` a obalit s ním část textu, který nemá být Texy zpracováván. + +/--code texy + Příklad: ''**this is not strong**'' +\-- + + +Rozdělování do bloků (div) +========================== + +Tuto schopnost využijete při tvorbě složitějších dokumentů. + +/--code texy + /---div .[header] + + content of div + + \--- +\-- + +/--texysource + /---div .[header] + + content of div + + \--- +\-- + + +Je možné bloky i vnořovat: + +/--code texy + /---div .[header] + + ## This is a header. + + /---div + vnořený div + \--- + + Texy je sexy! + + \--- +\-- + +/--texysource + /---div .[header] + + ## This is a header. + + /---div + vnořený div + \--- + + Texy je sexy! + + \--- +\-- + + +Citace +====== + +Citace jsou odsazené, podobně jako v emailech, znakem `>` + +/--code texy +> This is a blockquote with two paragraphs. +> +> 640 K should be enough for everyone +\-- + +/--texysource +> This is a blockquote with two paragraphs. +> +> 640 K should be enough for everyone +\-- + + +Odkazy +====== + +Odkazy se zapisují tak, že odkazující text uzavřete do uvozovek a následujete dvojtečkou a URL. Texy se snaží inteligentně odhadnout konec URL. Můžete mu i pomoci tím, že URI uzavřete do hranatých závorek. Část `http://` není povinná. + +Jako odkaz je možné vkládat i emaily, Texy je transformuje do podoby, která by měla zmást spamboty. + +/--code texy +Look at homepage:[https://texy.info]. + +Do you know "La Trine":https://www.latrine.cz? + +"Write me":me@example.com +\-- + +/--texysource +Look at homepage:[https://texy.info]. + +Do you know "La Trine":https://www.latrine.cz? + +"Write me":me@example.com +\-- + + +Reference +--------- + +Aby se tok textu "neznečišťoval" vkládáním URL, je možné všechny adresy uvést na jednom místě a pak se na ně jen odkazovat. Tomu se říká reference. Kromě adresy je možné doplnit i text odkazu a [modifikátor | #modifier]. + +/--code texy + [homepage]: https://texy.info/ Texy .(homepage) + [nette]: http://nette.org + +This is [homepage] + +Look at "this site":[nette] +\-- + + +Obrázky +======= + +Zapisují se mezi hranaté závorky s hvězdičkou: + +/--code texy +[* image.gif *] +\-- + +/--texysource line +[* image.gif *] +\-- + +V textových odstavcích je často třeba zvolit, má-li být obrázek zarovnán k levému nebo pravému kraji. Toho docílíte pomocí znaku `<` a `>` použitého před pravou závorkou: + +/--code texy +[* image.gif <] Left-aligned image + +[* image.gif >] Right-aligned image +\-- + +/--texysource +[* image.gif <] Left-aligned image + +[* image.gif >] Right-aligned image +\-- + +*Poznámka: V uvedeném příkladu Texy použil pro zarovnání přímý styl. Je možné systém nakonfigurovat tak, aby místo přiřadil obrázkům zvolenou třídu.* + +*Poznámka: pro všechny (relativní) URL obrázků je možné nastavit výchozí adresář. V uvedeným příkladech to byl `images/`, proto v Texy není adresář uveden, zatímco ve vygenerovaném HTML ano.* + +*Poznámka: pokud není implicitně určen alternativní text (jak na to viz níže), použije Texy výchozí. Zde je to prosté `image`* + + +Rozměry +------- + +U lokálních obrázků Texy zjistí rozměry automaticky. Pokud je chcete určit ručně, zapište je takto: + +/--code texy +[* image.gif 10x20 *] +\-- + +/--texysource line +[* image.gif 10x20 *] +\-- + + +Modifikátory +------------ + +O nich se více dozvíte v [jiné kapitole | #modifier], ale neuškodí si ukázat, jak se u obrázků zapisují. Zkusme si modifikátor pro určení alternativního textu a třídy: + +/--code texy +[* image.gif .(alt text)[foto] *] +\-- + +/--texysource line +[* image.gif .(alt text)[foto] *] +\-- + + +Reference +--------- + +Ze stejných důvodů, jako u odkazů, je i obrázky možné zapisovat pomocí referencí. Je třeba definovat URL (nebo více URL oddělených `|`) a případně i modifikátory. + +/--code texy +What a beautiful girl [* picture*] ! + +[* picture*]: image.gif .(my girl) +\-- + +/--texysource +What a beautiful girl [* picture*] ! + +[* picture*]: image.gif .(my girl) +\-- + + +Obrázek s popiskou +------------------ + +Za obrázkem uveďte tři hvězdičky a následuje popiska: + + +/--code texy +[* image.gif *] *** Toto je *popiska* pod obrázkem +\-- + +/--texysource +[* image.gif *] *** Toto je *popiska* pod obrázkem +\-- + + +Fráze +===== + +Asi nejpoužívanější syntax v Texy. Téměř ve všech případech se používá zdvojený znak. + +/--code texy +//kurzíva// + +*taky kurzíva* + +**tučné** + +superscript^2 vs. subscript_2 +\-- + +/--texysource +//kurzíva// + +*taky kurzíva* + +**tučné** + +superscript^2 vs. subscript_2 +\-- + +/--div .[output] +//kurzíva// + +*taky kurzíva* + +**tučné** + +***nejsilněji zdůrazněné*** + +superscript^2 vs. subscript_2 +\-- + +Speciálním případem fráze je tzv. kód. Od ostatních se liší tím, že jeho obsah nebude nadále formátován a zobrazí se doslovně: + +/--code texy +Odstraňte `<br />` a entitu `&ndash` +\-- + +/--texysource +Odstraňte `<br />` a entitu `&ndash` +\-- + +*Poznámka: jestli se použije element `<code>` nebo jiný (případně žádný) je možné rozhodnout pouhou konfigurací Texy* + + +S modifikátorem +--------------- + +Je možné jej vložit do každé fráze, vždy těsně před uzavírací znak: + +/--code texy +**silný a zelený .{color:green}** jako Hulk +\-- + +/--texysource +**silný a zelený .{color:green}** jako Hulk +\-- + +/--div .[output] +**silný a zelený .{color:green}** jako Hulk +\-- + + +Přímé HTML +========== + +Texy není náhrada za HTML. Nehledá ani alternativní způsoby zápisu HTML. Cílem je zjednodušit psaní obsahu. Pokud se Vám zdá jednodušší zapsat některou strukturu přímo v HTML, můžete tak učinit. HTML značky jsou plně podporované. + +/--code texy +This <strong class=info>is strong</strong> text. +<br> This is not. +\-- + +/--texysource +This <strong class=info>is strong</strong> text. +<br> This is not. +\-- + +*Poznámka: všimněte si, že Texy upraví zápis atributů a značek tak, aby byly validní (i pro XHTML výstup). Stejně tak dbá na **well-formed zápis**!* + +*Poznámka: Rozhodování, která značky a které atributy můžou být v textu použity, je plně uživatelsky ovladatelné. Demonstruje to jeden příklad z distribuce.* + + +Seznamy +======= + +Odrážkové seznamy zapisujeme pomocí `*` `+` nebo `-`. Musí být zapsán hned na začátku řádku a za ním musí následovat mezera. + +/--code texy +- Red +- Green +- Blue +\-- + +/--texysource +- Red +- Green +- Blue +\-- + +/--div .[output] +- Red +- Green +- Blue +\-- + + +Číslované seznamy +----------------- + +Texy zná těchto pět způsobů zápisu (první dva jsou ekvivalentní): + +/--code texy +1) Učit se +2) Učit se +3) Učit se + +a) Dlouhý +b) Široký +c) Krátkozraký + +A) DOS +B) Windows +C) Linux + +I) Yesterday +II) Today +III) Tomorrow +\-- + +/--texysource +1) Učit se +2) Učit se +3) Učit se + +a) Dlouhý +b) Široký +c) Krátkozraký + +A) DOS +B) Windows +C) Linux + +I) Yesterday +II) Today +III) Tomorrow +\-- + +/--div .[output] +1) Učit se +2) Učit se +3) Učit se + +a) Dlouhý +b) Široký +c) Krátkozraký + +A) DOS +B) Windows +C) Linux + +I) Yesterday +II) Today +III) Tomorrow +\-- + + +Vnořené seznamy +--------------- + +/--code texy +a) Bird + I) Bird + - Red + - Green + - Blue + II) McHale + III) Parish +b) McHale +c) Parish + 1) Bird + 2) McHale + 3) Parish +\-- + +/--texysource +a) Bird + I) Bird + - Red + - Green + - Blue + II) McHale + III) Parish +b) McHale +c) Parish + 1) Bird + 2) McHale + 3) Parish +\-- + + +Definiční seznam +---------------- + +/--code texy +Koncert Divokej Bill: + - termín: 9. 12. 2004 + - místo: Hala Vodová, Brno + - Cena: 260 Kč +\-- + +/--texysource +Koncert Divokej Bill: + - termín: 9. 12. 2004 + - místo: Hala Vodová, Brno + - Cena: 260 Kč +\-- + +/--div .[output] +Koncert Divokej Bill: + - termín: 9. 12. 2004 + - místo: Hala Vodová, Brno + - Cena: 260 Kč +\-- + + +S modifikátorem +--------------- + +Modifikátor, který ovlivňuje celý seznam, se uvádí na řádku před ním. Ostatní (klasicky) na konci řádku: + +/--code texy +.{color:red} +triangl: .{color:blue} + - trojúhelník .{color:green} + - neladěný bicí hudební nástroj + - tringulační věž +\-- + +/--div .[output] +.{color:red} +triangl: .{color:blue} + - trojúhelník .{color:green} + - neladěný bicí hudební nástroj + - tringulační věž +\-- + + +Modifikátory +============ + +Nejsilnější zbraň Texy Lze použít tyto druhy modifikátorů: + +- (titulek) popisné, přidají objektu titulek (nebo alternativní text obrázkům) +- `[class1 class2 #id]` určující třídu a / nebo ID prvku +- {class:blue} přímý zápis stylu +- {target:_blank} nebo přímý zápis HTML atributů +- horizontální zarovnání: + - doleva < + - doprava > + - vycentrovaný <> + - do bloku = + +- vertikální zarovnání: (jen u tabulek) + - nahoru ^ + - na střed - + - dolů _ + +Modifikátory se zapisují spojitě (bez mezer) a **musí jim předcházet tečka**. Takže třeba `.(popis)[left]` nastavuje atribut title na `popis` a třídu na `left`. + +**Modifikátory je vždy zapisují zcela doprava**. + +Příklad použití modifikátoru na odstavci textu: + +/--code texy +Vycentrováno modifikátorem .<> + +Obarveno modifikátorem .{color:blue; lang: cs} +\-- + +/--texysource +Vycentrováno modifikátorem .<> + +Obarveno modifikátorem .{color:blue; lang: cs} +\-- + + +Typografie +========== + +Sem patří všechny úpravy a náhrady textu, které upravují jeho vzhled v souladu s typografickými pravidly a podobně: + +/--code texy +- "české" 'typografické' uvozovky +- pomlčka vs. spojovník: 10-15 vs. česko-slovenský +- pomlčka: jedna -- dvě +- typografický křížek u rozměrů 10 x 20 +- šipky <- a -> a <-> ; +- tři tečky... +- zachování HTML entit & +- náhrady(TM) nebo(R) za příslušné entity(C) +\-- + +/--div .[output] +- "české" 'typografické' uvozovky +- pomlčka vs. spojovník: 10-15 vs. česko-slovenský +- pomlčka: jedna -- dvě +- typografický křížek u rozměrů 10 x 20 +- šipky <- a -> a <-> ; +- tři tečky... +- zachování HTML entit & +- náhrady(TM) nebo(R) za příslušné (C)entity +\-- + +práce s mezerami: + +/--code texy +- vkládání nezalomitelných mezer za jednopísmenné předložky (v autě u okna) +- nedělitelné mezery u telefonních čísel +420 776 552 046 +\-- + +/--code html +vkládání nezalomitelných mezer za jednopísmenné předložky (v autě u okna) + +nedělitelné mezery u telefonních čísel +420 776 552 046 +\-- + +*Poznámka: Nahrazování se obvykle řídí dalšími pravidly, které určují, kdy +symbol nahradit a kdy ne. Například šipka `->` nemůže být na konci řádku atd. Proto nebuďte překvapeni, když v některých případech Texy náhradu neprovede. Pokud to považujete za chybu, dejte mi vědět.* + + +Zkratky, akronymy +----------------- + +Používá se zápisu s dvojitou kulatou závorkou: + +/--code texy +jednoslovné: NATO((North Atlantic Treaty Organisation)) + +víceslovné: "et al."((a další)) +\-- + +/--texysource +jednoslovné: NATO((North Atlantic Treaty Organisation)) + +víceslovné: "et al."((a další)) +\-- + + +Klikatelné URI +-------------- + +Automatický převod URI do klikatelné formy (včetně emailů) + +/--code texy +další informace na www.texy.info a také ... +\-- + +/--div .[output] +další informace na www.texy.info a také ... +\-- + + +Rozdělení velmi dlouhých slov +============================= + +Velmi zajímavá a důležitá funkce Texy. Dlouhá slova mohou narušit vzhled stránky, proto je vhodné prohlížeči naznačit, kde je může zalomit. Texy tyto místa hledá s přihlédnutím k národním zvyklostem, tedy slovo rozděluje podle slabik: + +/--code texy +nejneobhospodařovávatelnějšími +\-- + +/--code html +nejneobhospoda­řovávatelnější­mi</p +\-- + +*Poznámka: limit délky slova je volitelný* + +*Poznámka: současné prohlížeče na jádru Gecko (Mozilla, Firefox) jsou k naznačenému dělení slepí. Doufám, že vývojáři tento nedostatek brzy odstraní. Nebo zkuste [tohle | https://forum.texy.info/cs/viewtopic.php?id=36]* + + +Tabulky +======= + +Příklad jednoduché tabulky, sloupce se oddělují znakem `|` + +/--code texy +| first col | second col | third col +| Adam | Eva | Franta +\-- + +A výsledek je: + +| first col | second col | third col +| Adam | Eva | Franta + + +Hlavičku tabulky můžeme definovat tímto zápisem: + +/--code texy +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Jesus | Christ | 33 +| Cecilie | Svobodova | 74 +\-- + +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Jesus | Christ | 33 +| Cecilie | Svobodova | 74 + +Pokud hlavičku netvoří řádek (řádky), můžeme ji definovat na úrovni buněk. Stačí vložit hvězdičku ihned po znaku `|` + + +/--code texy +|* First Name | Jesus | Cecilie +|* Last Name | Christ | Svobodova +|* Age | 33 | 74 +\-- + + +|* First Name | Jesus | Cecilie +|* Last Name | Christ | Svobodova +|* Age | 33 | 74 + + +Sloučení sloupců +---------------- + +všimněte si zdvojeného || + +/--code texy +|----------------------------- +| Name || Age +|---------------------------- +| Jesus | Christ | 33 +\-- + +|----------------------------- +| Name || Age +|---------------------------- +| Jesus | Christ | 33 + + +Sloučení řádků +-------------- + +Všimněte si znaku `^` symbolizujícího směr nahoru: + + +/--code texy +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Bill || 50 +| ^| 52 +| Jim | Beam | 70 +\-- + +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Bill || 50 +| ^| 52 +| Jim | Beam | 70 + + +Modifikátory +------------ + +Platí tato pravidla: +- modifikátor ovlivňující celou tabulku se vkládá bezprostředně před tabulku +- ovlivňující řádek se vkládá na konec řádku +- ovlivňující sloupec se vkládá na začátek buňky (vlevo v buňce) +- a nakonec ovlivňující buňku se vkládá na konec buňky (pravo v buňce) + +Podívejte se na příklad. + +/--code texy +.(people) +| .{color: green} first col | second col .>| third col | .{font-style:italic} +| Adam | Eva .{color: blue}| Franta | +\-- + +Zde je: +- `.(people)` modifikátor tabulky +- `.{color: green}` modifikátor sloupce +- `.{font-style:italic}` modifikátor řádku +- `.{color: blue}` a také `.>` modifikátor buňky + +Takže výsledná tabulka vypadá takto: + + +.(people) +| .{color: green} first col | second col .>| third col | .{font-style:italic} +| Adam | Eva .{color: blue}| Franta | diff --git a/texy/cs/syntax.texy b/texy/cs/syntax.texy new file mode 100644 index 0000000000..9a430f8080 --- /dev/null +++ b/texy/cs/syntax.texy @@ -0,0 +1,464 @@ +Syntaxe +******* + +--> "Podrobný popis syntaxe":syntax-podrobne + + +Nástroj Texy vznikl proto, aby nezkušeným uživatelům umožnil snadno editovat obsah webových stránek. Proto je i syntaxe maximálně intuitivní. Záměrem je, aby text v čisté (nezformátované) formě byl přehledný a jeho formát tušitelný. + +Dnes Texy výborně slouží i zkušeným znalcům jazyka HTML. Dovoluje volně kombinovat Texy zápis s HTML značkami. Zkušení uživatelé se tedy nemusí učit nový meta-jazyk a plně využít svých znalostí. Texy jim pouze zjednodušuje práci. + +Prvotní logikou syntaxe je **žádnou syntaxi nepoužívat**. Jen psát čistý text. Vkládání rozšířených informací, jako třeba CSS třídy nebo odkazy, nenaruší tok textu. A zapíší se způsobem, který snadno pochopí i netechnicky založení uživatelé. + + +Odstavce textu .[#paragraph] +============================ + +Za odstavec se považuje jeden nebo více bezprostředně za sebou následujících řádků textu. Odstavečky jsou od sebe oddělené prázdným řádkem. + +/--code texy +První odstavec lorem ipsum dolor sit amet. + +Druhý odstavec, který tvoří jeden řádek. +A druhý řádek textu. Texy je spojí. +\-- + +Zalomení řádku v odstavci docílíte vložením jedné mezery vlevo: + +/--code texy +Kdoví jestli + jestli jsou na měsíci vůbec nějaký stopy + a proč kope kolem sebe kdo se topí + jakej sval to Zemí otáčí +\-- + + +Titulky .[#heading] +=================== + +Titulky je možné zapsat hned dvěma způsoby: **podtržením** nebo **předsazením**. + +Každý titulek má svůj stupeň. V případě **podtržení** o důležitosti titulku rozhoduje podtrhávací znak. Od nejvyšší po nejnižší jsou to tyto: `#` `*` `=` `-` + +/--code texy +Hlavní titulek +************** + + +Podtitulek +========== +\-- + +U titulků zapsaných **předsazením** určuje úroveň počet předsazených znaků. A ty mohou být `#` nebo `=` + +Platí: čím více znaků, tím důležitější titulek (minimum jsou dva znaky, maximum sedm). + +/--code texy +=== Hlavní titulek === + +## Podtitulek +\-- + +Jak vidíte v případě podtitulku, znaky vpravo je možné vynechat. + + +Horizontální čáry .[#horizline] +=============================== + +Texy zná tyto způsoby zápisu: + + +/--code texy +------------ + +******** +\-- + + +Vypnutí Texy .[#disable-texy] +============================= + +Klíčové slovo `html` nebo `text` ovlivňuje, jestli obsah bude chápán jako HTML (včetně značek), nebo prostý text. + +/--code texy + /---html + <em>příklad</em>: **this is not strong** + \--- + + + /---text + <em>příklad</em>: **this is not strong** + \--- +\-- + +Pro inline vypnutí Texy je možné použít dvojitý apostrof `''` a obalit s ním část textu, který nemá být Texy zpracováván. + +/--code texy + Příklad: ''**this is not strong**'' +\-- + + +Citace .[#blockquote] +===================== + +Citace jsou odsazené, podobně jako v emailech, znakem `>` + +/--code texy +> This is a blockquote with two paragraphs. +> +> 640 K should be enough for everyone +\-- + + +Odkazy .[#link] +=============== + +Odkazy se zapisují tak, že odkazující text uzavřete do uvozovek a následujete dvojtečkou a URL. Texy se snaží inteligentně odhadnout konec URL. Můžete mu i pomoci tím, že URI uzavřete do hranatých závorek. Část `http://` není povinná. + +Jako odkaz je možné vkládat i emaily, Texy je transformuje do podoby, která by měla zmást spamboty. + +/--code texy +Look at homepage:[https://texy.info]. + +Do you know "La Trine":https://www.latrine.cz? + +"Write me":me@example.com + +\-- + + +Obrázky .[#image] +================= + +Zapisují se mezi hranaté závorky s hvězdičkou: + +/--code texy +[* image.gif .(alternativní text) *] +\-- + + +V textových odstavcích je často třeba zvolit, má-li být obrázek zarovnán k levému nebo pravému kraji. Toho docílíte pomocí znaku `<` a `>` použitého před pravou závorkou: + +/--code texy +[* image.gif <] Left-aligned image. Lorem ipsum ... + +[* image.gif >] Right-aligned image. Curabitur quam ... +\-- + + +Obrázek s popiskou +------------------ + +Za obrázkem uveďte tři hvězdičky a následuje popiska: + + +/--code texy +[* image.gif *] *** Toto je *popiska* pod obrázkem +\-- + + +Fráze .[#phrase] +================ + +Asi nejpoužívanější syntax v Texy. Téměř ve všech případech se používá zdvojený znak. + +/--code texy +//kurzíva// + +**tučné** + +x^2 + y^3 +\-- + + +/--div .[output] +//kurzíva// + +**tučné** + +x^2 + y^3 +\-- + + +Texy lze i dočasně vypnout - obsah nebude formátován a zobrazí se doslovně: + +/--code texy +Odstraňte ''<br />'' a entitu ''&ndash'' +\-- + + +Přímé HTML .[#html] +=================== + +Texy není náhrada za HTML. Nehledá ani alternativní způsoby zápisu HTML. Cílem je zjednodušit psaní obsahu. Pokud se Vám zdá jednodušší zapsat některou strukturu přímo v HTML, můžete tak učinit. HTML značky jsou plně podporované. + +/--code texy +This <strong class=info>is strong</strong> text. +<br> This is not. +\-- + + +Seznamy .[#list] +================ + +Odrážkové seznamy zapisujeme pomocí `*` `+` nebo `-`. Musí být zapsán hned na začátku řádku a za ním musí následovat mezera. + +/--code texy +- Red +- Green +- Blue +\-- + + +Číslované seznamy +----------------- + +Texy zná těchto pět způsobů zápisu (první dva jsou ekvivalentní): + +/--code texy +1) Učit se +2) Učit se +3) Učit se + +a) Dlouhý +b) Široký +c) Krátkozraký + +A) DOS +B) Windows +C) Linux + +I) Yesterday +II) Today +III) Tomorrow +\-- + + +Vnořené seznamy +--------------- + +/--code texy +a) Bird + I) Bird + - Red + - Green + - Blue + II) McHale + III) Parish +b) McHale +c) Parish + 1) Bird + 2) McHale + 3) Parish +\-- + + +Definiční seznam +---------------- + +Koncert "Divokej Bill":www.divokybill.cz: + - termín: 9. 12. 2004 + - místo: Hala Vodová, Brno + - Cena: 260 Kč + +/--code texy +Koncert Divokej Bill: + - termín: 9. 12. 2004 + - místo: Hala Vodová, Brno + - Cena: 260 Kč +\-- + + +Modifikátory .[#modifier] +========================= + +Nejsilnější zbraň Texy. Lze použít tyto druhy modifikátorů: + +- (titulek) popisné, přidají objektu titulek (nebo alternativní text obrázkům) +- `[class1 class2 #id]` určující třídu a / nebo ID prvku +- {class:blue} přímý zápis stylu +- {target:_blank} nebo přímý zápis HTML atributů +- horizontální zarovnání: + - doleva < + - doprava > + - vycentrovaný <> + - do bloku = + +- vertikální zarovnání: (jen u tabulek) + - nahoru ^ + - na střed - + - dolů _ + +Modifikátory se zapisují spojitě (bez mezer) a **musí jim předcházet tečka**. Takže třeba `.(popis)[left]` nastavuje atribut title na `popis` a třídu na `left`. + +**Modifikátory je vždy zapisují zcela doprava**. + +Příklad použití modifikátoru na odstavci textu: + +/--code texy +Vycentrováno modifikátorem .<> + +Obarveno modifikátorem .{color:blue; lang: cs} +\-- + +/--code html +<p style="text-align:center">Vycentrováno modifikátorem</p> + +<p style="color:blue" lang="cs">Obarveno modifikátorem</p> +\-- + + +Typografie .[#typography] +========================= + +Sem patří všechny úpravy a náhrady textu, které upravují jeho vzhled v souladu s typografickými pravidly a podobně: + +/--code texy +- "české" 'typografické' uvozovky +- pomlčka vs. spojovník: 10-15 vs. česko-slovenský +- pomlčka: jedna -- dvě +- typografický křížek u rozměrů 10 x 20 +- šipky <- a -> a <-> ; +- tři tečky... +- zachování HTML entit & +- náhrady(TM) nebo(R) za příslušné entity(C) +\-- + +/--div .[output] +- "české" 'typografické' uvozovky +- pomlčka vs. spojovník: 10-15 vs. česko-slovenský +- pomlčka: jedna -- dvě +- typografický křížek u rozměrů 10 x 20 +- šipky <- a -> a <-> ; +- tři tečky... +- zachování HTML entit & +- náhrady(TM) nebo(R) za příslušné (C)entity +\-- + +práce s mezerami: + +/--code texy +- vkládání nezalomitelných mezer za jednopísmenné předložky (v autě u okna) +- nedělitelné mezery u telefonních čísel +420 776 552 046 +\-- + +/--code html +vkládání nezalomitelných mezer za jednopísmenné předložky (v autě u okna) + +nedělitelné mezery u telefonních čísel +420 776 552 046 +\-- + +*Poznámka: Nahrazování se obvykle řídí dalšími pravidly, které určují, kdy +symbol nahradit a kdy ne. Například šipka `->` nemůže být na konci řádku atd. Proto nebuďte překvapeni, když v některých případech Texy náhradu neprovede.* + + +Zkratky, akronymy .[#acronym] +----------------------------- + +Používá se zápisu s dvojitou kulatou závorkou: + +/--code texy +jednoslovné: NATO((North Atlantic Treaty Organisation)) + +víceslovné: "et al."((a další)) +\-- + + +Klikatelné webové adresy +------------------------ + +Automatický převod webových adres a emailů do klikatelné formy + +/--code texy +další informace na www.texy.info a také ... +\-- + +/--div .[output] +další informace na www.texy.info a také ... +\-- + + +Rozdělení velmi dlouhých slov .[#longwords] +=========================================== + +Velmi zajímavá a důležitá funkce Texy. Dlouhá slova mohou narušit vzhled stránky, proto je vhodné prohlížeči naznačit, kde je může zalomit. Texy tyto místa hledá s přihlédnutím k národním zvyklostem, tedy slovo rozděluje podle slabik: + +/--code texy +nejneobhospodařovávatelnějšími +\-- + +/--code html +nejneobhospoda­řovávatelnější­mi</p +\-- + +*Poznámka: limit délky slova je volitelný* + + +Tabulky .[#table] +================= + +Příklad jednoduché tabulky, sloupce se oddělují znakem `|` + +/--code texy +| first col | second col | third col +| Adam | Eva | Franta +\-- + +A výsledek je: + +| first col | second col | third col +| Adam | Eva | Franta + + +Hlavičku tabulky můžeme definovat tímto zápisem: + +/--code texy +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Jesus | Christ | 33 +| Cecilie | Svobodova | 74 +\-- + +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Jesus | Christ | 33 +| Cecilie | Svobodova | 74 + + +Sloučení sloupců +---------------- + +všimněte si zdvojeného || + +/--code texy +| Name || Age +|---------------------------- +| Jesus | Christ | 33 +\-- + +| Name || Age +|---------------------------- +| Jesus | Christ | 33 + + +Sloučení řádků +-------------- + +Všimněte si znaku `^` symbolizujícího směr nahoru: + + +/--code texy +| First Name | Last Name | Age +|---------------------------- +| Bill || 50 +| ^| 52 +| Jim | Beam | 70 +\-- + +| First Name | Last Name | Age +|---------------------------- +| Bill || 50 +| ^| 52 +| Jim | Beam | 70 diff --git a/texy/cs/texy-vs-wysiwyg.texy b/texy/cs/texy-vs-wysiwyg.texy new file mode 100644 index 0000000000..c79d99679e --- /dev/null +++ b/texy/cs/texy-vs-wysiwyg.texy @@ -0,0 +1,23 @@ +Texy versus WYSIWYG editory +*************************** + + +WYSIWYG((What You See is What You Get)) editor zobrazuje dokument během editace takové podobě, v jaké bude vytištěn, zobrazen na webu atd. Příkladem takového editoru je Word. + +Tento druh editorů zažil v oblasti správy internetového obsahu skutečný boom. Kdo by také odolal jejich vizuálně atraktivnímu prostředí a myšoidnímu ovládání. Díky programátorům "šikovných komponent .[about](htmlArea, FCKeditor nebo tinyRTE)" se jejich implementace stala hračkou a dnes je nabízí každý CMS((Content Management Systém = Systém na správu obsahu)). V praxi se však ukazuje, jak jsou jejich **přednosti jen zdánlivé**. + + +WYSIWYG se pro web nehodí +------------------------- + +Web má totiž docela **jiná specifika** než tištěný dokument. Zatímco u tiskovin je prioritou vizuálním uspořádání (ze kterého si lidský mozek odvodí strukturu, tj. co je nadpis, co je text a co je popiska obrázku), u webu je základem struktura. + +S vizuálním editorem se vlastně neustále svádí boj. Nejprve bojuje tvůrce administračního rozhraní, který se jej snaží přinutit **generovat použitelný kód**. Poté s ním bojuje samotný uživatel, který se snaží s využitím plné škály dostupných barev a fontů vytvoří hrozivou stránku. Následuje snaha ořezat možnosti editoru, kterou střídá další touha uživatele využít jeho plný potenciál. + +Současné WYSIWYG editory se pro web zkrátka nehodí. Prohlašuji to s vědomím autora několika administračních rozhraní, které jej využívají. Potřeboval jsem přijít s vhodnějším nástrojem a tak vzniklo Texy + + +WYSIWYM +------- + +WYSIWYM znamená What You See Is What You Mean. diff --git a/texy/cs/try-settings.texy b/texy/cs/try-settings.texy new file mode 100644 index 0000000000..66be081ed6 --- /dev/null +++ b/texy/cs/try-settings.texy @@ -0,0 +1,55 @@ +Konfigurace dema +**************** + +Toto nastavení se používá v [demu | https://fiddle.nette.org/texy/]: + +/--php +$texy = new Texy\Texy; + +$texy->imageModule->root = '/images/'; +$texy->imageModule->linkedRoot = '/images/'; +$texy->headingModule->generateID = true; + +// syntax highlighting +$texy->addHandler('block', 'blockHandler'); +\-- + +Handler pro zvýrazňování syntaxe používá knihovnu Prism.js: + +/--php +function blockHandler( + Texy\HandlerInvocation $invocation, + string $blocktype, + string $content, + string $lang, + Texy\Modifier $modifier +): Texy\HtmlElement +{ + if ($blocktype !== 'block/code') { + // nothing to do + return $invocation->proceed(); + } + + $texy = $invocation->getTexy(); + $elPre = Texy\HtmlElement::el('pre'); + if ($modifier) { + $modifier->decorate($texy, $elPre); + } + $elPre->attrs['class'] = 'language-' . $lang; + $content = $texy->protect(htmlspecialchars($content), $texy::CONTENT_BLOCK); + $elPre->create('code', $content); + return $elPre; +} +\-- + +Po dokončení konverze se navíc v textu zamění některé znaky za entity, aby byly lépe patrné: + +/--php +$html = $texy->process($text); + +$html = str_replace( + ["\xc2\xa0", "\xc2\xad", "\xe2\x80\x93", "\xe2\x80\x94"], + [' ', '­', '–', '—'], + $html +); +\-- diff --git a/texy/en/@home.texy b/texy/en/@home.texy new file mode 100644 index 0000000000..eebdc1e671 --- /dev/null +++ b/texy/en/@home.texy @@ -0,0 +1,31 @@ +What Is Texy +------------ + +Texy allows you to enter content using an **easy to read** [Texy syntax | syntax] which is filtered into *structurally valid* HTML. No knowledge of HTML is required. Texy is one of the most complex formatting tools. It allows adding of images, links, nested lists, tables and has full support for CSS. + +Texy supports hyphenation of long words (which reflects language rules), clickable emails and URL (emails are obfuscated against spambots), national typographic single and double quotation marks, ellipses, em dashes, dimension sign, nonbreakable spaces (e.g. in phone numbers), acronyms, arrows and many others. + +Texy is being developed by David Grudl since 2004. It is written in PHP. + +Texy is licenced by BSD and GNU General Public License. Plugins for several content-management systems are available. + + +Features +-------- + +- simple, intuitive and human friendy markup +- generate clean and valid HTML code +- may be used together with syntax highlighter +- designed with proper typography in mind +- supports hyphenation + + +Why Texy +-------- + +- bulletproof - ensures the well-formedness of the resulting code +- predectable behaviour +- flexible configuration + + +{{maintitle: Texy – human friendly markup for PHP}} diff --git a/texy/en/@menu.texy b/texy/en/@menu.texy new file mode 100644 index 0000000000..080eb567c1 --- /dev/null +++ b/texy/en/@menu.texy @@ -0,0 +1,6 @@ +- [home | @home] +- [syntax briefly | syntax] +- [syntax in detail | syntax-full] +- [fiddle | https://fiddle.nette.org/texy/] +- [API | https://api.nette.org/texy/] +- [GitHub | https://github.com/dg/texy] diff --git a/texy/en/@meta.texy b/texy/en/@meta.texy new file mode 100644 index 0000000000..4ec8857a1e --- /dev/null +++ b/texy/en/@meta.texy @@ -0,0 +1 @@ +{{sitename: Texy Documentation}} diff --git a/texy/en/@try.texy b/texy/en/@try.texy new file mode 100644 index 0000000000..77c2fe7dac --- /dev/null +++ b/texy/en/@try.texy @@ -0,0 +1,15 @@ +Welcome! +-------- + +You can use Texy if you like: +- **bold** font or *italic* +- and this is how to "link":https://texy.info +- see "syntax":[syntax] for more information + + +But you can also stay with HTML: +- like this <b>HTML</b> +- Or even <b class=xx>totally <i>stupid</b>, Texy will solve it + + +[syntax]: /en/syntax diff --git a/texy/en/syntax-full.texy b/texy/en/syntax-full.texy new file mode 100644 index 0000000000..2df749bac6 --- /dev/null +++ b/texy/en/syntax-full.texy @@ -0,0 +1,889 @@ +Detailed Description of Syntax +****************************** + + +- [#Philosophy] +- [#Paragraphs of Text] +- [#Headings] +- [#Horizontal lines] +- [#Code] +- [#Turning off the Texy] +- [#Quotes] +- [#Links] +- [#Images] +- [#Phrases] +- [#Direct HTML] +- [#Lists] +- [#Modifiers] +- [#Typography] +- [#Long Words Hyphenation] +- [#Tables] + + +Philosophy +========== + +The Texy tool was created to allow inexperienced users to easily edit the content of web pages. Therefore, the syntax is maximally intuitive. The intention is that the text in pure (unformatted) form is clear and its format can be guessed. + +Today, Texy also serves well-experienced HTML experts. Allows you to freely combine Texy notation with HTML tags. Thus, experienced users do not have to learn a new meta-language and make full use of their knowledge. Texy only simplifies their work. + +The primary logic of the syntax is ** not to use any syntax **. Just write plain text. Inserting advanced information, such as CSS classes or links, does not disrupt the flow of text. And it is written in a way that even non-technical users can easily understand. + + +Paragraphs of Text +================== + +A paragraph is considered to be one or more consecutive lines of text. The paragraphs are separated by a blank line. + +/--code texy +First paragraph lorem ipsum dolor sit amet. + +Second paragraph, který tvoří jeden řádek. +And second line of paragraph. Texy will join the lines. +\-- + +/--texysource +First paragraph lorem ipsum dolor sit amet. + +Second paragraph, který tvoří jeden řádek. +And second line of paragraph. Texy will join the lines. +\-- + +*In the edit box (textarea) is not a division into two lines of paragraph evident. That's why Texy considers them one paragraph.* + +To wrap a line in a paragraph, insert one space to the left: + +/--code texy +April is the cruellest month, breeding + Lilacs out of the dead land, mixing + Memory and desire, stirring + Dull roots with spring rain. +\-- + +/--texysource +April is the cruellest month, breeding + Lilacs out of the dead land, mixing + Memory and desire, stirring + Dull roots with spring rain. +\-- + + +Headings +======== + +Headings can be written in two ways: **underlined** or **surrounded**. + +Each headline has its own degree. In the case of **underlined**, the importance of the title is decided by the underline character. From the highest to the lowest, these are: `#` `*` `=` `-` + +/--code texy +Head Title +********** + + +Subtitle +======== +\-- + +/--texysource +Head Title +********** + + +Subtitle +======== +\-- + +For **surrounded** titles, the level determines the number of preceding characters. It can be `#` nebo `=` + +The following applies: the more characters, the more important the title (minimum two characters, maximum seven). + +/--code texy +=== Head title === + +## Subtitle +\-- + +As you can see in the case of the subtitle, the characters on the right can be omitted. + +*Subtitle levels are always relative only! So Texy finds the highest caption used and relatively differentiates the other captions.* + + +Horizontal Lines +================ + +Texy knows these notations: + +/--code texy +------------ + +******** +\-- + +/--texysource +------------- + +******** +\-- + + +Code +==== + +Used to insert source code. Syntax highlighting can also be activated using an add-on module. + +/--code texy + /---code php + function reImage($matches) { + $content = $matches[1]; + $align = $matches[5]; + $href = $matches[6]; + } + \--- +\-- + +/--texysource + /---code php + function reImage($matches) { + $content = $matches[1]; + $align = $matches[5]; + $href = $matches[6]; + } + \--- +\-- + +*Note the words `php` to indicate the language.* + + +Turning Off the Texy +==================== + +The keyword `html` or` text` affects whether the content will be understood as HTML (including tags) or plain text. + +/--code texy + /---html + <em>example</em>: **this is not strong** + \--- + + + /---text + <em>example</em>: **this is not strong** + \--- +\-- + +To turn off Texy inline, it is possible to use a double apostrophe `''` and wrap a part of the text that is not to be Texy processed. + +/--code texy + Example: ''**this is not strong**'' +\-- + + +Division into Blocks (Div) +========================== + +Use this ability to create more complex documents. + +/--code texy + /---div .[header] + + content of div + + \--- +\-- + +/--texysource + /---div .[header] + + content of div + + \--- +\-- + + +It is also possible to nest blocks: + +/--code texy + /---div .[header] + + ## This is a header. + + /---div + inner div + \--- + + Texy is sexy! + + \--- +\-- + +/--texysource + /---div .[header] + + ## This is a header. + + /---div + inner div + \--- + + Texy is sexy! + + \--- +\-- + + +Quotes +====== + +Quotes are indented, similarly to emails, by a character `>` + +/--code texy +> This is a blockquote with two paragraphs. +> +> 640 K should be enough for everyone +\-- + +/--texysource +> This is a blockquote with two paragraphs. +> +> 640 K should be enough for everyone +\-- + + +Links +===== + +Links are written by enclosing the referencing text in quotation marks, followed by a colon and a URL. Texy tries to intelligently guess the end of the URL. You can also help it by enclosing the URL in square brackets. The `http://` section is optional. + +It is also possible to insert emails as a link, Texy transforms them into a form that should confuse spambots. + +/--code texy +Look at homepage:[https://texy.info]. + +Do you know "La Trine":https://www.latrine.cz? + +"Write me":me@example.com +\-- + +/--texysource +Look at homepage:[https://texy.info]. + +Do you know "La Trine":https://www.latrine.cz? + +"Write me":me@example.com +\-- + + +References +---------- + +In order not to "pollute" the text flow by inserting URLs, it is possible to list all addresses in one place and then just link to them. This is called a reference. In addition to the address, it is possible to add the text of the link and the [modifier | #modifier]. + +/--code texy + [homepage]: https://texy.info/ Texy .(homepage) + [nette]: http://nette.org + +This is [homepage] + +Look at "this site":[nette] +\-- + + +Images +====== + +They are written between square brackets with an asterisk: + +/--code texy +[* image.gif *] +\-- + +/--texysource line +[* image.gif *] +\-- + +In text paragraphs, you often need to choose whether the image should be left-aligned or right-aligned. To do this, use the `<` and `>` character used before the right parenthesis: + +/--code texy +[* image.gif <] Left-aligned image + +[* image.gif >] Right-aligned image +\-- + +/--texysource +[* image.gif <] Left-aligned image + +[* image.gif >] Right-aligned image +\-- + +*Note: In the example above, Texy used a straight CSS style for alignment. It is possible to configure the system to assign a selected class to images instead.* + +*Note: it is possible to set a default directory for all (relative) image URLs. In the examples given, it was `images/`, so in Texy code the directory is not listed, while in the generated HTML it is.* + +*Note: if no alternative text is specified by default (as shown below), Texy will use the default. Here's a simple `image`* + + +Dimensions +---------- + +For local images, Texy detects the dimensions automatically. If you want to specify them manually, enter them as follows: + +/--code texy +[* image.gif 10x20 *] +\-- + +/--texysource line +[* image.gif 10x20 *] +\-- + + +Modifiers +--------- + +You can learn more about them in [another chapter | #modifier], but it doesn't hurt to show how they're written on images. Let's try a modifier to specify alternate text and class: + +/--code texy +[* image.gif .(alt text)[photo] *] +\-- + +/--texysource line +[* image.gif .(alt text)[photo] *] +\-- + + +Reference +--------- + +For the same reasons as for links, images can also be written using references. You need to define URLs (or multiple URLs separated by `|`) and possibly modifiers. + +/--code texy +What a beautiful girl [* picture*] ! + +[* picture*]: image.gif .(my girl) +\-- + +/--texysource +What a beautiful girl [* picture*] ! + +[* picture*]: image.gif .(my girl) +\-- + + +Figure with Caption +------------------- + +Enter three asterisks after the image, followed by a caption: + + +/--code texy +[* image.gif *] *** This is the *caption* below the image +\-- + +/--texysource +[* image.gif *] *** This is the *caption* below the image +\-- + + +Phrases +======= + +Probably the most used syntax in Texy. In almost all cases, a double character is used. + +/--code texy +//italics// + +*too italics* + +**bold** + +***heavy bold*** + +superscript^2 vs. subscript_2 +\-- + +/--texysource +//italics// + +*too italics* + +**bold** + +***heavy bold*** + +superscript^2 vs. subscript_2 +\-- + +/--div .[output] +//italics// + +*too italics* + +**bold** + +***heavy bold*** + +superscript^2 vs. subscript_2 +\-- + +A special case of a phrase is the so-called code. It differs from the other phrases in that its content will no longer be formatted and will be displayed literally: + +/--code texy +This is `<br />` and entity `&ndash` +\-- + +/--texysource +This is `<br />` and entity `&ndash` +\-- + +*Note: whether the `<code>` element is used or another (or none) can be changed simply by configuring Texy.* + + +With Modifier +------------- + +It is possible to insert it into each phrase, always just before the closing character: + +/--code texy +**strong and green .{color:green}** like Hulk +\-- + +/--texysource +**strong and green .{color:green}** like Hulk +\-- + +/--div .[output] +**strong and green .{color:green}** like Hulk +\-- + + +Direct HTML +=========== + +Texy is not a substitute for HTML. It also doesn't look for alternative ways to write HTML. The goal is to simplify content writing. If you find it easier to write a structure directly in HTML, you can do so. HTML tags are fully supported. + +/--code texy +This <strong class=info>is strong</strong> text. +<br> This is not. +\-- + +/--texysource +This <strong class=info>is strong</strong> text. +<br> This is not. +\-- + +*Note: note that Texy adjusts the notation of attributes and tags to be valid (even for XHTML output). It also pays attention to the **well-formed notation**!* + +*Note: Deciding which tags and which attributes can be used in the text is fully user configurable. This demonstrates one example from the distribution.* + + +Lists +===== + +Bulleted lists are written using `*`, `+` or `-`. It must be written at the very beginning of the line and followed by a space. + +/--code texy +- Red +- Green +- Blue +\-- + +/--texysource +- Red +- Green +- Blue +\-- + +/--div .[output] +- Red +- Green +- Blue +\-- + + +Numbered Lists +-------------- + +Texy knows these five ways of writing (the first two are equivalent): + +/--code texy +1) Learn +2) Learn +3) Learn + +a) Long +b) Wide +c) Shortsighted + +A) DOS +B) Windows +C) Linux + +I) Yesterday +II) Today +III) Tomorrow +\-- + +/--texysource +1) Learn +2) Learn +3) Learn + +a) Long +b) Wide +c) Shortsighted + +A) DOS +B) Windows +C) Linux + +I) Yesterday +II) Today +III) Tomorrow +\-- + +/--div .[output] +1) Learn +2) Learn +3) Learn + +a) Long +b) Wide +c) Shortsighted + +A) DOS +B) Windows +C) Linux + +I) Yesterday +II) Today +III) Tomorrow +\-- + + +Nested Lists +------------ + +/--code texy +a) Bird + I) Bird + - Red + - Green + - Blue + II) McHale + III) Parish +b) McHale +c) Parish + 1) Bird + 2) McHale + 3) Parish +\-- + +/--texysource +a) Bird + I) Bird + - Red + - Green + - Blue + II) McHale + III) Parish +b) McHale +c) Parish + 1) Bird + 2) McHale + 3) Parish +\-- + + +Definition List +--------------- + +/--code texy +Wild Bill concert: + - date: 9 December 2004 + - place: Vodová Hall, Brno + - price: 260 CZK +\-- + +/--texysource +Wild Bill concert: + - date: 9 December 2004 + - place: Vodová Hall, Brno + - price: 260 CZK +\-- + +/--div .[output] +Wild Bill concert: + - date: 9 December 2004 + - place: Vodová Hall, Brno + - price: 260 CZK +\-- + + +With Modifiers +-------------- + +The modifier that affects the entire list is listed in the line before it. Others (classic) at the end of the line: + +/--code texy +.{color:red} +triangl: .{color:blue} + - triangle .{color:green} + - untuned percussion musical instrument + - tringulation tower +\-- + +/--div .[output] +.{color:red} +triangl: .{color:blue} + - triangle .{color:green} + - untuned percussion musical instrument + - tringulation tower +\-- + + +Modifiers .[#modifier] +====================== + +Texy's most powerful weapon. The following types of modifiers can be used: + +- (caption) adds a title to the object (or alternative text to images) +- `[class1 class2 #id]` specifying the class and / or ID of the element +- {class: blue} direct style notation +- {target: _blank} or direct entry of HTML attributes +- horizontal alignment: + - left < + - right > + - centered <> + - to block = + +- vertical alignment: (only for tables) + - up ^ + - center - + - down _ + +Modifiers are written continuously (without spaces) and **must be preceded by a dot**. So for example `.(description)[left]` sets the title attribute to `description` and the class to `left`. + +**Modifiers are always written rightmost**. + +Example of applying a modifier to a paragraph of text: + +/--code texy +Centered with a modifier .<> + +Colored by a modifier .{color:blue; lang: cs} +\-- + +/--texysource +Centered with a modifier .<> + +Colored by a modifier .{color:blue; lang: cs} +\-- + + +Typography .[#typography] +========================= + +This includes all modifications and replacements of the text that modify its appearance in accordance with typographic rules and the like: + +/--code texy +- "English" 'typographic' quotation marks +- dash vs. hyphen: 10-15 vs. česko-slovenský +- dash: one -- two +- typographic cross for dimensions 10 x 20 +- arrows <- and -> and <-> ; +- three dots... +- preservation of HTML entities & +- replacing (TM) or (R) with the relevant entities (C) +\-- + +/--div .[output] +- "English" 'typographic' quotation marks +- dash vs. hyphen: 10-15 vs. česko-slovenský +- dash: one -- two +- typographic cross for dimensions 10 x 20 +- arrows <- and -> and <-> ; +- three dots... +- preservation of HTML entities & +- replacing (TM) or (R) with the relevant entities (C) +\-- + +Spaces handling: + +/--code texy +- inserting unbreakable spaces for one-letter prepositions (a car) +- unbreakable spaces for telephone numbers +420 776 552 046 +\-- + +/--code html +inserting unbreakable spaces for one-letter prepositions (a car) + +unbreakable spaces for telephone numbers +420 776 552 046 +\-- + +*Note: Replacements are usually governed by other rules that determine when +symbol replace and when not. For example, the arrow `->` cannot be at the end of a line, etc. So don't be surprised if in some cases Texy doesn't make the substitution.* + + +Abbreviations, Acronyms +----------------------- + +Double parenthetical notation is used: + +/--code texy +one word: NATO((North Atlantic Treaty Organisation)) + +multiword: "et al."((and more)) +\-- + +/--texysource +one word: NATO((North Atlantic Treaty Organisation)) + +multiword: "et al."((and more)) +\-- + + +Clickable Web Addresses +----------------------- + +Automatic conversion of web addresses and emails into a clickable form + +/--code texy +more information at www.texy.info and also ... +\-- + +/--div .[output] +more information at www.texy.info and also ... +\-- + + +Long Words Hyphenation +====================== + +Very interesting and important function of Texy. Long words can disrupt the appearance of the page, so it's a good idea to tell your browser where to wrap them. Texy searches for these places taking into account national customs, so he divides the word according to syllables: + +/--code texy +nejneobhospodařovávatelnějšími +\-- + +/--code html +nejneobhospoda­řovávatelnější­mi</p +\-- + +*Note: the word length limit is optional* + + +Tables .[#table] +================ + +Example of a simple table, columns are separated by a character `|` + +/--code texy +| first col | second col | third col +| Adam | Eva | Franta +\-- + +And the result is: + +| first col | second col | third col +| Adam | Eva | Franta + + +The table header can be defined with this notation: + +/--code texy +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Jesus | Christ | 33 +| Cecilie | Svobodova | 74 +\-- + +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Jesus | Christ | 33 +| Cecilie | Svobodova | 74 + +If the header does not form a row (rows), we can define it at the cell level. Just insert an asterisk immediately after the `|` character + + +/--code texy +|* First Name | Jesus | Cecilie +|* Last Name | Christ | Svobodova +|* Age | 33 | 74 +\-- + + +|* First Name | Jesus | Cecilie +|* Last Name | Christ | Svobodova +|* Age | 33 | 74 + + +Merging Columns +--------------- + +Notice the double `||`: + +/--code texy +|----------------------------- +| Name || Age +|---------------------------- +| Jesus | Christ | 33 +\-- + +|----------------------------- +| Name || Age +|---------------------------- +| Jesus | Christ | 33 + + +Merging Rows +------------ + +Notice the `^` character symbolizing the upward direction: + + +/--code texy +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Bill || 50 +| ^| 52 +| Jim | Beam | 70 +\-- + +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Bill || 50 +| ^| 52 +| Jim | Beam | 70 + + +Modifiers +--------- + +The following rules apply: +- a modifier affecting the whole table is inserted immediately before the table +- the modifier affecting the line is inserted at the end of the line +- the modifier affecting the column is inserted at the beginning of the cell (left in the cell) +- and finally the modifier affecting the cell is inserted at the end of the cell (right in the cell) + +Take a look at an example. + +/--code texy +.(people) +| .{color: green} first col | second col .>| third col | .{font-style:italic} +| Adam | Eva .{color: blue}| Franta | +\-- + +There is: +- `.(people)` table modifier +- `.{color: green}` column modifier +- `.{font-style:italic}` line modifier +- `.{color: blue}` a také `.>` cell modifier + +So the resulting table looks like this: + + +.(people) +| .{color: green} first col | second col .>| third col | .{font-style:italic} +| Adam | Eva .{color: blue}| Franta | diff --git a/texy/en/syntax.texy b/texy/en/syntax.texy new file mode 100644 index 0000000000..2e07405d68 --- /dev/null +++ b/texy/en/syntax.texy @@ -0,0 +1,463 @@ +Syntax +****** + +--> "Detailed syntax description":syntax-full + + +The Texy tool was created to allow inexperienced users to easily edit the content of web pages. Therefore, the syntax is maximally intuitive. The intention is that the text in pure (unformatted) form is clear and its format can be guessed. + +Today, Texy also serves well-experienced HTML experts. Allows you to freely combine Texy notation with HTML tags. Thus, experienced users do not have to learn a new meta-language and make full use of their knowledge. Texy only simplifies their work. + +The primary logic of the syntax is **not to use any syntax**. Just write plain text. Inserting advanced information, such as CSS classes or links, does not disrupt the flow of text. And it is written in a way that even non-technical users can easily understand. + + +Paragraphs .[#paragraph] +======================== + +A paragraph is considered to be one or more consecutive lines of text. The paragraphs are separated by a blank line. + +/--code texy +First paragraph lorem ipsum dolor sit amet. + +Second paragraph, který tvoří jeden řádek. +And second line of paragraph. Texy will join the lines. +\-- + +To wrap a line in a paragraph, insert one space to the left: + +/--code texy +April is the cruellest month, breeding + Lilacs out of the dead land, mixing + Memory and desire, stirring + Dull roots with spring rain. +\-- + + +Headings .[#heading] +==================== + +Headings can be written in two ways: **underlined** or **surrounded**. + +Each headline has its own degree. In the case of **underlined**, the importance of the title is decided by the underline character. From the highest to the lowest, these are: `#` `*` `=` `-` + +/--code texy +Head title +********** + + +Subtitle +======== +\-- + +For **surrounded** titles, the level determines the number of preceding characters. It can be `#` nebo `=` + +The following applies: the more characters, the more important the title (minimum two characters, maximum seven). + +/--code texy +=== Head title === + +## Subtitle +\-- + +As you can see in the case of the subtitle, the characters on the right can be omitted. + + +Horizontal Lines .[#horizline] +============================== + +Texy knows these notations: + + +/--code texy +------------ + +******** +\-- + + +Turning Off the Texy .[#disable-Texy] +===================================== + +The keyword `html` or` text` affects whether the content will be understood as HTML (including tags) or plain text. + +/--code texy + /---html + <em>example</em>: **this is not strong** + \--- + + + /---text + <em>example</em>: **this is not strong** + \--- +\-- + +To turn off Texy inline, it is possible to use a double apostrophe `''` and wrap a part of the text that is not to be Texy processed. + +/--code texy + Example: ''**this is not strong**'' +\-- + + +Quotes .[#blockquote] +===================== + +Quotes are indented, similarly to emails, by a character `>` + +/--code texy +> This is a blockquote with two paragraphs. +> +> 640 K should be enough for everyone +\-- + + +Links .[#link] +============== + +Links are written by enclosing the referencing text in quotation marks, followed by a colon and a URL. Texy tries to intelligently guess the end of the URL. You can also help it by enclosing the URL in square brackets. The `http://` section is optional. + +It is also possible to insert emails as a link, Texy transforms them into a form that should confuse spambots. + +/--code texy +Look at homepage:[https://texy.info]. + +Do you know "La Trine":https://www.latrine.cz? + +"Write me":me@example.com +\-- + + +Images .[#image] +================ + +They are written between square brackets with an asterisk: + +/--code texy +[* image.gif .(alternative text) *] +\-- + + +In text paragraphs, you often need to choose whether the image should be left-aligned or right-aligned. To do this, use the `<` and `>` character used before the right parenthesis: + +/--code texy +[* image.gif <] Left-aligned image. Lorem ipsum ... + +[* image.gif >] Right-aligned image. Curabitur quam ... +\-- + + +Figure with Caption +------------------- + +Enter three asterisks after the image, followed by a caption: + + +/--code texy +[* image.gif *] *** This is the *caption* below the image +\-- + + +Phrases .[#phrase] +================== + +Probably the most used syntax in Texy. In almost all cases, a double character is used. + +/--code texy +//italics// + +**bold** + +x^2 + y^3 +\-- + + +/--div .[output] +//italics// + +**bold** + +x^2 + y^3 +\-- + + +Texts can also be temporarily turned off - the content will not be formatted and will be displayed literally: + +/--code texy +Remove ''<br />'' and entity ''&ndash'' +\-- + + +Direct HTML .[#html] +==================== + +Texy is not a substitute for HTML. It also doesn't look for alternative ways to write HTML. The goal is to simplify content writing. If you find it easier to write a structure directly in HTML, you can do so. HTML tags are fully supported. + +/--code texy +This <strong class=info>is strong</strong> text. +<br> This is not. +\-- + + +Lists .[#list] +============== + +Bulleted lists are written using `*`, `+` or `-`. It must be written at the very beginning of the line and followed by a space. + +/--code texy +- Red +- Green +- Blue +\-- + + +Numbered Lists +-------------- + +Texy knows these five ways of writing (the first two are equivalent): + +/--code texy +1) Learn +2) Learn +3) Learn + +a) Long +b) Wide +c) Shortsighted + +A) DOS +B) Windows +C) Linux + +I) Yesterday +II) Today +III) Tomorrow +\-- + + +Nested Lists +------------ + +/--code texy +a) Bird + I) Bird + - Red + - Green + - Blue + II) McHale + III) Parish +b) McHale +c) Parish + 1) Bird + 2) McHale + 3) Parish +\-- + + +Definition List +--------------- + +Wild Bill concert: + - date: 9 December 2004 + - place: Vodová Hall, Brno + - price: 260 CZK + +/--code texy +Wild Bill concert: + - date: 9 December 2004 + - place: Vodová Hall, Brno + - price: 260 CZK +\-- + + +Modifiers .[#modifier] +====================== + +Texy's most powerful weapon. The following types of modifiers can be used: + +- (caption) adds a title to the object (or alternative text to images) +- `[class1 class2 #id]` specifying the class and / or ID of the element +- {class: blue} direct style notation +- {target: _blank} or direct entry of HTML attributes +- horizontal alignment: + - left < + - right > + - centered <> + - to block = + +- vertical alignment: (only for tables) + - up ^ + - center - + - down _ + +Modifiers are written continuously (without spaces) and **must be preceded by a dot**. So for example `.(description)[left]` sets the title attribute to `description` and the class to `left`. + +**Modifiers are always written rightmost**. + +Example of applying a modifier to a paragraph of text: + +/--code texy +Centered with a modifier .<> + +Colored by a modifier .{color:blue; lang: cs} +\-- + +/--code html +<p style="text-align:center">Centered with a modifier</p> + +<p style="color:blue" lang="cs">Colored by a modifier</p> +\-- + + +Typography .[#typography] +========================= + +This includes all modifications and replacements of the text that modify its appearance in accordance with typographic rules and the like: + +/--code texy +- "English" 'typographic' quotation marks +- dash vs. hyphen: 10-15 vs. česko-slovenský +- dash: one -- two +- typographic cross for dimensions 10 x 20 +- arrows <- and -> and <-> ; +- three dots... +- preservation of HTML entities & +- replacing (TM) or (R) with the relevant entities (C) +\-- + +/--div .[output] +- "English" 'typographic' quotation marks +- dash vs. hyphen: 10-15 vs. česko-slovenský +- dash: one -- two +- typographic cross for dimensions 10 x 20 +- arrows <- and -> and <-> ; +- three dots... +- preservation of HTML entities & +- replacing (TM) or (R) with the relevant entities (C) +\-- + +Spaces handling: + +/--code texy +- inserting unbreakable spaces for one-letter prepositions (a car) +- unbreakable spaces for telephone numbers +420 776 552 046 +\-- + +/--code html +inserting unbreakable spaces for one-letter prepositions (a car) + +unbreakable spaces for telephone numbers +420 776 552 046 +\-- + +*Note: Replacements are usually governed by other rules that determine when +symbol replace and when not. For example, the arrow `->` cannot be at the end of a line, etc. So don't be surprised if in some cases Texy doesn't make the substitution.* + + +Abbreviations, Acronyms .[#acronym] +----------------------------------- + +Double parenthetical notation is used: + +/--code texy +one word: NATO((North Atlantic Treaty Organisation)) + +multiword: "et al."((and more)) +\-- + + +Clickable Web Addresses +----------------------- + +Automatic conversion of web addresses and emails into a clickable form + +/--code texy +more information at www.texy.info and also ... +\-- + +/--div .[output] +more information at www.texy.info and also ... +\-- + + +Long Words Hyphenation .[#longwords] +==================================== + +Very interesting and important function of Texy. Long words can disrupt the appearance of the page, so it's a good idea to tell your browser where to wrap them. Texy searches for these places taking into account national customs, so he divides the word according to syllables: + +/--code texy +nejneobhospodařovávatelnějšími +\-- + +/--code html +nejneobhospoda­řovávatelnější­mi</p +\-- + +*Note: the word length limit is optional* + + +Tables .[#table] +================ + +Example of a simple table, columns are separated by a character `|` + +/--code texy +| first col | second col | third col +| Adam | Eva | Franta +\-- + +And the result is: + +| first col | second col | third col +| Adam | Eva | Franta + + +The table header can be defined with this notation: + +/--code texy +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Jesus | Christ | 33 +| Cecilie | Svobodova | 74 +\-- + +|----------------------------- +| First Name | Last Name | Age +|---------------------------- +| Jesus | Christ | 33 +| Cecilie | Svobodova | 74 + + +Merging Columns +--------------- + +Notice the double `||`: + +/--code texy +| Name || Age +|---------------------------- +| Jesus | Christ | 33 +\-- + +| Name || Age +|---------------------------- +| Jesus | Christ | 33 + + +Merging Rows +------------ + +Notice the `^` character symbolizing the upward direction: + + +/--code texy +| First Name | Last Name | Age +|---------------------------- +| Bill || 50 +| ^| 52 +| Jim | Beam | 70 +\-- + +| First Name | Last Name | Age +|---------------------------- +| Bill || 50 +| ^| 52 +| Jim | Beam | 70 diff --git a/texy/en/try-settings.texy b/texy/en/try-settings.texy new file mode 100644 index 0000000000..0b8706a6a8 --- /dev/null +++ b/texy/en/try-settings.texy @@ -0,0 +1,55 @@ +Demo Configuration +****************** + +This setting is used in [demo | https://fiddle.nette.org/texy/]: + +/--php +$texy = new Texy\Texy; + +$texy->imageModule->root = '/images/'; +$texy->imageModule->linkedRoot = '/images/'; +$texy->headingModule->generateID = true; + +// syntax highlighting +$texy->addHandler('block', 'blockHandler'); +\-- + +The handler uses Prism.js to highlight syntax: + +/--php +function blockHandler( + Texy\HandlerInvocation $invocation, + string $blocktype, + string $content, + string $lang, + Texy\Modifier $modifier +): Texy\HtmlElement +{ + if ($blocktype !== 'block/code') { + // nothing to do + return $invocation->proceed(); + } + + $texy = $invocation->getTexy(); + $elPre = Texy\HtmlElement::el('pre'); + if ($modifier) { + $modifier->decorate($texy, $elPre); + } + $elPre->attrs['class'] = 'language-' . $lang; + $content = $texy->protect(htmlspecialchars($content), $texy::CONTENT_BLOCK); + $elPre->create('code', $content); + return $elPre; +} +\-- + +In addition, when the conversion is complete, some characters are replaced by entities in the text to make them more visible: + +/--php +$html = $texy->process($text); + +$html = str_replace( + ["\xc2\xa0", "\xc2\xad", "\xe2\x80\x93", "\xe2\x80\x94"], + [' ', '­', '–', '—'], + $html +); +\-- diff --git a/texy/files/image.gif b/texy/files/image.gif new file mode 100644 index 0000000000000000000000000000000000000000..b99eabeaf8b0c5aaaf75c040951fae1980ed96cd GIT binary patch literal 942 zcmV;f15x}(Nk%w1VK@LN0K@<QW{I`w=;p`I*_W)y^YZdfc&)g`+0xnJq_@xg{rwL| zk@NBG`T6-ERG8Z0>pW?r{rL0m?d#;}@Q9+q0z8ZC@b&-w`1$qm_4V~GU!M2(_;{AR z`}_Le-{1QB`v3p`A^sdZLr+jyK15-5X=ETra&=^EAa{3nE@WqTE@OHCA^8LV00000 zEC2ui05||B000I5;3tmNVVp-bu59bR@ak+u_7uQ#ORc9>6-I10#B4GE#32xfFasA) zMKA!=GK~gOK<utGg~DM$#&ABCih<TpG;osvVz7BA00eIrp*TP+fEo@60BsEq6d3{m zWQ`XQ2n`zy33@CN1qLDm6b}F!R&5Fo8wec*6ayU*7#a*79IzG!866&$D;pFDlo%6O zS~vp&s}~3q0388BE*cvL8kM*X$W4_N0Ug>+5fKZm4-JbL(#-_XxE}@L0pub+1o5pN zmB$m-LmcIL2dVh^8EP#{=OqjiW<w0@3Xq2&5d#Xw?R$|xhq7A^hmh)a=AplaA|x2V zU`dS%7PMCXX(%WV<A(`tHt2J~U}Y`}2KFT=Fkk_Ji7y-|xCe5=E>Axl7&!UC000RI zDpV30@#hEw54s^7G9lu?rW7_{6%gTrt^yVkOb|mLj1T}6#4V7v6M;{lE;wW;fDc1J z1P=^E$OAxuf+`y%STKrUR)$d>KprT-(aV9MtVA5!Yry9R2bl~{@L9n|lL8y=v?Jkw zfV(aO6bNPp_u1eN6$%LGDm#J&0f#SaC`I9(O%Vyuc`1CLY=WHvNn%1U0f12e7tHoC zKv@@p2)Cwxzzd^Bhu;7+6zTxM1A<Hg_(niA;mDl~1|Y0!Ug`3N_LdZKz%RT&gc=S9 z5YoZ_0va5U0Wt#Uke>vH9AH%f5;Wmb7X}0)fKME(F@XRCI52@XW_(7>BUD)7#sdh@ zrGavKIIsvAlzafe1@g@JS^^eaAV3ZP3=rFh0{lo&a;}w=04#LvGg$_<E#}oZ<YhEO z1xIYK;9wjS@W3h^X^9IhVk|J&N)<G4B1vZAGC%`8d7~U3WNj5E5ioVe%033owWk++ z_(|gjmk9>vphHl2B6t(wpdq7t3Up{w4K%kXr7(DM7Yn7?6+xDqe!xHmqa7y#7#pam zXQ?fa62%I+0ifoku(A*d7b6Hzz-WKw%GCoC7(fRA20ZeburOF~fRPB0$Rn=I_P_xP Q1Ut@Ctrt&1IcNv~JMQpsg8%>k literal 0 HcmV?d00001 diff --git a/texy/meta.json b/texy/meta.json new file mode 100644 index 0000000000..27991fd5a8 --- /dev/null +++ b/texy/meta.json @@ -0,0 +1,5 @@ +{ + "version": "3.x", + "repo": "dg/texy", + "composer": "texy/texy" +} From ddb26a98bcc2fc0846853788e30e090cf94ab2b4 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Mon, 27 Apr 2026 17:21:29 +0200 Subject: [PATCH 002/112] neon: link to fiddle --- neon/bg/@home.texy | 2 +- neon/bg/format.texy | 2 +- neon/cs/@home.texy | 2 +- neon/cs/format.texy | 2 +- neon/de/@home.texy | 2 +- neon/de/format.texy | 2 +- neon/el/@home.texy | 2 +- neon/el/format.texy | 2 +- neon/en/@home.texy | 2 +- neon/en/format.texy | 2 +- neon/es/@home.texy | 2 +- neon/es/format.texy | 2 +- neon/fr/@home.texy | 2 +- neon/fr/format.texy | 2 +- neon/hu/@home.texy | 2 +- neon/hu/format.texy | 2 +- neon/it/@home.texy | 2 +- neon/it/format.texy | 2 +- neon/ja/@home.texy | 2 +- neon/ja/format.texy | 2 +- neon/pl/@home.texy | 2 +- neon/pl/format.texy | 2 +- neon/pt/@home.texy | 2 +- neon/pt/format.texy | 2 +- neon/ro/@home.texy | 2 +- neon/ro/format.texy | 2 +- neon/ru/@home.texy | 2 +- neon/ru/format.texy | 2 +- neon/sl/@home.texy | 2 +- neon/sl/format.texy | 2 +- neon/tr/@home.texy | 2 +- neon/tr/format.texy | 2 +- neon/uk/@home.texy | 2 +- neon/uk/format.texy | 2 +- www/bg/packages.texy | 2 +- www/cs/packages.texy | 2 +- www/de/packages.texy | 2 +- www/el/packages.texy | 2 +- www/en/packages.texy | 2 +- www/es/packages.texy | 2 +- www/fr/packages.texy | 2 +- www/hu/packages.texy | 2 +- www/it/packages.texy | 2 +- www/ja/packages.texy | 2 +- www/pl/packages.texy | 2 +- www/pt/packages.texy | 2 +- www/ro/packages.texy | 2 +- www/ru/packages.texy | 2 +- www/sl/packages.texy | 2 +- www/tr/packages.texy | 2 +- www/uk/packages.texy | 2 +- 51 files changed, 51 insertions(+), 51 deletions(-) diff --git a/neon/bg/@home.texy b/neon/bg/@home.texy index c25a22d34b..cf81855aa0 100644 --- a/neon/bg/@home.texy +++ b/neon/bg/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON е разбираем за човека език за сериализация на данни. Използва се в Nette за конфигурационни файлове. [api:Nette\Neon\Neon] е статичен клас за работа с NEON. -Запознайте се с [формата NEON |format] и [изпробвайте го |https://ne-on.org]. +Запознайте се с [формата NEON |format] и [изпробвайте го |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/bg/format.texy b/neon/bg/format.texy index 5b31151ace..0b833e9108 100644 --- a/neon/bg/format.texy +++ b/neon/bg/format.texy @@ -2,7 +2,7 @@ *********** .[perex] -NEON е лесно четим структуриран формат за данни. В Nette се използва за конфигурационни файлове. Използва се и за структурирани данни, като настройки, езикови преводи и т.н. [Опитайте го|https://ne-on.org]. +NEON е лесно четим структуриран формат за данни. В Nette се използва за конфигурационни файлове. Използва се и за структурирани данни, като настройки, езикови преводи и т.н. [Опитайте го|https://fiddle.nette.org/neon/]. NEON е съкращение от *Nette Object Notation*. Той е по-малко сложен и тромав от XML или JSON, но предоставя подобни функции. Много е подобен на YAML. Основното предимство е, че NEON има така наречените [#ентитети], благодарение на които конфигурацията на DI сървисите е [също толкова секси |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. И позволява индентация с табулации. diff --git a/neon/cs/@home.texy b/neon/cs/@home.texy index 5375b2e479..75a6e36deb 100644 --- a/neon/cs/@home.texy +++ b/neon/cs/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON je lidsky srozumitelný jazyk pro serializaci dat. Používá se v Nette pro konfigurační soubory. [api:Nette\Neon\Neon] je statická třída pro práci s NEONem. -Seznamte se s [formátem NEON|format] a [vyzkoušejte si jej |https://ne-on.org]. +Seznamte se s [formátem NEON|format] a [vyzkoušejte si jej |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/cs/format.texy b/neon/cs/format.texy index 1f7f06bfcf..f8bf97f6d5 100644 --- a/neon/cs/format.texy +++ b/neon/cs/format.texy @@ -2,7 +2,7 @@ Formát NEON *********** .[perex] -NEON je v lidsky čitelný strukturovaný datový formát. V Nette se používá pro konfigurační soubory. Také se používá pro strukturovaná data, jako jsou nastavení, jazykové překlady atd. [Vyzkoušejte si jej|https://ne-on.org]. +NEON je v lidsky čitelný strukturovaný datový formát. V Nette se používá pro konfigurační soubory. Také se používá pro strukturovaná data, jako jsou nastavení, jazykové překlady atd. [Vyzkoušejte si jej|https://fiddle.nette.org/neon/]. NEON je zkratka pro *Nette Object Notation*. Je méně složitý a nemotorný než XML nebo JSON, ale poskytuje podobné funkce. Je velmi podobný YAML. Hlavní přednost je tom, že NEON má takzvané [#entity], díky kterým je konfigurace DI služeb [taky sexy |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. A umožňuje odsazovat tabulátory. diff --git a/neon/de/@home.texy b/neon/de/@home.texy index 158c387de2..02745de78d 100644 --- a/neon/de/@home.texy +++ b/neon/de/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON ist eine für Menschen verständliche Sprache zur Serialisierung von Daten. Sie wird in Nette für Konfigurationsdateien verwendet. [api:Nette\Neon\Neon] ist eine statische Klasse für die Arbeit mit NEON. -Machen Sie sich mit dem [NEON-Format|format] vertraut und [probieren Sie es aus |https://ne-on.org]. +Machen Sie sich mit dem [NEON-Format|format] vertraut und [probieren Sie es aus |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/de/format.texy b/neon/de/format.texy index bd78f27538..d95e6e9128 100644 --- a/neon/de/format.texy +++ b/neon/de/format.texy @@ -2,7 +2,7 @@ Das NEON-Format *************** .[perex] -NEON ist ein menschenlesbares strukturiertes Datenformat. In Nette wird es für Konfigurationsdateien verwendet. Es wird auch für strukturierte Daten wie Einstellungen, Sprachübersetzungen usw. verwendet. [Probieren Sie es aus|https://ne-on.org]. +NEON ist ein menschenlesbares strukturiertes Datenformat. In Nette wird es für Konfigurationsdateien verwendet. Es wird auch für strukturierte Daten wie Einstellungen, Sprachübersetzungen usw. verwendet. [Probieren Sie es aus|https://fiddle.nette.org/neon/]. NEON ist die Abkürzung für *Nette Object Notation*. Es ist weniger komplex und sperrig als XML oder JSON, bietet aber ähnliche Funktionen. Es ist YAML sehr ähnlich. Der Hauptvorteil ist, dass NEON sogenannte [#Entitäten] hat, dank derer die Konfiguration von DI-Diensten [auch sexy ist |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. Und es erlaubt die Einrückung mit Tabulatoren. diff --git a/neon/el/@home.texy b/neon/el/@home.texy index 18c47bc4a6..acf29cb3fd 100644 --- a/neon/el/@home.texy +++ b/neon/el/@home.texy @@ -5,7 +5,7 @@ Nette NEON Το NEON είναι μια γλώσσα σειριοποίησης δεδομένων κατανοητή από τον άνθρωπο. Χρησιμοποιείται στο Nette για αρχεία διαμόρφωσης. Η [api:Nette\Neon\Neon] είναι μια στατική κλάση για την εργασία με το NEON. -Εξοικειωθείτε με [τη μορφή NEON|format] και [δοκιμάστε την |https://ne-on.org]. +Εξοικειωθείτε με [τη μορφή NEON|format] και [δοκιμάστε την |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/el/format.texy b/neon/el/format.texy index 11d6436156..7aab34dd05 100644 --- a/neon/el/format.texy +++ b/neon/el/format.texy @@ -2,7 +2,7 @@ ********** .[perex] -Το NEON είναι μια ευανάγνωστη μορφή δομημένων δεδομένων. Στο Nette χρησιμοποιείται για αρχεία διαμόρφωσης. Χρησιμοποιείται επίσης για δομημένα δεδομένα, όπως ρυθμίσεις, γλωσσικές μεταφράσεις κ.λπ. [Δοκιμάστε το|https://ne-on.org]. +Το NEON είναι μια ευανάγνωστη μορφή δομημένων δεδομένων. Στο Nette χρησιμοποιείται για αρχεία διαμόρφωσης. Χρησιμοποιείται επίσης για δομημένα δεδομένα, όπως ρυθμίσεις, γλωσσικές μεταφράσεις κ.λπ. [Δοκιμάστε το|https://fiddle.nette.org/neon/]. Το NEON είναι ακρωνύμιο του *Nette Object Notation*. Είναι λιγότερο περίπλοκο και αδέξιο από το XML ή το JSON, αλλά παρέχει παρόμοιες λειτουργίες. Είναι πολύ παρόμοιο με το YAML. Το κύριο πλεονέκτημα είναι ότι το NEON διαθέτει τις λεγόμενες [οντότητες |#Entities], χάρη στις οποίες η διαμόρφωση των υπηρεσιών DI είναι [επίσης σέξι |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. Και επιτρέπει την εσοχή με tabs. diff --git a/neon/en/@home.texy b/neon/en/@home.texy index d162c54f3f..644b40e72f 100644 --- a/neon/en/@home.texy +++ b/neon/en/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON is a human-friendly data serialization language. It is used in Nette for configuration files. [api:Nette\Neon\Neon] is a static class for working with NEON. -Get to know the [NEON format|format] and [try it out |https://ne-on.org]. +Get to know the [NEON format|format] and [try it out |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/en/format.texy b/neon/en/format.texy index 5f6a51d2c8..6120dafc6a 100644 --- a/neon/en/format.texy +++ b/neon/en/format.texy @@ -2,7 +2,7 @@ NEON Format *********** .[perex] -NEON is a human-readable structured data format. In Nette, it is used for configuration files. It is also used for structured data such as settings, language translations, etc. [Try it on the sandbox |https://ne-on.org]. +NEON is a human-readable structured data format. In Nette, it is used for configuration files. It is also used for structured data such as settings, language translations, etc. [Try it on the sandbox |https://fiddle.nette.org/neon/]. NEON stands for *Nette Object Notation*. It is less complex and cumbersome than XML or JSON, but provides similar capabilities. It is very similar to YAML. The main advantage is that NEON has so-called [#entities], thanks to which the configuration of DI services is [so sexy |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. And it allows tabs for indentation. diff --git a/neon/es/@home.texy b/neon/es/@home.texy index ee418ef146..987fd26060 100644 --- a/neon/es/@home.texy +++ b/neon/es/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON es un lenguaje legible por humanos para la serialización de datos. Se utiliza en Nette para archivos de configuración. [api:Nette\Neon\Neon] es una clase estática para trabajar con NEON. -Familiarícese con el [formato NEON|format] y [pruébelo |https://ne-on.org]. +Familiarícese con el [formato NEON|format] y [pruébelo |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/es/format.texy b/neon/es/format.texy index 8503a12140..802c47aabe 100644 --- a/neon/es/format.texy +++ b/neon/es/format.texy @@ -2,7 +2,7 @@ Formato NEON ************ .[perex] -NEON es un formato de datos estructurados legible por humanos. En Nette se utiliza para archivos de configuración. También se utiliza para datos estructurados, como configuraciones, traducciones de idiomas, etc. [Pruébelo|https://ne-on.org]. +NEON es un formato de datos estructurados legible por humanos. En Nette se utiliza para archivos de configuración. También se utiliza para datos estructurados, como configuraciones, traducciones de idiomas, etc. [Pruébelo|https://fiddle.nette.org/neon/]. NEON es la abreviatura de *Nette Object Notation*. Es menos complejo y torpe que XML o JSON, pero proporciona funciones similares. Es muy similar a YAML. La principal ventaja es que NEON tiene las llamadas [#entidades], gracias a las cuales la configuración de los servicios DI es [tan sexy |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. Y permite la indentación con tabuladores. diff --git a/neon/fr/@home.texy b/neon/fr/@home.texy index 280177a817..5718b806ce 100644 --- a/neon/fr/@home.texy +++ b/neon/fr/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON est un langage de sérialisation de données lisible par l'homme. Il est utilisé dans Nette pour les fichiers de configuration. [api:Nette\Neon\Neon] est une classe statique pour travailler avec NEON. -Familiarisez-vous avec le [format NEON|format] et [essayez-le |https://ne-on.org]. +Familiarisez-vous avec le [format NEON|format] et [essayez-le |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/fr/format.texy b/neon/fr/format.texy index 87c410844f..d6bb2a37cf 100644 --- a/neon/fr/format.texy +++ b/neon/fr/format.texy @@ -2,7 +2,7 @@ Format NEON *********** .[perex] -NEON est un format de données structurées lisible par l'homme. Dans Nette, il est utilisé pour les fichiers de configuration. Il est également utilisé pour les données structurées, telles que les paramètres, les traductions linguistiques, etc. [Essayez-le|https://ne-on.org]. +NEON est un format de données structurées lisible par l'homme. Dans Nette, il est utilisé pour les fichiers de configuration. Il est également utilisé pour les données structurées, telles que les paramètres, les traductions linguistiques, etc. [Essayez-le|https://fiddle.nette.org/neon/]. NEON est l'acronyme de *Nette Object Notation*. Il est moins complexe et lourd que XML ou JSON, mais offre des fonctionnalités similaires. Il est très similaire à YAML. Le principal avantage est que NEON possède ce qu'on appelle des [#entités], grâce auxquelles la configuration des services DI est [aussi sexy |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. Et il permet d'indenter avec des tabulations. diff --git a/neon/hu/@home.texy b/neon/hu/@home.texy index 2b63918f98..21bcd0f12e 100644 --- a/neon/hu/@home.texy +++ b/neon/hu/@home.texy @@ -5,7 +5,7 @@ Nette NEON A NEON egy ember által olvasható adatszerializációs nyelv. A Nette-ben konfigurációs fájlokhoz használják. A [api:Nette\Neon\Neon] egy statikus osztály a NEON-nal való munkához. -Ismerkedjen meg a [NEON formátummal|format] és [próbálja ki |https://ne-on.org]. +Ismerkedjen meg a [NEON formátummal|format] és [próbálja ki |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/hu/format.texy b/neon/hu/format.texy index a47bd628d7..1ce3208a68 100644 --- a/neon/hu/format.texy +++ b/neon/hu/format.texy @@ -2,7 +2,7 @@ NEON Formátum ************* .[perex] -A NEON egy ember által olvasható, strukturált adatformátum. A Nette-ben konfigurációs fájlokhoz használják. Strukturált adatokhoz is használják, mint például beállítások, nyelvi fordítások stb. [Próbálja ki|https://ne-on.org]. +A NEON egy ember által olvasható, strukturált adatformátum. A Nette-ben konfigurációs fájlokhoz használják. Strukturált adatokhoz is használják, mint például beállítások, nyelvi fordítások stb. [Próbálja ki|https://fiddle.nette.org/neon/]. A NEON a *Nette Object Notation* rövidítése. Kevésbé bonyolult és nehézkes, mint az XML vagy a JSON, de hasonló funkciókat biztosít. Nagyon hasonlít a YAML-hez. A fő előnye az, hogy a NEON-nak vannak úgynevezett [entity |#Entitások] entitásai, amelyeknek köszönhetően a DI szolgáltatások konfigurálása [is szexi |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. És lehetővé teszi a tabulátorokkal történő behúzást. diff --git a/neon/it/@home.texy b/neon/it/@home.texy index 3358f69547..426acfeb41 100644 --- a/neon/it/@home.texy +++ b/neon/it/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON è un linguaggio leggibile dall'uomo per la serializzazione dei dati. Viene utilizzato in Nette per i file di configurazione. [api:Nette\Neon\Neon] è una classe statica per lavorare con NEON. -Scopri il [formato NEON|format] e [provalo |https://ne-on.org]. +Scopri il [formato NEON|format] e [provalo |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/it/format.texy b/neon/it/format.texy index 347d561bc0..29895c0557 100644 --- a/neon/it/format.texy +++ b/neon/it/format.texy @@ -2,7 +2,7 @@ Formato NEON ************ .[perex] -NEON è un formato di dati strutturati leggibile dall'uomo. In Nette viene utilizzato per i file di configurazione. Viene utilizzato anche per dati strutturati come impostazioni, traduzioni linguistiche, ecc. [Provatelo|https://ne-on.org]. +NEON è un formato di dati strutturati leggibile dall'uomo. In Nette viene utilizzato per i file di configurazione. Viene utilizzato anche per dati strutturati come impostazioni, traduzioni linguistiche, ecc. [Provatelo|https://fiddle.nette.org/neon/]. NEON è l'acronimo di *Nette Object Notation*. È meno complesso e goffo di XML o JSON, ma fornisce funzionalità simili. È molto simile a YAML. Il vantaggio principale è che NEON ha le cosiddette [#entità], grazie alle quali la configurazione dei servizi DI è [così sexy |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. E consente l'indentazione con tabulazioni. diff --git a/neon/ja/@home.texy b/neon/ja/@home.texy index c1646c3b4d..4dd5573af1 100644 --- a/neon/ja/@home.texy +++ b/neon/ja/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEONは、人間が理解しやすいデータシリアライズ言語です。Netteでは設定ファイルに使用されます。[api:Nette\Neon\Neon]は、NEONを操作するための静的クラスです。 -[NEON 形式|format]について学び、[試してみてください |https://ne-on.org]。 +[NEON 形式|format]について学び、[試してみてください |https://fiddle.nette.org/neon/]。 </div> diff --git a/neon/ja/format.texy b/neon/ja/format.texy index 4d668d9660..bb34a28dd0 100644 --- a/neon/ja/format.texy +++ b/neon/ja/format.texy @@ -2,7 +2,7 @@ NEONフォーマット ********** .[perex] -NEONは、人間が判読可能な構造化データ形式です。Netteでは設定ファイルに使用されます。また、設定、言語翻訳などの構造化データにも使用されます。[試してみてください |https://ne-on.org]。 +NEONは、人間が判読可能な構造化データ形式です。Netteでは設定ファイルに使用されます。また、設定、言語翻訳などの構造化データにも使用されます。[試してみてください |https://fiddle.nette.org/neon/]。 NEONは *Nette Object Notation* の略です。XMLやJSONよりも複雑でなく、扱いにくくありませんが、同様の機能を提供します。YAMLに非常に似ています。主な利点は、NEONにはいわゆる[#エンティティ]があり、これによりDIサービスの[設定 |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]が非常に洗練されることです。そして、タブによるインデントが可能です。 diff --git a/neon/pl/@home.texy b/neon/pl/@home.texy index 133d536c91..4419eac6b0 100644 --- a/neon/pl/@home.texy +++ b/neon/pl/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON to czytelny dla człowieka język do serializacji danych. Jest używany w Nette do plików konfiguracyjnych. [api:Nette\Neon\Neon] to statyczna klasa do pracy z NEONem. -Zapoznaj się z [formatem NEON|format] i [wypróbuj go |https://ne-on.org]. +Zapoznaj się z [formatem NEON|format] i [wypróbuj go |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/pl/format.texy b/neon/pl/format.texy index 98a2ecc681..f70c1ab533 100644 --- a/neon/pl/format.texy +++ b/neon/pl/format.texy @@ -2,7 +2,7 @@ Format NEON *********** .[perex] -NEON to czytelny dla człowieka format danych strukturalnych. W Nette jest używany do plików konfiguracyjnych. Jest również używany do danych strukturalnych, takich jak ustawienia, tłumaczenia językowe itp. [Wypróbuj go|https://ne-on.org]. +NEON to czytelny dla człowieka format danych strukturalnych. W Nette jest używany do plików konfiguracyjnych. Jest również używany do danych strukturalnych, takich jak ustawienia, tłumaczenia językowe itp. [Wypróbuj go|https://fiddle.nette.org/neon/]. NEON to skrót od *Nette Object Notation*. Jest mniej skomplikowany i nieporęczny niż XML czy JSON, ale zapewnia podobne funkcje. Jest bardzo podobny do YAML. Główną zaletą jest to, że NEON ma tak zwane [#encje], dzięki którym konfiguracja usług DI jest [również seksowna |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. I pozwala na wcięcia za pomocą tabulatorów. diff --git a/neon/pt/@home.texy b/neon/pt/@home.texy index a15233b262..4c1962f4e5 100644 --- a/neon/pt/@home.texy +++ b/neon/pt/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON é uma linguagem de serialização de dados legível por humanos. É usado no Nette para arquivos de configuração. [api:Nette\Neon\Neon] é uma classe estática para trabalhar com NEON. -Familiarize-se com o [formato NEON |format] e [experimente |https://ne-on.org]. +Familiarize-se com o [formato NEON |format] e [experimente |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/pt/format.texy b/neon/pt/format.texy index 6de60e563b..4cbb9c9979 100644 --- a/neon/pt/format.texy +++ b/neon/pt/format.texy @@ -2,7 +2,7 @@ Formato NEON ************ .[perex] -NEON é um formato de dados estruturados legível por humanos. No Nette, é usado para arquivos de configuração. Também é usado para dados estruturados, como configurações, traduções de idiomas, etc. [Experimente |https://ne-on.org]. +NEON é um formato de dados estruturados legível por humanos. No Nette, é usado para arquivos de configuração. Também é usado para dados estruturados, como configurações, traduções de idiomas, etc. [Experimente |https://fiddle.nette.org/neon/]. NEON é a abreviação de *Nette Object Notation*. É menos complexo e desajeitado que XML ou JSON, mas fornece recursos semelhantes. É muito semelhante ao YAML. A principal vantagem é que o NEON possui as chamadas [#entidades], graças às quais a configuração dos serviços de DI é [tão sexy |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. E permite indentação com tabulações. diff --git a/neon/ro/@home.texy b/neon/ro/@home.texy index d25985bcb0..af4d0d2fd4 100644 --- a/neon/ro/@home.texy +++ b/neon/ro/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON este un limbaj ușor de înțeles pentru serializarea datelor. Este utilizat în Nette pentru fișierele de configurare. [api:Nette\Neon\Neon] este o clasă statică pentru lucrul cu NEON. -Familiarizați-vă cu [formatul NEON|format] și [încercați-l |https://ne-on.org]. +Familiarizați-vă cu [formatul NEON|format] și [încercați-l |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/ro/format.texy b/neon/ro/format.texy index 98b69e1935..3206bc7bbb 100644 --- a/neon/ro/format.texy +++ b/neon/ro/format.texy @@ -2,7 +2,7 @@ Formatul NEON ************* .[perex] -NEON este un format de date structurate lizibil pentru om. În Nette, este utilizat pentru fișierele de configurare. De asemenea, este utilizat pentru date structurate, cum ar fi setări, traduceri lingvistice etc. [Încercați-l|https://ne-on.org]. +NEON este un format de date structurate lizibil pentru om. În Nette, este utilizat pentru fișierele de configurare. De asemenea, este utilizat pentru date structurate, cum ar fi setări, traduceri lingvistice etc. [Încercați-l|https://fiddle.nette.org/neon/]. NEON este acronimul pentru *Nette Object Notation*. Este mai puțin complex și greoi decât XML sau JSON, dar oferă funcționalități similare. Este foarte asemănător cu YAML. Principalul avantaj este că NEON are așa-numitele [#entități], datorită cărora configurarea serviciilor DI este [la fel de sexy |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. Și permite indentarea cu tabulatori. diff --git a/neon/ru/@home.texy b/neon/ru/@home.texy index b147c9f670..0678c994c7 100644 --- a/neon/ru/@home.texy +++ b/neon/ru/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON — это понятный человеку язык для сериализации данных. Он используется в Nette для конфигурационных файлов. [api:Nette\Neon\Neon] — это статический класс для работы с NEON. -Ознакомьтесь с [форматом NEON|format] и [попробуйте его |https://ne-on.org]. +Ознакомьтесь с [форматом NEON|format] и [попробуйте его |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/ru/format.texy b/neon/ru/format.texy index b34e014c24..0420b2c34c 100644 --- a/neon/ru/format.texy +++ b/neon/ru/format.texy @@ -2,7 +2,7 @@ *********** .[perex] -NEON — это человекочитаемый формат структурированных данных. В Nette он используется для конфигурационных файлов. Он также используется для структурированных данных, таких как настройки, языковые переводы и т. д. [Попробуйте его|https://ne-on.org]. +NEON — это человекочитаемый формат структурированных данных. В Nette он используется для конфигурационных файлов. Он также используется для структурированных данных, таких как настройки, языковые переводы и т. д. [Попробуйте его|https://fiddle.nette.org/neon/]. NEON — это аббревиатура от *Nette Object Notation*. Он менее сложен и громоздок, чем XML или JSON, но предоставляет схожие функции. Он очень похож на YAML. Главное преимущество заключается в том, что NEON имеет так называемые [#сущности], благодаря которым конфигурация DI-сервисов [тоже сексуальна |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. И позволяет использовать табуляцию для отступов. diff --git a/neon/sl/@home.texy b/neon/sl/@home.texy index a944ff0832..dd379d3b09 100644 --- a/neon/sl/@home.texy +++ b/neon/sl/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON je človeku razumljiv jezik za serializacijo podatkov. V Nette se uporablja za konfiguracijske datoteke. [api:Nette\Neon\Neon] je statični razred za delo z NEONom. -Spoznajte [format NEON|format] in [ga preizkusite |https://ne-on.org]. +Spoznajte [format NEON|format] in [ga preizkusite |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/sl/format.texy b/neon/sl/format.texy index 25fe9ceed2..3d33a746a7 100644 --- a/neon/sl/format.texy +++ b/neon/sl/format.texy @@ -2,7 +2,7 @@ Format NEON *********** .[perex] -NEON je človeško berljiv strukturiran podatkovni format. V Nette se uporablja za konfiguracijske datoteke. Uporablja se tudi za strukturirane podatke, kot so nastavitve, jezikovni prevodi itd. [Preizkusite ga|https://ne-on.org]. +NEON je človeško berljiv strukturiran podatkovni format. V Nette se uporablja za konfiguracijske datoteke. Uporablja se tudi za strukturirane podatke, kot so nastavitve, jezikovni prevodi itd. [Preizkusite ga|https://fiddle.nette.org/neon/]. NEON je okrajšava za *Nette Object Notation*. Je manj zapleten in okoren kot XML ali JSON, vendar zagotavlja podobne funkcije. Je zelo podoben YAML. Glavna prednost je v tem, da ima NEON tako imenovane [#Entitete], zahvaljujoč katerim je konfiguracija DI storitev [tudi seksi |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. In omogoča zamikanje s tabulatorji. diff --git a/neon/tr/@home.texy b/neon/tr/@home.texy index 05d3e314f1..85a02ccf3a 100644 --- a/neon/tr/@home.texy +++ b/neon/tr/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON, verilerin serileştirilmesi için insan tarafından okunabilir bir dildir. Nette'de yapılandırma dosyaları için kullanılır. [api:Nette\Neon\Neon], NEON ile çalışmak için statik bir sınıftır. -[NEON biçimiyle |format] tanışın ve [onu deneyin |https://ne-on.org]. +[NEON biçimiyle |format] tanışın ve [onu deneyin |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/tr/format.texy b/neon/tr/format.texy index 7d724df789..62bf99acf4 100644 --- a/neon/tr/format.texy +++ b/neon/tr/format.texy @@ -2,7 +2,7 @@ NEON Formatı ************ .[perex] -NEON, insan tarafından okunabilir yapılandırılmış bir veri formatıdır. Nette'de yapılandırma dosyaları için kullanılır. Ayrıca ayarlar, dil çevirileri vb. gibi yapılandırılmış veriler için de kullanılır. [Deneyin |https://ne-on.org]. +NEON, insan tarafından okunabilir yapılandırılmış bir veri formatıdır. Nette'de yapılandırma dosyaları için kullanılır. Ayrıca ayarlar, dil çevirileri vb. gibi yapılandırılmış veriler için de kullanılır. [Deneyin |https://fiddle.nette.org/neon/]. NEON, *Nette Object Notation*'ın kısaltmasıdır. XML veya JSON'dan daha az karmaşık ve hantaldır, ancak benzer işlevler sunar. YAML'ye çok benzer. Ana avantajı, NEON'un DI servislerinin yapılandırılmasını [çok seksi |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f] yapan sözde [#Varlıklar (Entities)]'a sahip olmasıdır. Ve sekmelerle girintilemeye izin verir. diff --git a/neon/uk/@home.texy b/neon/uk/@home.texy index bd187fa15f..19805388d7 100644 --- a/neon/uk/@home.texy +++ b/neon/uk/@home.texy @@ -5,7 +5,7 @@ Nette NEON NEON — це зрозуміла для людини мова серіалізації даних. Вона використовується в Nette для конфігураційних файлів. [api:Nette\Neon\Neon] — це статичний клас для роботи з NEON. -Ознайомтеся з [форматом NEON|format] та [спробуйте його |https://ne-on.org]. +Ознайомтеся з [форматом NEON|format] та [спробуйте його |https://fiddle.nette.org/neon/]. </div> diff --git a/neon/uk/format.texy b/neon/uk/format.texy index d168a547a3..9229ee9561 100644 --- a/neon/uk/format.texy +++ b/neon/uk/format.texy @@ -2,7 +2,7 @@ *********** .[perex] -NEON — це читабельний для людини структурований формат даних. У Nette він використовується для конфігураційних файлів. Також він використовується для структурованих даних, таких як налаштування, мовні переклади тощо. [Спробуйте його |https://ne-on.org]. +NEON — це читабельний для людини структурований формат даних. У Nette він використовується для конфігураційних файлів. Також він використовується для структурованих даних, таких як налаштування, мовні переклади тощо. [Спробуйте його |https://fiddle.nette.org/neon/]. NEON — це абревіатура від *Nette Object Notation*. Він менш складний і громіздкий, ніж XML або JSON, але надає схожі функції. Він дуже схожий на YAML. Головна перевага полягає в тому, що NEON має так звані [#сутності], завдяки яким конфігурація DI-сервісів [також сексі |https://gist.github.com/dg/26baf3ce8f29d0f751e9dddfaa06504f]. І дозволяє використовувати табуляцію для відступів. diff --git a/www/bg/packages.texy b/www/bg/packages.texy index cc362f1f78..8915f6a226 100644 --- a/www/bg/packages.texy +++ b/www/bg/packages.texy @@ -12,7 +12,7 @@ | **Http**:[http:] | Слой, капсулиращ HTTP заявка & отговор | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Страхотна система за шаблони | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Изпращане на имейли | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Четене и запис на формат [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Четене и запис на формат [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Генератор на PHP код | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | Най-удобното автоматично зареждане | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Маршрутизация | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/cs/packages.texy b/www/cs/packages.texy index 22e1d9d7e2..01ad3a5618 100644 --- a/www/cs/packages.texy +++ b/www/cs/packages.texy @@ -12,7 +12,7 @@ Seznam balíčků Nette | **Http**:[http:] | Vrstva zapouzdřující HTTP request & response | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Skvělý šablonovací systém | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Odesílání e-mailů | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Čtení a zápis formátu [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Čtení a zápis formátu [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Generátor PHP kódu | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | Nejkomfortnější autoloading | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Routování | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/de/packages.texy b/www/de/packages.texy index 1eabb71e2e..da060914a0 100644 --- a/www/de/packages.texy +++ b/www/de/packages.texy @@ -12,7 +12,7 @@ Liste der Nette-Pakete | **Http**:[http:] | Schicht zur Kapselung von HTTP Request & Response | GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Großartiges Template-System | GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Senden von E-Mails | GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Lesen und Schreiben des [NEON |https://ne-on.org] Formats | GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Lesen und Schreiben des [NEON |https://fiddle.nette.org/neon/] Formats | GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | PHP-Code-Generator | GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | Das komfortabelste Autoloading | GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Routing | GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/el/packages.texy b/www/el/packages.texy index b4289bca58..a609f055d0 100644 --- a/www/el/packages.texy +++ b/www/el/packages.texy @@ -12,7 +12,7 @@ | **Http**:[http:] | Επίπεδο που ενσωματώνει το HTTP request & response | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Εξαιρετικό σύστημα προτύπων | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Αποστολή e-mail | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Ανάγνωση και εγγραφή της μορφής [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Ανάγνωση και εγγραφή της μορφής [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Γεννήτρια κώδικα PHP | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | Η πιο άνετη αυτόματη φόρτωση | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Δρομολόγηση | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/en/packages.texy b/www/en/packages.texy index 61945fc199..60d091be49 100644 --- a/www/en/packages.texy +++ b/www/en/packages.texy @@ -12,7 +12,7 @@ List of Packages | **Http**:[http:] | Layer encapsulating HTTP request & response | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Amazing template engine | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Sending emails | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Reading and writing [NEON format |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Reading and writing [NEON format |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | PHP code generator | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | The most comfortable class autoloading | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | URL routing | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/es/packages.texy b/www/es/packages.texy index ae2279ea62..6c48cc5a33 100644 --- a/www/es/packages.texy +++ b/www/es/packages.texy @@ -12,7 +12,7 @@ Lista de paquetes de Nette | **Http**:[http:] | Capa que encapsula la petición y respuesta HTTP | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Excelente sistema de plantillas | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Envío de correos electrónicos | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Lectura y escritura del formato [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Lectura y escritura del formato [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Generador de código PHP | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | El autoloading más cómodo | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Enrutamiento | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/fr/packages.texy b/www/fr/packages.texy index 99c919906e..ecb97d45fd 100644 --- a/www/fr/packages.texy +++ b/www/fr/packages.texy @@ -12,7 +12,7 @@ Liste des paquets Nette | **[Http |http:]** | Couche encapsulant la requête & réponse HTTP | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **[Latte |latte:]** | Excellent système de template | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **[Mail |mail:]** | Envoi d'e-mails | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **[Neon |neon:]** | Lecture et écriture du format [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **[Neon |neon:]** | Lecture et écriture du format [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **[Php Generator |php-generator:]** | Générateur de code PHP | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **[Robot Loader |robot-loader:]** | Autoloading le plus confortable | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **[Routing |application:routing]** | Routage | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/hu/packages.texy b/www/hu/packages.texy index a7b9279ea5..a7045aeafa 100644 --- a/www/hu/packages.texy +++ b/www/hu/packages.texy @@ -12,7 +12,7 @@ Nette csomagok listája | **Http**:[http:] | HTTP kérést és választ beágyazó réteg | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Nagyszerű sablonrendszer | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | E-mailek küldése | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | [NEON |https://ne-on.org] formátum olvasása és írása | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | [NEON |https://fiddle.nette.org/neon/] formátum olvasása és írása | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | PHP kódgenerátor | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | A legkényelmesebb autoloading | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Útválasztás | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/it/packages.texy b/www/it/packages.texy index f0ba73804f..27503bc5a7 100644 --- a/www/it/packages.texy +++ b/www/it/packages.texy @@ -12,7 +12,7 @@ Elenco dei pacchetti Nette | **Http**:[http:] | Livello che incapsula HTTP request & response | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Fantastico sistema di template | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Invio di e-mail | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Lettura e scrittura del formato [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Lettura e scrittura del formato [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Generatore di codice PHP | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | L'autoloading più comodo | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Routing | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/ja/packages.texy b/www/ja/packages.texy index f458e2463c..d968ddcccc 100644 --- a/www/ja/packages.texy +++ b/www/ja/packages.texy @@ -12,7 +12,7 @@ Netteパッケージ一覧 | **Http**:[http:] | HTTPリクエスト&レスポンスをカプセル化する層 | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | 素晴らしいテンプレートシステム | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | メール送信 | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | [NEON |https://ne-on.org] 形式の読み書き | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | [NEON |https://fiddle.nette.org/neon/] 形式の読み書き | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | PHPコードジェネレータ | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | 最も快適なオートローディング | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | ルーティング | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/pl/packages.texy b/www/pl/packages.texy index 83d35268fa..f2ad154f60 100644 --- a/www/pl/packages.texy +++ b/www/pl/packages.texy @@ -12,7 +12,7 @@ Lista pakietów Nette | **Http**:[http:] | Warstwa hermetyzująca żądanie i odpowiedź HTTP | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Doskonały system szablonów | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Wysyłanie e-maili | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Odczyt i zapis formatu [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Odczyt i zapis formatu [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Generator kodu PHP | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | Najwygodniejszy autoloading | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Routing | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/pt/packages.texy b/www/pt/packages.texy index e017413f0a..a9c9fe63d7 100644 --- a/www/pt/packages.texy +++ b/www/pt/packages.texy @@ -12,7 +12,7 @@ Lista de Pacotes Nette | **Http**:[http:] | Camada encapsulando requisição & resposta HTTP | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Ótimo sistema de templates | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Envio de e-mails | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Leitura e escrita do formato [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Leitura e escrita do formato [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Gerador de código PHP | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | O autoloading mais confortável | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Roteamento | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/ro/packages.texy b/www/ro/packages.texy index 6dfbb6916f..a77a3ef94a 100644 --- a/www/ro/packages.texy +++ b/www/ro/packages.texy @@ -12,7 +12,7 @@ Lista pachetelor Nette | **Http**:[http:] | Strat care încapsulează cererea & răspunsul HTTP | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Sistem excelent de șabloane | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Trimiterea de e-mailuri | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Citirea și scrierea formatului [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Citirea și scrierea formatului [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Generator de cod PHP | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | Cel mai confortabil autoloading | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Rutare | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/ru/packages.texy b/www/ru/packages.texy index 70d68370c5..fa212efcd5 100644 --- a/www/ru/packages.texy +++ b/www/ru/packages.texy @@ -12,7 +12,7 @@ | **Http**:[http:] | Слой, инкапсулирующий HTTP request & response | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Отличная система шаблонов | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Отправка электронной почты | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Чтение и запись формата [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Чтение и запись формата [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Генератор PHP-кода | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | Самый удобный автозагрузчик | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Маршрутизация | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/sl/packages.texy b/www/sl/packages.texy index 81a32cb6c7..9b3c556109 100644 --- a/www/sl/packages.texy +++ b/www/sl/packages.texy @@ -12,7 +12,7 @@ Seznam paketov Nette | **Http**:[http:] | Plast, ki inkapsulira HTTP zahtevo & odgovor | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Odličen sistem predlog | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Pošiljanje e-pošte | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Branje in pisanje formata [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Branje in pisanje formata [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Generator PHP kode | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | Najudobnejši autoloading | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Usmerjanje | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/tr/packages.texy b/www/tr/packages.texy index d53e725ad0..644f78541e 100644 --- a/www/tr/packages.texy +++ b/www/tr/packages.texy @@ -12,7 +12,7 @@ Nette Paket Listesi | **Http**:[http:] | HTTP isteğini ve yanıtını kapsayan katman | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Harika şablonlama sistemi | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | E-posta gönderme | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | [NEON |https://ne-on.org] formatını okuma ve yazma | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | [NEON |https://fiddle.nette.org/neon/] formatını okuma ve yazma | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | PHP kodu oluşturucu | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | En konforlu otomatik yükleme | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Yönlendirme | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] diff --git a/www/uk/packages.texy b/www/uk/packages.texy index 0e639722d6..983e47f622 100644 --- a/www/uk/packages.texy +++ b/www/uk/packages.texy @@ -12,7 +12,7 @@ | **Http**:[http:] | Шар, що інкапсулює HTTP request & response | [GitHub |https://github.com/nette/http] [API |https://api.nette.org/http/] | **Latte**:[latte:] | Чудова система шаблонів | [GitHub |https://github.com/nette/latte] [API |https://api.nette.org/latte/] | **Mail**:[mail:] | Надсилання електронних листів | [GitHub |https://github.com/nette/mail] [API |https://api.nette.org/mail/] -| **Neon**:[neon:] | Читання та запис формату [NEON |https://ne-on.org] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] +| **Neon**:[neon:] | Читання та запис формату [NEON |https://fiddle.nette.org/neon/] | [GitHub |https://github.com/nette/neon] [API |https://api.nette.org/neon/] | **Php Generator**:[php-generator:] | Генератор PHP коду | [GitHub |https://github.com/nette/php-generator] [API |https://api.nette.org/php-generator/] | **Robot Loader**:[robot-loader:] | Найкомфортніше автозавантаження | [GitHub |https://github.com/nette/robot-loader] [API |https://api.nette.org/robot-loader/] | **Routing**:[application:routing] | Маршрутизація | [GitHub |https://github.com/nette/routing] [API |https://api.nette.org/routing/] From bf6918a6e21b2ff25353f14fb67955953a22eaa2 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Wed, 22 Oct 2025 00:18:43 +0200 Subject: [PATCH 003/112] texy: extensive documentation update --- texy/cs/@home.texy | 126 +++- texy/cs/@menu.texy | 15 +- texy/cs/@try.texy | 15 - texy/cs/api-block-module.texy | 11 - texy/cs/api-blockquote-module.texy | 8 - texy/cs/api-emoticon-module.texy | 47 -- texy/cs/api-figure-module.texy | 33 - texy/cs/api-heading-module.texy | 42 -- texy/cs/api-horizline-module.texy | 8 - texy/cs/api-html-module.texy | 21 - texy/cs/api-htmloutput-module.texy | 17 - texy/cs/api-image-module.texy | 35 - texy/cs/api-link-module.texy | 42 -- texy/cs/api-list-module.texy | 12 - texy/cs/api-longwords-module.texy | 19 - texy/cs/api-paragraph-module.texy | 4 - texy/cs/api-phrase-module.texy | 22 - texy/cs/api-script-module.texy | 23 - texy/cs/api-table-module.texy | 18 - texy/cs/api-texy.texy | 69 -- texy/cs/api-typography-module.texy | 21 - texy/cs/api-zaklady.texy | 25 - texy/cs/api.texy | 28 - texy/cs/architecture.texy | 425 ++++++++++++ texy/cs/configuration.texy | 719 ++++++++++++++++++++ texy/cs/custom-handlers.texy | 850 +++++++++++++++++++++++ texy/cs/custom-syntax.texy | 519 ++++++++++++++ texy/cs/develop.texy | 35 + texy/cs/priklady-vyuziti.texy | 26 - texy/cs/quickstart.texy | 205 ++++++ texy/cs/syntax-podrobne.texy | 889 ------------------------ texy/cs/syntax.texy | 997 +++++++++++++++++++-------- texy/cs/texy-vs-wysiwyg.texy | 23 - texy/cs/try-settings.texy | 4 +- texy/en/@home.texy | 125 +++- texy/en/@menu.texy | 10 +- texy/en/@try.texy | 15 - texy/en/architecture.texy | 425 ++++++++++++ texy/en/configuration.texy | 719 ++++++++++++++++++++ texy/en/custom-handlers.texy | 850 +++++++++++++++++++++++ texy/en/custom-syntax.texy | 519 ++++++++++++++ texy/en/develop.texy | 35 + texy/en/quickstart.texy | 205 ++++++ texy/en/syntax-full.texy | 889 ------------------------ texy/en/syntax.texy | 1002 ++++++++++++++++++++-------- texy/en/try-settings.texy | 4 +- 46 files changed, 7163 insertions(+), 2988 deletions(-) delete mode 100644 texy/cs/@try.texy delete mode 100644 texy/cs/api-block-module.texy delete mode 100644 texy/cs/api-blockquote-module.texy delete mode 100644 texy/cs/api-emoticon-module.texy delete mode 100644 texy/cs/api-figure-module.texy delete mode 100644 texy/cs/api-heading-module.texy delete mode 100644 texy/cs/api-horizline-module.texy delete mode 100644 texy/cs/api-html-module.texy delete mode 100644 texy/cs/api-htmloutput-module.texy delete mode 100644 texy/cs/api-image-module.texy delete mode 100644 texy/cs/api-link-module.texy delete mode 100644 texy/cs/api-list-module.texy delete mode 100644 texy/cs/api-longwords-module.texy delete mode 100644 texy/cs/api-paragraph-module.texy delete mode 100644 texy/cs/api-phrase-module.texy delete mode 100644 texy/cs/api-script-module.texy delete mode 100644 texy/cs/api-table-module.texy delete mode 100644 texy/cs/api-texy.texy delete mode 100644 texy/cs/api-typography-module.texy delete mode 100644 texy/cs/api-zaklady.texy delete mode 100644 texy/cs/api.texy create mode 100644 texy/cs/architecture.texy create mode 100644 texy/cs/configuration.texy create mode 100644 texy/cs/custom-handlers.texy create mode 100644 texy/cs/custom-syntax.texy create mode 100644 texy/cs/develop.texy delete mode 100644 texy/cs/priklady-vyuziti.texy create mode 100644 texy/cs/quickstart.texy delete mode 100644 texy/cs/syntax-podrobne.texy delete mode 100644 texy/cs/texy-vs-wysiwyg.texy delete mode 100644 texy/en/@try.texy create mode 100644 texy/en/architecture.texy create mode 100644 texy/en/configuration.texy create mode 100644 texy/en/custom-handlers.texy create mode 100644 texy/en/custom-syntax.texy create mode 100644 texy/en/develop.texy create mode 100644 texy/en/quickstart.texy delete mode 100644 texy/en/syntax-full.texy diff --git a/texy/cs/@home.texy b/texy/cs/@home.texy index 3a23512250..5930451575 100644 --- a/texy/cs/@home.texy +++ b/texy/cs/@home.texy @@ -1,32 +1,120 @@ Texy! je sexy! -============== +************** -Texy je program, díky kterému můžete snadno, bez odborných znalostí, psát texty na webové stránky. +.[perex] +Texy je **výkonný a bezpečný markup procesor** pro PHP, který převádí jednoduchý text do validního HTML. Na rozdíl od jiných markup jazyků není Texy jen další variantou Markdown - je to **plně konfigurovatelný systém**, který můžete přizpůsobit prakticky jakékoliv syntaxi. -Chcete zvýraznit písmo? Vytvořit nadpis či odrážky? Přidat obrázek nebo tabulku? Nemusíte zápasit se složitým textovým editorem. Stačí psát prostý text a Texy už úpravu zvládne za vás. Výsledkem bude hezky zformátovaná stránka. ---> [Vyzkoušejte si to | https://fiddle.nette.org/texy/] +Proč Texy? +========== -Texy dnes používají [tisíce spokojených uživatelů | napsali o Texy]. +Bezpečnost na prvním místě +-------------------------- -Co všechno umí? ---------------- +Texy je navrženo s důrazem na bezpečnost. Automaticky **chrání před XSS útoky**, validuje URL adresy a filtruje nebezpečné HTML značky. Vestavěný `safeMode()` je ideální pro zpracování uživatelského obsahu v komentářích nebo na fórech. -- vytvářet odkazy, odrážky, tabulky,... -- vkládat do textu obrázky -- zná českou typografii -- a navíc je **zdarma!** (pod licencí BSD a GPL) -- generuje vždy validní HTML kód -- vkládá pevné mezery za jednopísmenné předložky -- je dokonale konfigurovatelné a přizpůsobitelné +```php +Texy\Configurator::safeMode($texy); +// Nyní je Texy bezpečné pro obsah od uživatelů +``` -Objevte Texy! -------------- +Konfigurovatelnost bez kompromisů +--------------------------------- + +Chcete používat Markdown syntaxi? Nebo potřebujete úplně vlastní markup? **Texy to zvládne.** Můžete: + +- Vypnout nebo zapnout libovolné části syntaxe +- Změnit výchozí chování pomocí handlerů +- Přidat zcela vlastní syntaktické prvky +- Nakonfigurovat Texy tak, aby zpracovávalo Markdown nebo jakýkoliv jiný formát + +```php +$texy = new Texy; +$texy->allowed['image'] = false; // vypnout obrázky +$texy->allowed['phrase/strong'] = false; // vypnout tučné písmo +``` + + +České typografické speciality +----------------------------- + +Texy **dokonale rozumí češtině**. Automaticky: + +- Vkládá **pevné mezery** za jednopísmenné předložky a spojky: v autě, u okna, s kamarádem +- Rozděluje **dlouhá slova** podle slabik: nejneobhospodařovávatelnějšími +- Používá správné **typografické uvozovky**: „dvojité“ a ‚jednoduché‘ +- Zaměňuje **spojovník za pomlčku**: 10-15 vs. česko-slovenský +- Přidává **nezalomitelné mezery** u telefonních čísel: +420 776 552 046 + + +Validní a wellformed HTML +------------------------- + +Texy generuje **vždy validní HTML5 kód**. Automaticky opravuje chybně vnořené značky, uzavírá nezavřené elementy a dbá na správnou strukturu dokumentu. Výstup je nejen validní, ale i **pěkně naformátovaný** s odsazením. + + +Co je Texy? +=========== + +Texy je **obecný procesor markup textu**. To znamená, že má sice svou výchozí syntaxi (podobnou Markdown, ale mnohem bohatší), ale můžete ji kompletně změnit nebo rozšířit. + +**Není to jen parser** - Texy je komplexní systém s modulární architekturou, kde každý modul zpracovává konkrétní část syntaxe (nadpisy, odkazy, obrázky, tabulky...). Díky systému handlerů můžete zasáhnout do libovolného bodu zpracování a změnit výsledek podle svých potřeb. + + +Texy vs. Markdown +================= + +Základní syntaxe je podobná, ale Texy nabízí mnohem více: + +|--------------------------- +| Funkce | Markdown | Texy +|--------------------------- +| Tučné písmo | `**text**` | `**text**` +| Kurzíva | `*text*` nebo `_text_` | `*text*` nebo `//text//` +| Nadpisy | `# Nadpis` | `# Nadpis` nebo podtržení +| Obrázky | `![alt](url)` | `[* url *]` +| Tabulky | omezené | plná podpora včetně sloučení +| Modifikátory | ne | ano - `.{color:red}[class]` +| Typografie | ne | ano - uvozovky, pomlčky, mezery +| Dělení slov | ne | ano - podle slabik +| Konfigurovatelnost | omezená | úplná - vlastní syntaxe +| Bezpečnost | závisí na impl. | vestavěná (safeMode) + +**Příklad rozdílů:** + +```texy +Markdown: +![Obrázek](image.jpg) + +Texy: +[* image.jpg 300x200 .(Popisek obrázku)[photo] <] +``` + +Texy umožňuje definovat rozměry, třídy, zarovnání a mnoho dalšího přímo v syntaxi. + + +Kdy použít Texy? +================ + +Texy je ideální pro: + +**CMS systémy** Potřebujete bezpečně zpracovávat obsah od editorů? Texy nabízí granulární kontrolu nad tím, co mohou uživatelé použít. + +**Blogy a dokumentace** Bohatá syntaxe pro tabulky, obrázky s popiskami, typografii a kód se syntax highlightingem. + +**Komentáře a diskuzní fóra** SafeMode zajistí, že uživatelé nemohou vložit nebezpečný kód, ale zároveň mají k dispozici formátování textu. + +**Projekty s vlastními požadavky** Potřebujete embed YouTube videí? Speciální syntax pro vaše makra? Vlastní markup jazyk? S Texy to vytvoříte snadno. + + +Historie +======== + +Texy vytvořil David Grudl před **více než 20 lety** v roce 2004 jako jeden z prvních markup procesorů pro PHP. Původně bylo vyvinuto pro **PHP 4**, ale během své dlouhé historie prošlo mnoha aktualizacemi a dnes plně využívá všech možností **PHP 8**. + +Dvě dekády aktivního vývoje znamenají **vyzkoušenou a stabilní** knihovnu, které důvěřují stovky projektů. Texy je dnes **vyzrálé řešení** s velkou historií, ale stále aktivně udržované a moderní. -- Srovnání [Texy versus WYSIWYG editory | texy-vs-wysiwyg] -- [Příklady využití | priklady-vyuziti] -- [Základy syntaxe | syntax] {{maintitle: Texy – formátovač textů pro PHP}} diff --git a/texy/cs/@menu.texy b/texy/cs/@menu.texy index eb7458d858..bc703bedfb 100644 --- a/texy/cs/@menu.texy +++ b/texy/cs/@menu.texy @@ -1,8 +1,7 @@ -- [úvodní stránka | @home] -- [syntax stručně | syntax] -- [syntax podrobně | syntax-podrobne] -- [fiddle | https://fiddle.nette.org/texy/] -- [manuál | api] -- [blog | https://phpfashion.com/category/texy] -- [API | https://api.nette.org/texy/] -- [GitHub | https://github.com/dg/texy] +- [úvod | @home] +- [syntaxe | syntax] +- [pro programátory | develop] +- "blog .[link-external]":https://phpfashion.com/category/texy +- "hřiště .[link-external]":https://fiddle.nette.org/texy/ +- "API .[link-external]":https://api.nette.org/texy/ +- "GitHub .[link-external]":https://github.com/dg/texy diff --git a/texy/cs/@try.texy b/texy/cs/@try.texy deleted file mode 100644 index 9c443df131..0000000000 --- a/texy/cs/@try.texy +++ /dev/null @@ -1,15 +0,0 @@ -Vítejte! --------- - -Můžete používat syntax Texy!, pokud Vám vyhovuje: -- třeba **tučné** písmo nebo *kurzíva* -- a takto se dělá "odkaz":https://texy.info -- více najdete na stránce "syntax":[syntax] - - -Ale také můžete zůstat u HTML: -- takto <b>HTML</b> -- nebo i <b class=xx>úplně <i>hloupě</b>, Texy! to pořeší - - -[syntax]: /cs/syntax diff --git a/texy/cs/api-block-module.texy b/texy/cs/api-block-module.texy deleted file mode 100644 index aae50f9b19..0000000000 --- a/texy/cs/api-block-module.texy +++ /dev/null @@ -1,11 +0,0 @@ -Třída Texy\Modules\BlockModule -****************************** - -Má na starosti zpracování bloků `/-- xxx`. Modul vypneme zakázáním syntaxe `blocks`: - -/--code php -$texy->allowed['blocks'] = false; - -// nebo pro jednotlivé typy bloků -$texy->allowed['block/code'] = false; -\-- diff --git a/texy/cs/api-blockquote-module.texy b/texy/cs/api-blockquote-module.texy deleted file mode 100644 index b9fd28e72a..0000000000 --- a/texy/cs/api-blockquote-module.texy +++ /dev/null @@ -1,8 +0,0 @@ -Třída Texy\Modules\BlockQuoteModule -*********************************** - -Má na starosti blokové citace. Modul vypneme zakázáním syntaxe `blockquote`: - -/--code php -$texy->allowed['blockquote'] = false; -\-- diff --git a/texy/cs/api-emoticon-module.texy b/texy/cs/api-emoticon-module.texy deleted file mode 100644 index 8f0f8fa475..0000000000 --- a/texy/cs/api-emoticon-module.texy +++ /dev/null @@ -1,47 +0,0 @@ -Třída Texy\Modules\EmoticonModule -********************************* - -Má na starosti nahrazování smajlíků za obrázky. Ve výchozím nastavení je modul vypnutý, zapneme jej povolením syntaxe `emoticon`: - -/--code php -$texy->allowed['emoticon'] = true; -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $icons | array | | tabulka všech smajlíků a odpovídajících obrázků -| $class | string | null | CSS třída -| $root | string | | kořenový adresář obrázků na webu -| $fileRoot | string | | kořenový adresář obrázků na disku - -Pokud nenastavíte hodnoty pro `$root` a `$fileRoot`, použijí se ty z modulu [Texy\Modules\ImageModule|api-image-module]. Výchozí seznam smajlíků je tento: - -/--code php -$icons = [ - ':-)' => 'smile.gif', - ':-(' => 'sad.gif', - ';-)' => 'wink.gif', - ':-D' => 'biggrin.gif', - '8-O' => 'eek.gif', - '8-)' => 'cool.gif', - ':-?' => 'confused.gif', - ':-x' => 'mad.gif', - ':-P' => 'razz.gif', - ':-|' => 'neutral.gif', -]; -\-- - -Můžete jej pozměnit přímo zásahem do pole: - -/--code php -$texy->emoticonModule->icons[':-)'] = 'smile.png'; - -unset($texy->emoticonModule->icons[':-P']); -\-- - -Poslední znak smajlíku se může opakovat, tedy klíč. `:-)` akceptuje i smajlík v podobě `:-)))`. diff --git a/texy/cs/api-figure-module.texy b/texy/cs/api-figure-module.texy deleted file mode 100644 index db0d483886..0000000000 --- a/texy/cs/api-figure-module.texy +++ /dev/null @@ -1,33 +0,0 @@ -Třída Texy\Modules\FigureModule -******************************* - -Má na starosti obrázky s popiskou. Module vypneme zakázáním syntaxe `figure`: - -/--code php -$texy->allowed['figure'] = false; -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $class | string | `'figure'` | třída neplovoucího kontejneru `<div>` -| $leftClass | string | null | třída `<div>` plovoucího vlevo -| $rightClass | string | null | třída `<div>` plovoucího vpravo -| $widthDelta | int | 10 | offset pro výpočet šířky - - -Neplovoucím kontejnerům `<div>` bude přiřazena třída `$class`, plovoucím `$leftClass` nebo `$rightClass`. Pokud není specifikována třída u plovoucích kontejnerů, Texy ji sestaví takto: - -`$texy->figureModule->class . '-' . $texy->alignClasses['left'] resp. 'right'` - -Není-li nastavená třída ani v poli `$alignClasses`, Texy kontejner zarovnává CSS vlastností `float`. - -U plovoucích kontejnerů Texy počítá a nastavuje šířků podle vzorce: - -`šířka <div> = šířka obrázku <img> + $widthDelta` - -Je tedy nutné zjistit šířku obrázku. K tomu je potřeba korektně nastavit cestu `$texy->imageModule->fileRoot`, viz [Texy\Modules\ImageModule | api-image-module]. diff --git a/texy/cs/api-heading-module.texy b/texy/cs/api-heading-module.texy deleted file mode 100644 index 701de25493..0000000000 --- a/texy/cs/api-heading-module.texy +++ /dev/null @@ -1,42 +0,0 @@ -Třída Texy\Modules\HeadingModule -******************************** - -Má na starosti nadpisy a titulky. Module (de)aktivujeme zakázáním syntaxe: - -/--code php -// podtržené titulky -$texy->allowed['heading/underlined'] = false; - -// ohraničené titulky -$texy->allowed['heading/surrounded'] = false; -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $title | string | null | nejvyšší titulek -| $TOC | array | | zde se vygeneruje obsah -| $generateID | boolean| false | generovat titulkům ID? -| $idPrefix | string | `'toc-'` | prefix pro generované ID -| $top | int | 1 | strop, úroveň nejvyššího titulku -| $moreMeansHigher | boolean | true | více znaků znamená vyšší titulek -| $balancing | int | *dynamic* | způsob vážení titulků -| $levels | array | | tabulka vážení titulků - -Po zpracování textu bude první titulek uložen do proměnné $title (bez HTML kódování, tedy vhodné pro použití v `<title>`). - -Způsob vážení titulků určuje $balancing, které může nabývat hodnot `Texy\Modules\HeadingModule::DYNAMIC` (výchozí) nebo `Texy\Modules\HeadingModule::FIXED`. Více informací [ve fóru | https://forum.texy.info/cs/viewtopic.php?pid=22]. - - -Příklady --------- - -Nejvyšší titulek bude mít úroveň `<h2>` - -/--code php -$texy->headingModule->top = 2; -\-- diff --git a/texy/cs/api-horizline-module.texy b/texy/cs/api-horizline-module.texy deleted file mode 100644 index ebbd47da44..0000000000 --- a/texy/cs/api-horizline-module.texy +++ /dev/null @@ -1,8 +0,0 @@ -Třída Texy\Modules\HorizLineModule -********************************** - -Má na starosti horizontální čáry. Modul vypneme zakázáním syntaxe `horizline`: - -/--code php -$texy->allowed['horizline'] = false; -\-- diff --git a/texy/cs/api-html-module.texy b/texy/cs/api-html-module.texy deleted file mode 100644 index f925f2ade8..0000000000 --- a/texy/cs/api-html-module.texy +++ /dev/null @@ -1,21 +0,0 @@ -Třída Texy\Modules\HtmlModule -***************************** - -Má na starosti HTML značky a komentáře na vstupu. Modul vypneme zakázáním syntaxe: - -/--code php -// vypneme HTML značky -$texy->allowed['html/tag'] = false; - -// vypneme HTML komentáře -$texy->allowed['html/comment'] = false; -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $passComment | boolean | true | zobrazit HTML komentáře na výstupu? diff --git a/texy/cs/api-htmloutput-module.texy b/texy/cs/api-htmloutput-module.texy deleted file mode 100644 index ec049261fa..0000000000 --- a/texy/cs/api-htmloutput-module.texy +++ /dev/null @@ -1,17 +0,0 @@ -Třída Texy\Modules\HtmlOutputModule -*********************************** - -Má na starosti formátování výstupního XHTML / HTML. Formátuje výsledné HTML a zároveň hlídá a koriguje "wellformed" zápis. - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $indent | boolean | true | formátovat výstup do úhledné podoby? -| $baseIndent | int | 0 | minimální odsazení každého řádku -| $lineWrap | int | 80 | maximální šířka řádku - -Volitelné koncové značky se odstraňují pouze v režimu HTML. Zalamování řádků lze vypnout nastavením `$texy->htmlOutputModule->lineWrap = false`. diff --git a/texy/cs/api-image-module.texy b/texy/cs/api-image-module.texy deleted file mode 100644 index bebde249b3..0000000000 --- a/texy/cs/api-image-module.texy +++ /dev/null @@ -1,35 +0,0 @@ -Třída Texy\Modules\ImageModule -****************************** - -Má na starosti obrázky. Tzv. obrázky s popiskou zpracovává [Texy\Modules\FigureModule|api-figure-module]. Modul vypneme zakázáním syntaxe: - -/--code php -// zákaz obrázků -$texy->allowed['image'] = false; - -// zákaz referencí -$texy->allowed['image/definition'] = false; -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $root | string | `'images/'` | kořenový adresář obrázků na webu -| $linkedRoot | string | `'images/'` | kořenový adresář obrázků v pozici odkazu -| $fileRoot | string | | kořenový adresář obrázků na disku -| $leftClass | string | null | třída obrázku plovoucího vlevo -| $rightClass | string | null | třída obrázku plovoucího vpravo -| $defaultAlt | string | `''` | výchozí hodnota atributu `alt` - - -Fyzická cesta k obrázkům `$fileRoot` se používá ke zjištění jich rozměrů, které se pak uvedou v HTML výstupu. Modul se cestu pokusí detekovat z prostředí serveru, nicméně je vhodnější ji nastavit manuálně. - -Proměnná `$linkedRoot` určuje kořenový adresář obrázků, které jsou použity jako cíl odkazu, tedy `[odkaz | * img.jpg *]`. - -Plovoucím obrázkům je nastavena třída `$leftClass` resp. `$rightClass`, nebo `$texy->alignClasses['left']` resp. `'right'`. Pokud není ani jedno specifikováno, Texy obrázky zarovnává CSS vlastností `float`. - -Protože Texy umí překlápět obrázky při přejetí myškou (onmouseover efekt), je nutné zajistit včasné načtení těchto obrázků (preload). K tomu slouží děsně fikaný skript, který se aktivuje událostí `onload` u jednotlivých obrázků. Skript je uložen v proměnné `$onLoad`. diff --git a/texy/cs/api-link-module.texy b/texy/cs/api-link-module.texy deleted file mode 100644 index f9fc26e49a..0000000000 --- a/texy/cs/api-link-module.texy +++ /dev/null @@ -1,42 +0,0 @@ -Třída Texy\Modules\LinkModule -***************************** - -Má na starosti definice, reference, odkazy. Modul vypneme zakázáním syntaxe: - -/--code php -// vypnout zpracování definicí [ref]: www.texy.info -$texy->allowed['link/definition'] = false; - -// vypnout zpracování referencí [ref] -$texy->allowed['link/reference'] = false; - -// vypnout dělání www adres klikatelnými -$texy->allowed['link/url'] = false; - -// vypnout dělání emailových adres klikatelnými -$texy->allowed['link/email'] = false; -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $root | string | `''` | kořenový adresář relativních odkazů -| $imageClass | string | | třída pro odkazy vedoucí na obrázky (od Texy 2.2) -| $forceNoFollow | boolean | false | doplňovat `rel="nofollow"`? -| $shorten | boolean | true | zkracovat URL? - -Texy automaticky doplňuje atribut nofollow odkazům, které mají pseudotřídu `nofollow`, např. `"odkaz .[nofollow]":www.texy.info`. - - -Příklad -------- - -Chceme přidávat nofollow ke všem odkazům - -/--code php -$texy->linkModule->forceNoFollow = true; -\-- diff --git a/texy/cs/api-list-module.texy b/texy/cs/api-list-module.texy deleted file mode 100644 index 222f9d94e4..0000000000 --- a/texy/cs/api-list-module.texy +++ /dev/null @@ -1,12 +0,0 @@ -Třída Texy\Modules\ListModule -***************************** - -Má na starosti číslované, nečíslované a definiční seznamy. Modul vypneme zakázáním syntaxe: - -/--code php -// číslované a nečíslované seznamy -$texy->allowed['list'] = false; - -// definiční listy -$texy->allowed['list/definition'] = false; -\-- diff --git a/texy/cs/api-longwords-module.texy b/texy/cs/api-longwords-module.texy deleted file mode 100644 index 00c5ca765a..0000000000 --- a/texy/cs/api-longwords-module.texy +++ /dev/null @@ -1,19 +0,0 @@ -Třída Texy\Modules\LongWordsModule -********************************** - -Má na starosti rozdělení dlouhých slov. Modul vypneme zakázáním syntaxe `longwords`: - -/--code php -$texy->allowed['longwords'] = false; -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $wordLimit | int | 20 | maximální délka slova - -Texy vkládá do příliš dlouhých slov (nad $wordLimit) značku volitelného zalomení `­`. Snaží se přitom respektovat specifika dělení slov na slabiky. diff --git a/texy/cs/api-paragraph-module.texy b/texy/cs/api-paragraph-module.texy deleted file mode 100644 index 28b10e479e..0000000000 --- a/texy/cs/api-paragraph-module.texy +++ /dev/null @@ -1,4 +0,0 @@ -Třída Texy\Modules\ParagraphModule -********************************** - -Má na starosti jednotlivé odstavce textu. diff --git a/texy/cs/api-phrase-module.texy b/texy/cs/api-phrase-module.texy deleted file mode 100644 index c978f68adc..0000000000 --- a/texy/cs/api-phrase-module.texy +++ /dev/null @@ -1,22 +0,0 @@ -Třída Texy\Modules\PhraseModule -******************************* - -Má na starosti tzv. fráze, tedy úseky textu (tučný text, odkaz, ...). Můžeme vypínat a zapínat jednotlivé syntaxe: - -/--code php -$texy->allowed['phrase/strong'] = false; -... -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $tags | array | | HTML značky pro jednotlivé fráze -| $linksAllowed | boolean | true | je možné k frázi přidat odkaz? - - -Pod pojmem fráze se rozumí jakákoliv řádková syntax jako `**tučné**`, `//kurzíva//`, `subskript_2`, `[odkaz | www.texy.info`]. diff --git a/texy/cs/api-script-module.texy b/texy/cs/api-script-module.texy deleted file mode 100644 index 0e3ace4d82..0000000000 --- a/texy/cs/api-script-module.texy +++ /dev/null @@ -1,23 +0,0 @@ -Třída Texy\Modules\ScriptModule -******************************* - -Má na starosti volání uživatelských funkcí a vkládání externích dat. Modul vypneme zakázáním syntaxe `script`: - -/--code php -$texy->allowed['script'] = false; -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $separator | string | `';'` | oddělovat argumentů - - -Příklady --------- - -Najdete ve [fóru | https://forum.texy.info/cs/viewtopic.php?pid=1954]. diff --git a/texy/cs/api-table-module.texy b/texy/cs/api-table-module.texy deleted file mode 100644 index 6114f24229..0000000000 --- a/texy/cs/api-table-module.texy +++ /dev/null @@ -1,18 +0,0 @@ -Třída Texy\Modules\TableModule -****************************** - -Má na starosti tabulky. Modul vypneme zakázáním syntaxe `table`: - -/--code php -$texy->allowed['table'] = false; -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $oddClass | string | null | CSS třída přiřazená lichým řádkům tabulky -| $evenClass | string | null | CSS třída přiřazená sudým řádkům tabulky diff --git a/texy/cs/api-texy.texy b/texy/cs/api-texy.texy deleted file mode 100644 index 6eba43a35c..0000000000 --- a/texy/cs/api-texy.texy +++ /dev/null @@ -1,69 +0,0 @@ -Třída Texy\Texy -*************** - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| `$allowed` | mixed | | povolené "Texy syntaxe" -| `$allowedTags` | mixed | *validní značky* | povolené HTML značky -| `$allowedClasses` | mixed | Texy::ALL | povolené CSS třídy a identifikátory -| `$allowedStyles` | mixed | Texy::ALL | povolené CSS styly -| `$alignClasses` | array | *vynulované* | CSS třídy pro zarovnání textu a obrázků -| `$mergeLines` | boolean | true | spojovat řádky? -| `$tabWidth` | int | 8 | šířka tabulátorů kvůli převední na mezery -| `$obfuscateEmail` | boolean| true | maskovat emailové adresy před roboty? -| `$urlSchemeFilters` | array| null | - - -Proměnné `$allowedTags`, `$allowedClasses` a `$allowedStyles` mohou nabývat hodnot: - -- `Texy::ALL` - jsou povoleny všechny HTML značky resp. styly resp. třídy -- `Texy::NONE` - naopak jsou všechny zakázány -- *array* - výčet povolených hodnot - - `$allowedTags` - povolené značky tvoří klíče, viz příklady níže - - `$allowedClasses` - seznam tříd a ID, přičemž ID začínají prefixem # - - `$allowedStyles` - seznam CSS vlastností - -Pole `$alignClasses` definuje CSS třídy pro zarovnání textu a obrázků. Blíže popsáno [ve fóru | https://forum.texy.info/cs/viewtopic.php?id=532]. - -Texy spojuje řádky následující za sebou do odstavců. Toto chování je možné vypnout nastavením vlastnosti `$texy->mergeLines = false`. - - -Příklady --------- - -Povolíme pouze HTML elementy `<strong>, <div>, <a>` a některé jejich atributy: - -/--code php -$texy->allowedTags = [ - 'strong' => Texy::NONE, // <strong> nesmí mít žádné attributy - 'div' => Texy::ALL, // <div> může mít jakékoliv atributy - 'a' => ['href', 'lang', 'target'], // <a> může mít jen tyto atributy -]; -\-- - -Všechny HTML značky zakážeme: - -/--code php -$texy->allowedTags = Texy::NONE; -\-- - -Povolíme pouze CSS třídy `class1, class2` a CSS identifikátory `id1, id2` - -/--code php -$texy->allowedClasses = ['class1', 'class2', '#id1', '#id2']; -\-- - -Povolíme pouze CSS vlastnosti `font-size, color, width` - -/--code php -$texy->allowedStyles = ['font-size', 'color', 'width']; -\-- - -Místo přímých stylů `style="text-align:left"` apod. používej třídy `class="left"` apod. - -/--code php -$texy->alignClasses['left'] = 'left'; -$texy->alignClasses['right'] = 'right'; -... // dále možno definovat: center, justify, top, bottom, middle -\-- diff --git a/texy/cs/api-typography-module.texy b/texy/cs/api-typography-module.texy deleted file mode 100644 index a66748609f..0000000000 --- a/texy/cs/api-typography-module.texy +++ /dev/null @@ -1,21 +0,0 @@ -Třída Texy\Modules\TypographyModule -*********************************** - -Má na starosti typografické úpravy výsledného textu. Modul vypneme zakázáním syntaxe `typography`: - -/--code php -$texy->allowed['typography'] = false; -\-- - - -Konfigurace ------------ - -|---------------- -| proměnná | typ | výchozí | popis | -|---------------- -| $locale | string | `'cs'` | určení národních specifik -| static $locales | array | | předvolená specifika - - -Vlastnost $locale může nabývat hodnot: cs, en, fr, de, pl. Další specifika lze doplnit do statické proměnné $locales. diff --git a/texy/cs/api-zaklady.texy b/texy/cs/api-zaklady.texy deleted file mode 100644 index 3672e3ba65..0000000000 --- a/texy/cs/api-zaklady.texy +++ /dev/null @@ -1,25 +0,0 @@ -Základní dovednosti -******************* - -Jak přeformátovat text do HTML? Stačí do kódu začlenit knihovnu Texy, například pomocí Composeru: - -/-- -composer require texy/texy -\-- - -A vytvořit objekt `$texy = new Texy\Texy;`. Celý převod obstará metoda `$html = $texy->process($text)`, kde proměnná `$text` obsahuje vstupní text a vrací se zformátovaný HTML výstup. - -/--php -// vytvoříme objekt -$texy = new Texy\Texy; - -// můžeme jej nakonfigurovat -$texy->imageModule->root = 'images/'; - -// a zpracujeme vstupní $text -$html = $texy->process($text); -\-- - -Pokud potřebujete formátovat jednořádkový text (tedy bez blokových elementů), použijte `$html = $texy->processLine($text)`. - -V ukázce vidíte, jak je možné objekt `$texy` konfigurovat. Protože parametrů je hodně, jsou rozčleněny do logických celků označovaných jako moduly. Instance každého modulu je uložena v atributu `$texy->blockModule`, `$texy->emoticonModule` atd. (viz [jednotlivé moduly | api#moduly]). diff --git a/texy/cs/api.texy b/texy/cs/api.texy deleted file mode 100644 index e0d5c9e4db..0000000000 --- a/texy/cs/api.texy +++ /dev/null @@ -1,28 +0,0 @@ -Programátorský manuál -********************* - -Texy je napsané v objektovém PHP. Minimální požadovaná verze pro Texy 3.0 je PHP 7.1. - ---> [Základní dovednosti | api-zaklady] - ---> [API dokumentace | https://api.nette.org/texy/] - ---> Jednotlivé moduly: .[#moduly] -- [Texy\Texy | api-texy] - jádro Texy -- [Texy\Modules\BlockModule | api-block-module] - zpracování bloků `/-- xxx` -- [Texy\Modules\BlockQuoteModule | api-blockquote-module] - blokové citace -- [Texy\Modules\EmoticonModule | api-emoticon-module] - nahrazování smajlíků za obrázky -- [Texy\Modules\FigureModule | api-figure-module] - obrázky s popiskou -- [Texy\Modules\HeadingModule | api-heading-module] - nadpisy, titulky -- [Texy\Modules\HorizLineModule | api-horizline-module] - horizontální čáry -- [Texy\Modules\HtmlModule | api-html-module] - HTML značky a komentáře na vstupu -- [Texy\Modules\HtmlOutputModule | api-htmloutput-module] - formátování výstupního HTML -- [Texy\Modules\ImageModule | api-image-module] - obrázky -- [Texy\Modules\LinkModule | api-link-module] - odkazy a reference -- [Texy\Modules\ListModule | api-list-module] - číslované, nečíslované a definiční seznamy -- [Texy\Modules\LongWordsModule | api-longwords-module] - rozdělení dlouhých slov -- [Texy\Modules\ParagraphModule | api-paragraph-module] - jednotlivé odstavce textu -- [Texy\Modules\PhraseModule | api-phrase-module] - fráze, tedy úseky textu (tučný text, odkaz, ...) -- [Texy\Modules\ScriptModule | api-script-module] - volání uživatelských funkcí -- [Texy\Modules\TableModule | api-table-module] - tabulky -- [Texy\Modules\TypographyModule | api-typography-module] - typografické úpravy výsledného textu diff --git a/texy/cs/architecture.texy b/texy/cs/architecture.texy new file mode 100644 index 0000000000..b86ea90545 --- /dev/null +++ b/texy/cs/architecture.texy @@ -0,0 +1,425 @@ +Architektura a principy +####################### + +.[perex] +Texy je nástroj pro převod textu napsaného ve vlastním markup jazyce do HTML. Na rozdíl od jednoduchých převodníků, které text zpracovávají lineárně pomocí série náhrad, používá Texy sofistikovaný systém založený na parsování, modulární architektuře a postupném budování DOM stromu. + +Základní tok zpracování probíhá ve čtyřech hlavních fázích: + +1. Předzpracování textu - normalizace, úprava mezer a tabulátorů, volání notification handlerů pro přípravu +2. Parsování - rozpoznání syntaxí pomocí regulárních výrazů a postupné budování DOM stromu +3. Post-processing - typografické úpravy, zpracování dlouhých slov, wellforming HTML +4. Finální sestavení - konverze DOM stromu do HTML řetězce + +Klíčovým rozdílem oproti naivním přístupům je oddělení fáze rozpoznávání syntaxí od jejich zpracování. Parser nejprve identifikuje, kde se v textu nachází která syntaktická konstrukce, a teprve poté předává nalezené části jednotlivým modulům ke zpracování. To umožňuje vnořování syntaxí a jejich postupné rozbalování. + +*Poznámka: všechny třídy se nacházejí ve jmenném prostoru `Texy`, takže pokud dokument zmiňuje například třídu `HtmlElement`, její plný název je `Texy\HtmlElement`. Moduly jsou ve jmenném prostoru `Texy\Modules`* + + +Klíčové komponenty +================== + +Architektura Texy se skládá z několika hlavních komponent, z nichž každá má jasně vymezenou zodpovědnost: + +Třída Texy funguje jako centrální orchestrátor celého systému. Obsahuje odkazy na všechny moduly, spravuje registrované syntaxe a handlery, udržuje stav zpracování a koordinuje jednotlivé fáze konverze. Je to jediné místo, kde se jednotlivé komponenty propojují. + +**[#Moduly]** představují funkční jednotky zodpovědné za konkrétní oblasti markup jazyka. Každý modul při své konstrukci registruje syntaxe, které rozpoznává, a element handlery, které je zpracovávají. Například PhraseModule se stará o inline formátování jako tučný nebo kurzívou psaný text, zatímco TableModule zpracovává tabulky. Moduly jsou navrženy jako samostatné, znovupoužitelné jednotky s vlastní konfigurací přístupnou přes veřejné properties. + +**[#Parsery]** existují ve dvou variantách podle typu zpracovávaného obsahu. BlockParser zpracovává blokové struktury jako odstavce, nadpisy, seznamy nebo tabulky. Prochází text po řádcích, hledá začátky blokových konstrukcí a předává je *syntax handlerům*. LineParser se stará o inline syntaxe uvnitř řádků - odkazy, obrázky, formátování textu. Na rozdíl od BlockParser umožňuje vnořování syntaxí a jejich postupné rozbalování. + + +Základní terminologie +===================== + +Pro správné pochopení fungování Texy je nutné rozlišovat několik klíčových pojmů, které se v dokumentaci často objevují. + +**Syntax** označuje pojmenovanou syntaktickou konstrukci markup jazyka. Každá syntax má jedinečný název, například `phrase/strong` pro tučný text nebo `image` pro obrázky. Název syntaxe se používá pro její zapnutí či vypnutí v poli `Texy::$allowed` a předává se jako parametr do syntax handlerů pro rozlišení, která konkrétní syntax byla nalezena. + +**Pattern** je regulární výraz, který definuje, jak syntax vypadá v textu. Pattern je implementační detail syntaxe - autor syntaxe musí napsat regex, který ji rozpozná, ale z pohledu uživatele Texy je podstatnější název syntaxe a její význam. Jeden modul typicky registruje více syntaxí s různými patterny. + +**Syntax handler** je funkce volaná parserem ve chvíli, kdy najde výskyt syntaxe v textu. Dostává nalezený text a vrací `HtmlElement` nebo řetězec, který se vloží na původní místo. Syntax handler je místem, kde se rozhoduje, co se s nalezenou syntaxí stane - typicky vyvolá element handler pro vlastní zpracování. + +**Element** je prvek, pro který se generuje HTML reprezentace. Například `image` je element pro obrázky, `linkURL` pro odkazy, `phrase` pro inline formátování. Každý element má svůj výchozí element handler, který se stará o standardní zpracování. + +**Element handler** je funkce registrovaná pro určitý typ prvků a volaná přes systém HandlerInvocation. Charakteristické je použití metody `proceed()`, která umožňuje delegovat zpracování na další handler v řetězu nebo na výchozí handler modulu. Element handlery slouží k modifikaci nebo nahrazení výchozího chování. + +**Notification handler** je funkce volaná pro notifikaci o určité události. Na rozdíl od element handlerů nevrací žádnou hodnotu a nemůže ovlivnit výsledek zpracování. Používá se pro přípravu dat, logování nebo modifikace již vytvořeného DOM stromu. + +Rozdíl mezi jednotlivými handlery je klíčový pro pochopení architektury. Syntax handler je těsně svázán s parserem a konkrétním patternem - řeší otázku *co dělat, když parser najde tento pattern*. Element handlery jsou na vyšší úrovni abstrakce - řeší otázku *jak zpracovat tento typ prvku*, bez ohledu na to, která konkrétní syntax ji vytvořila. + + +Celkový tok zpracování +====================== + +Když Texy dostane vstupní text, projde následujícím procesem zpracování. + +V předzpracování dochází k normalizaci textu. Koncové značky řádků se sjednotí na Unix formát, mezery se standardizují a tabulátory se případně nahradí mezerami. Následně se vyvolají *notification handlery* registrované pro událost `beforeParse`. Tyto handlery mohou provést přípravu dat, například načíst definice referencí nebo upravit konfiguraci podle obsahu textu. + +Samotné parsování začíná vytvořením kořenového `HtmlElement`, který reprezentuje dokument. Texy pak rozhodne, zda text zpracovat jako jeden řádek nebo jako kompletní dokument s blokovými strukturami. V případě blokového zpracování se vytvoří BlockParser, který postupně prochází text a hledá jednotlivé blokové konstrukce. + +LineParser pracuje jinak než BlockParser. Neprochází text lineárně, ale postupně hledá nejbližší výskyt jakékoliv registrované syntaxe. Když nějakou najde, zavolá příslušný syntax handler, který vytvoří odpovídající HTML element. Tento element se pomocí speciálního maskování vloží zpět do textu a parser pokračuje dál. Díky tomu může najít a zpracovat syntaxe vnořené uvnitř již zpracovaných konstrukcí. + +Po dokončení parsování vznikne kompletní DOM strom reprezentující strukturu dokumentu. Texy vyvolá notification handlery pro událost `afterParse`, které mohou provést závěrečné úpravy stromu, například doplnit identifikátory nadpisů nebo sestavit obsah. + +Post-processing probíhá během konverze DOM stromu na HTML řetězec. Každý element se rekurzivně převádí na HTML kód, přičemž se aplikují typografické úpravy jako nahrazení uvozovek, pomlček nebo vkládání nezlomitelných mezer. Dále se provádí wellforming HTML - automatické uzavírání tagů, oprava špatně vnořených elementů, formátování a odsazování kódu. + +Finální fází je dekódování všech maskovaných částí zpět na HTML tagy, odstranění pomocných značek a sestavení výsledného HTML řetězce. + + +Systém syntaxí +************** + +Syntax v terminologii Texy představuje pojmenovanou syntaktickou konstrukci markup jazyka. Je to abstraktní koncept spojující několik prvků: unikátní název, regulární výraz pro rozpoznání a způsob zpracování. Název syntaxe slouží jako identifikátor v celém systému - používá se v poli `Texy::$allowed` pro zapnutí či vypnutí, předává se do handlerů pro rozlišení typu konstrukce a objevuje se v dokumentaci a konfiguračních souborech. + +Jmenné konvence syntaxí následují dva hlavní vzory. Jednodušší syntaxe mají jednoslovný název odpovídající jejich účelu, například `image`, `table` nebo `script`. Složitější oblasti používají hierarchické pojmenování se lomítkem, například `phrase/strong`, `phrase/em` nebo `link/reference`. Lomítko slouží k logickému seskupení souvisejících syntaxí a usnadňuje hromadné operace s nimi. + + +Line syntaxe +============ + +Line syntaxe slouží k rozpoznávání inline prvků uvnitř řádků textu. Typicky jde o formátování jako tučný nebo kurzívou psaný text, odkazy, obrázky nebo inline kód. Charakteristické pro line syntaxe je, že mohou být vnořené do sebe a parser je postupně rozbaluje. + +Registrace line syntaxe probíhá voláním `Texy::registerLinePattern()` s několika parametry. První je syntax handler, tedy callback volaný při nálezu. Druhý parametr je regulární výraz definující podobu syntaxe v textu. Třetí parametr je název syntaxe používaný v celém systému. Volitelný čtvrtý parametr je další regex pro test, zda má smysl pattern vůbec hledat - používá se pro optimalizaci, aby se nespouštěl komplexní pattern na textu, kterému určitě nemůže odpovídat. + +Pattern jako regulární výraz musí splňovat určitá pravidla. Nesmí být kotvený na začátek textu, protože se hledá kdekoliv v řádku. Měl by být co nejkonkrétnější, aby nedocházelo k falešným nálezům. + +Inline syntaxe uvnitř řádků textu zpracovává [#LineParser]. Když najde match, zavolá příslušný syntax handler. Ten dostává tři parametry. První je instance LineParser, která poskytuje přístup k Texy objektu a dalším informacím o kontextu. Druhý parametr je pole s výsledky regex matche včetně podvýrazů. Třetí parametr je název syntaxe, který je užitečný, když stejný callback obsluhuje více syntaxí. Handler musí vrátit buď `HtmlElement`, nebo řetězec, nebo null, pokud zpracování odmítá. + + +Block syntaxe +============= + +Block syntaxe rozpoznávají víceřádkové blokové konstrukce jako nadpisy, seznamy, tabulky, citace nebo speciální bloky. Na rozdíl od line syntaxí se block syntaxe nikdy nepřekrývají - každý řádek textu patří maximálně do jedné blokové konstrukce. + +Registrace block syntaxe používá `Texy::registerBlockPattern()` se třemi parametry: syntax handler, regulární výraz a název syntaxe. Pattern jako regulární výraz musí splňovat určitá pravidla. Musí odpovídat od začátku řádku a často obsahuje kotvu pro konec řádku. BlockParser automaticky přidává modifikátor m (multiline), takže pattern by ho neměl obsahovat. + +Block syntaxe uvnitř dokumentu zpracovává [#BlockParser]. Když najde match, zavolá příslušný syntax handler. Ten dostává podobné parametry jako u line syntaxí - BlockParser instanci, pole s matchem a název syntaxe. Vrací `HtmlElement` reprezentující celý zpracovaný blok, nebo null při odmítnutí zpracování. + + +Zapnutí a vypnutí syntaxe +========================= + +Pole `Texy::$allowed` poskytuje jemnou kontrolu nad tím, které syntaxe jsou v Texy aktivní. Je to jednoduchý, ale mocný mechanismus pro konfiguraci chování bez nutnosti měnit kód modulů. Když zakážete syntaxi `phrase/strong` tímto nastavením, parser přestane hledat konstrukci tučného textu: + +```php +$texy->allowed['phrase/strong'] = false; +``` + +Kontrola probíhá jednou při začátku parsování, takže dynamická změna `$allowed` během zpracování nemá efekt. + +Při konstrukci modulů se pro většinu syntaxí nastavuje výchozí hodnota `$allowed`. Některé syntaxe jsou ve výchozím stavu zapnuté, protože tvoří základ markup jazyka. Jiné jsou vypnuté, protože jsou pokročilé nebo potenciálně nebezpečné. Například emotikony jsou vypnuté, protože ne každý dokument je potřebuje, zatímco základní formátování je zapnuté. + +Bezpečný režim je situace, kdy zpracováváte nedůvěryhodný vstup, například komentáře od uživatelů. Chcete povolit základní formátování, ale zakázat obrázky, skripty nebo HTML tagy. `Texy\Configurator::safeMode()` nastaví `$allowed` pro bezpečnou kombinaci syntaxí. Zakáže obrázky, definice referencí a HTML komentáře, omezí HTML tagy na bezpečnou podmnožinu a vynutí `rel="nofollow"`, ale ponechá odkazy a základní formátování. + + +Parsery +******* + + +Syntax handler +============== + +Jak jsme si říkali v předchozí části, LineParser nebo BlockParser prochází text a hledá všechny registrované patterny. Když najde match, zavolá příslušný syntax handler a předá mu informace o nálezu - zejména pole s výsledky regex matche. + +Syntax handler analyzuje nalezený text a připravuje data pro zpracování. Může extrahovat části textu z regex skupin, vytvořit pomocné objekty jako `Link` nebo `Image`, parsovat modifikátory. Rozhoduje také, jaký element handler vyvolat. Zavolá `Texy::invokeAroundHandlers()` s názvem elementu a připravenými parametry. Tím začne jejich vykonávání. Vrácený výsledek se dostává zpět do syntax handleru, který ho vrátí parseru. + + +Element handler +=============== + +Element handlery implementují vzor chain of responsibility, který umožňuje složit výsledné chování z více vrstev. + +Registrace element handleru probíhá voláním `Texy::addHandler()` se dvěma parametry - názvem elementu a funkcí handleru. Jeden název elementu může mít zaregistrováno více handlerů, které se pak vykonávají v pořadí od posledně registrovaného k prvnímu. + +Název elementu identifikuje, o jaký typ zpracování jde, například `phrase` pro formátování, `image` pro obrázky nebo `link` pro odkazy (pozor: jde o něco jiného než názvy syntaxe). Někdy se používají složené názvy jako `linkReference` nebo `linkEmail` pro rozlišení různých druhů odkazů. Názvy jsou obecnější než názvy syntaxí - zatímco syntaxe `phrase/strong` je specifická konstrukce, element `phrase` pokrývá všechny druhy inline formátování. + +Vyvolání element handleru používá metodu `Texy::invokeAroundHandlers()`. Tato metoda dostává název prvku, instanci parseru a pole parametrů. Vytvoří HandlerInvocation objekt, který zapouzdřuje celý řetěz zaregistrovaných handlerů. První handler v řetězu dostane kontrolu a rozhoduje, zda zavolat `HandlerInvocation::proceed()` pro pokračování na další handler, nebo vrátit vlastní výsledek. + +HandlerInvocation objekt je klíčem k pochopení, jak řetězení funguje. Obsahuje zásobník všech handlerů pro daný prvek a aktuální pozici v tomto zásobníku. Když handler zavolá `proceed()`, HandlerInvocation posune pozici o jedno místo zpět v zásobníku a zavolá další handler. Pokud handler zavolá `proceed()` s modifikovanými parametry, tyto nové parametry se předají všem následujícím handlerům. Pokud handler vůbec nezavolá `proceed()`, řetěz se přeruší a jeho návratová hodnota se stane výsledkem celého zpracování. + +Pořadí vykonávání handlerů je od posledně zaregistrovaného k prvnímu. To znamená, že uživatelský handler zaregistrovaný dodatečně dostane kontrolu první a může rozhodnout, zda vůbec zavolá výchozí handler modulu. Toto pořadí umožňuje uživatelům přepsat výchozí chování bez nutnosti měnit kód modulu. + +Typické použití element handleru vypadá následovně. Handler zkontroluje vstupní parametry a rozhodne, zda chce zasáhnout do zpracování. Pokud ano, upraví data, zavolá `proceed()` s novými parametry a případně ještě upraví vrácený výsledek. Pokud chce handler úplně nahradit výchozí zpracování, vytvoří vlastní výsledek a vrátí ho bez volání `proceed()`. + + +Notification handler +==================== + +Notification handlery představují jednodušší, jednosměrný komunikační mechanismus. Na rozdíl od element handlerů neslouží k transformaci dat, ale k provedení vedlejších akcí. + +Registrace notification handleru používá stejnou metodu `Texy::addHandler()` jako element handlery. Rozdíl je v tom, jak se handler používá - notification handler nevrací žádnou hodnotu a nemá přístup k HandlerInvocation. První parametr je název události. Používají se popisné názvy jako `beforeParse` a `afterParse` pro globální události okolo parsování, nebo specifičtější jako `afterTable`, `afterList`, `afterBlockquote` pro události po vytvoření konkrétní struktury. Prefix before/after jasně indikuje časování události. + +Vyvolání notification handlerů používá metodu `Texy::invokeHandlers()`. Tato metoda jednoduše zavolá všechny zaregistrované handlery v pořadí a ignoruje jejich návratové hodnoty. Notification handlery dostanou parametry předané při vyvolání, ale nemohou je měnit pro další handlery v řadě. + +Typické použití notification handlerů zahrnuje několik scénářů. Handler pro událost `beforeParse` může načíst definice referencí z textu ještě před začátkem parsování. Handler `afterParse` může projít vytvořený DOM strom a doplnit chybějící atributy nebo sestavit tabulku obsahu. Handlery jako `afterTable` nebo `afterList` umožňují modulům provést závěrečné úpravy vytvořených struktur. + +Důležitý rozdíl oproti element handlerům je v tom, že notification handlery nemohou zabránit dalšímu zpracování. Všechny zaregistrované handlery se vždy vykonají, žádný nemůže přerušit řetěz. To je zamýšlené chování - notification handlery jsou o vedlejších efektech, ne o kontrole toku. + + +LineParser +========== + +LineParser zpracovává inline syntaxe uvnitř řádků textu postupným způsobem, který umožňuje vnořování a složité interakce mezi syntaxemi. + +Základní princip spočívá v hledání prvního výskytu jakékoliv syntaxe. V každé iteraci projde všechny syntaxe a zjistí, která z nich odpovídá nejblíže aktuální pozici v textu. Tato syntax *vyhrává* a zpracuje se. Pokud více syntaxí odpovídá na stejné pozici, vyhrává ta, která byla registrována dříve - to je priorita podle pořadí registrace. + +Když parser najde nejbližší match, zavolá příslušný syntax handler. Ten vrátí výsledek, který může být `HtmlElement` nebo řetězec. A tímto výsledkem se přepíše nalezený match v textu. + +Poté hledá znovu od aktuální pozice. Tento systém zajišťuje, že parser vždy vidí aktuální stav textu. Když nahradíme match novým textem, který může obsahovat další syntaxe, tyto syntaxe se najdou v příští iteraci. + +Property `$again` na LineParser objektu slouží k jemnému řízení toho, zda by se měla právě matchnutá syntaxe hledat znovu na stejné pozici po zpracování aktuálního matche. Výchozí hodnota je false, která říká: *Na této pozici už nemá smysl hledat tuto stejnou syntaxi. Posuň se dál.* + +Průchod končí, když parser dojde na konec textu nebo když už žádná syntaxe nemá další match. Výsledkem je text, kde všechny rozpoznatelné syntaxe byly zpracovány a nahrazeny výsledky, připravený pro finální konverzi. + + +Vnořování +--------- + +Schopnost zpracovat vnořené syntaxe je jednou z klíčových vlastností LineParser a představuje základní výzvu - jak zabránit tomu, aby již zpracované HTML tagy byly omylem interpretovány jako další syntax ke zpracování. + +Když parser zpracovává text obsahující vnořené syntaxe, nejprve najde vnější konstrukci. Například v textu `"odkaz **tučný** text":URL` parser nejprve najde syntaxi pro odkaz s uvozovkami. Pattern pro tuto syntaxi odpovídá celému řetězci od první uvozovky po dvojtečku a URL. Syntax handler vytvoří `HtmlElement` pro tag `<a>` a obsah `odkaz **tučný** text` se přidá jako potomek elementu. Tento řetězec vloží zpět do textu a pokračuje v hledání dalších syntaxí (`**tučný**`, který představuje tučný text). + +Ale teď má problém - v textu jsou také HTML značky, které mohou odpovídat začátku jiné syntaxe. Parser by začal zpracovávat již hotové HTML tagy, jako kdyby byly součástí původního textu. + +Nechceme, aby parser viděl HTML tagy. Potřebujeme nějaký způsob, jak rozlišit již zpracované části od částí čekajících na zpracování. Metoda `Texy::protect()` řeší tyto problémy elegantním způsobem - nahradí HTML tagy unikátním placeholderem složeným z řídicích znaků - speciálních bajtů mimo tisknutelné ASCII. + +Když se tedy `HtmlElement` převádí na řetězec (pomocí `toString()`), výsledek nevypadá jako `<a href="...">odkaz **tučný** text</a>`, ale například jako `\x17\x18\x19\x17odkaz **tučný** text\x17\x18\x1A\x17`. + +V textu během parsování tak nejsou nikdy přítomny skutečné HTML tagy. Místo nich jsou pouze placeholdery. Ale vnitřní text zůstává a parser ho normálně vidí a může v něm hledat další syntaxe. To umožňuje postupné vnořování - vnější syntax se zamaskuje, ale její obsah je stále přístupný pro vnitřní syntaxe. + +Na konci zpracování metoda `Texy::unProtect()` projde výsledný HTML řetězec a nahradí všechny placeholdery jejich skutečnými hodnotami. Teprve v tomto okamžiku se do výstupu dostanou skutečné HTML tagy. + + +Úrovně maskování +---------------- + +Různé druhy obsahu používají pro své placeholdery různé řídicí znaky, což umožňuje syntaxím selektivně rozhodnout, co mohou obsahovat. + +- `Texy::CONTENT_MARKUP` označuje běžný HTML markup jako tagy pro formátování nebo odkazy. Je to nejběžnější typ a používá ho většina inline elementů. Placeholder začíná a končí `\x17`. +- `Texy::CONTENT_REPLACED` označuje obsah, který byl nahrazen něčím jiným, typicky obrázky nebo jiné replaced elementy. Používá `\x16` jako marker. +- `Texy::CONTENT_TEXTUAL` označuje text, který byl escapován nebo jinak ošetřen, aby se nezpracovával. Používá se pro konstrukce jako code nebo notexy, kde chceme zobrazit původní text včetně markup symbolů, ne jejich interpretaci. +- `Texy::CONTENT_BLOCK` označuje blokové elementy. Je to nejnižší úroveň v hierarchii. Používá `\x14` jako marker. + +Hierarchie těchto typů není jen konvence, ale má praktický důsledek. Konstanta Patterns::MARK je definována jako `\x14-\x1F`, tedy rozsah pokrývající všechny tyto typy plus rezervu. Syntaxe používají tuto konstantu ve svých patterns pro vyloučení maskovaných částí. + +Různé syntaxe mohou mít různé požadavky na to, co mohou obsahovat za placeholdery. Pattern, který chce vidět pouze čistý text bez jakýchkoliv maskovaných částí, použije vyloučení `[^\x14-\x1F]`. To odmítne všechny placeholdery všech typů. Příkladem je pattern pro obrázky - URL obrázku by neměla obsahovat žádné HTML tagy ani bloky. + +Pattern, který akceptuje nižší úrovně, ale odmítá vyšší, použije užší rozsah. Například `[^\x17-\x1F]` odmítne pouze `CONTENT_MARKUP` a výš, ale akceptuje `CONTENT_BLOCK`, `CONTENT_TEXTUAL` a `CONTENT_REPLACED`. To je užitečné, pokud chceme povolit bloky, ale ne inline markup. Praktickým příkladem je TypographyModule, který provádí typografické úpravy jako nahrazení uvozovek nebo vkládání nezlomitelných mezer. Tyto úpravy by se měly aplikovat na běžný text, ale ne uvnitř bloků kódu nebo preformátovaného textu. + + +Kolize syntaxí +-------------- + +Kolize nastává, když více syntaxí může odpovídat na stejné pozici, a systém musí vybrat jednu z nich. + +Typickým příkladem jsou různé délky stejného symbolu. Syntaxe `phrase/strong+em` používá tři hvězdičky pro kombinaci tučného a kurzívy. Syntaxe `phrase/strong` používá dvě hvězdičky pro samotný tučný text. Syntaxe `phrase/em-alt` používá jednu hvězdičku pro kurzívu. Když parser najde text začínající třemi hvězdičkami, mohou technicky odpovídat všechny tři syntaxe. + +PhraseModule řeší tuto kolizi registrací syntaxí v pořadí od nejdelší po nejkratší. Nejprve registruje `phrase/strong+em` s patternem pro tři hvězdičky. Pak `phrase/strong` s patternem pro dvě hvězdičky. Nakonec `phrase/em-alt` s patternem pro jednu hvězdičku. Díky tomuto pořadí se při nálezu tří hvězdiček zpracuje nejprve `phrase/strong+em` a kratší syntaxe nedostanou šanci. + +Další příklad jsou odkazy v různých formátech. Syntaxe `phrase/wikilink` používá pattern pro `[text|url]`. Syntaxe `link/reference` používá pattern pro `[ref]`. Oba začínají otevírací hranatou závorkou. Pokud je v textu `[text|url]`, oba patterns technicky mohou začít matchnout. + +Řešením je opět specifičnost patterns. Pattern pro `phrase/wikilink` je specifičtější - vyžaduje svislítko uvnitř závorek. Pokud text obsahuje svislítko, matchne `phrase/wikilink`. Pokud ne, pattern selže a `link/reference` má šanci. Pořadí registrace zde také hraje roli - `phrase/wikilink` by měla být registrována dříve než `link/reference`. + + +BlockParser +=========== + +BlockParser používá fundamentálně odlišný přístup k zpracování, který reflektuje povahu blokových konstrukcí. Základní rozdíl je v absenci prolínání. Zatímco LineParser umožňuje, aby syntaxe byly vnořené do sebe a postupně se rozbalovaly, BlockParser pracuje s předpokladem, že každý blok je samostatná jednotka. Jeden řádek nebo skupina řádků patří k maximálně jednomu bloku. Bloky se nepřekrývají, nekříží a nevnoří na úrovni BlockParser. + +BlockParser začíná vyhledáním všech bloků, respektive jejich začátků. Parser projde všechny registrované block syntaxe a najde všechny jejich výskyty. Pokud více syntaxí matchuje na stejné pozici, použije se pořadí registrace - dříve registrovaná syntaxe má přednost. + + +API pro syntax handler +---------------------- + +BlockParser poskytuje syntax handlerům API pro práci s víceřádkovými strukturami. + +Metoda `BlockParser::moveBackward()` slouží k návratu na předchozí řádky. Přijímá počet řádků, o které se má vrátit. Parser posune svou interní pozici směrem k začátku textu, dokud nepřejde přes specifikovaný počet konců řádků. To umožňuje callbacku začít číst od začátku struktury, i když pattern matchnul až uprostřed nebo na konci. + +Metoda `BlockParser::next()` slouží k čtení dalšího řádku odpovídajícího určitému patternu. Přijímá regex pattern (automaticky přidá modifikátory `Am`) a referenci na proměnnou pro výsledek matche. Pokud další řádek v textu matchuje poskytnutý pattern, metoda naplní výsledek, posune interní pozici za tento řádek a vrátí true. Pokud další řádek nematchuje, metoda vrátí false a pozice se nezmění. + + +Moduly +****** + +Moduly jsou základní organizační jednotkou v architektuře Texy. Každý modul zapouzdřuje kompletní funkcionalitu pro určitou oblast markup jazyka. + +Primární zodpovědností modulu je registrace syntaxí. V konstruktoru modul volá `Texy::registerLinePattern()` nebo `registerBlockPattern()` pro všechny syntaxe, které chce zpracovávat. Tím říká parseru: *Když najdeš tyto patterny, zavolej mě.* Modul tak definuje, které konstrukce v textu rozpoznává. + +Druhá zodpovědnost je implementace element handlerů. Modul registruje handlery pro elementy, které jeho syntaxe vyvolávají. Tyto handlery obsahují logiku pro převod nalezených konstrukcí na HTML elementy. Element handler rozhoduje, jaký element vytvořit, jaké atributy nastavit a jak zpracovat obsah. + +Třetí zodpovědnost je poskytnutí konfigurace. Moduly mají veřejné properties, které umožňují uživatelům Texy upravit chování modulu bez nutnosti měnit jeho kód. Například ImageModule má properties pro nastavení root cesty k obrázkům nebo výchozího alt textu. + +Čtvrtá zodpovědnost je správa stavu specifického pro modul. Například HeadingModule sleduje všechny nalezené nadpisy v poli TOC pro sestavení obsahu. LinkModule spravuje slovník referencí pro odkazy. Tento stav je soukromý pro modul a ostatní části systému k němu nepřistupují přímo. + +Moduly jsou navrženy jako nezávislé jednotky. Každý modul může fungovat samostatně a neměl by záviset na implementačních detailech jiných modulů. Komunikace mezi moduly probíhá přes sdílené objekty jako `Link` nebo `Image`, ne přes přímé volání metod. + + +Struktura typického modulu +========================== + +Většina modulů v Texy následuje podobnou strukturu, která reflektuje jejich úlohu v systému. + +Modul dědí od základní třídy Module, která poskytuje přístup k Texy objektu přes protected property `$texy`. Konstruktor modulu přijímá instanci Texy a uloží si ji. To umožňuje modulu přistupovat ke konfiguraci a volat metody na Texy objektu. + +V konstruktoru probíhá veškerá inicializace. Modul nastaví výchozí hodnoty konfiguračních properties, případně nastaví výchozí hodnoty v poli `Texy::$allowed` pro své syntaxe. Pak registruje své syntaxe voláním `registerLinePattern()` nebo `registerBlockPattern()`. Každá registrace spojuje pattern, syntax handler a název syntaxe. Nakonec modul registruje své element handlery voláním `addHandler()`. + +Syntax handlery jsou metody modulu, které parser volá při nálezu syntaxe. Tyto metody typicky extrahují části z regex matche, vytvářejí pomocné objekty a vyvolávají element handlery. Syntax handler rozhoduje, jaký element handler vyvolat a jaké parametry předat. + +Element handlery jsou metody implementující skutečné zpracování. Dostávají HandlerInvocation objekt jako první parametr, následovaný parametry specifickými pro daný element. Element handler vytváří `HtmlElement`, aplikuje modifikátory, zpracovává obsah a vrací výsledek. Je to místo, kde se rozhoduje o finální podobě HTML. + +Veřejné properties slouží jako rozhraní pro konfiguraci. Uživatel Texy může nastavit tyto properties pro přizpůsobení chování modulu. Properties jsou typicky primitivní typy nebo pole, ne složité objekty, aby konfigurace byla jednoduchá. + + +Přehled klíčových modulů +======================== + +Standardní distribuce Texy obsahuje několik modulů pokrývajících různé aspekty markup jazyka. + +- **PhraseModule** zpracovává inline formátování textu. Registruje syntaxe pro tučný text, kurzívu, vložený a smazaný text, horní a dolní index, kód a další. Všechny tyto syntaxe vyvolávají společný handler pro element `phrase` a handler rozlišuje podle názvu syntaxe, jaký tag vytvořit. Modul umožňuje konfigurovat, které tagy se použijí pro jednotlivé druhy formátování. + +- **LinkModule** spravuje odkazy v dokumentu. Registruje syntaxe pro různé formáty odkazů - explicitní URL, emailové adresy, reference na definované odkazy. Poskytuje factory metody pro vytváření `Link` objektů a spravuje slovník referencí. Modul umožňuje konfigurovat root pro relativní odkazy, automatické `rel="nofollow"` pro externí odkazy a zkracování dlouhých URL. + +- **ImageModule** zpracovává obrázky podobným způsobem jako LinkModule odkazy. Registruje syntaxi pro inline obrázky a spravuje slovník referencí na definované obrázky. Poskytuje factory metody pro vytváření `Image` objektů a automatickou detekci rozměrů obrázků. Konfigurovatelné jsou cesty k obrázkům, výchozí alt text a CSS třídy pro zarovnání. + +- **HeadingModule** rozpoznává nadpisy v různých formátech - podtržené pomlčkami nebo rovnítky, obklopené mřížkami. Shromažďuje všechny nadpisy do pole TOC pro možné sestavení obsahu. Umožňuje konfigurovat generování ID, top úroveň nadpisů a režim balancování úrovní. + +- **ListModule** zpracovává seznamy - nečíslované, číslované a definiční. Rozpoznává různé typy odrážek a automaticky detekuje vnořování podle odsazení. Umožňuje konfigurovat, které znaky slouží jako odrážky a jaké HTML listy generovat. + +- **TableModule** je jedním z nejkomplexnějších modulů. Rozpoznává tabulky s hlavičkami, těly, titulky a podporuje colspan a rowspan. Zpracovává modifikátory pro řádky i buňky. + +- **BlockModule** zpracovává speciální bloky ohraničené `/--` a `\--`. Podporuje různé typy bloků - code pro kód, html pro přímé HTML, div pro generický kontejner. Umožňuje uživatelům definovat vlastní handlery pro vlastní typy bloků. + +- **TypographyModule** provádí post-processing pro typografické úpravy. Nahrazuje tři tečky elipsou, dvojité pomlčky en-dash, přímé uvozovky typografickými a vkládá nezlomitelné mezery. Pracuje na úrovni finálního řetězce mezi blokovými elementy. + +- **HtmlOutputModule** formátuje finální HTML výstup. Zajišťuje wellformed HTML automatickým zavíráním tagů, opravou nesprávného vnořování, odsazením kódu a zalamováním dlouhých řádků. Umožňuje konfigurovat úroveň odsazení a šířku řádků. + + +Interakce mezi moduly +===================== + +Ačkoliv jsou moduly navrženy jako nezávislé, v některých případech musí spolupracovat. + +Sdílené objekty jsou hlavní mechanismus komunikace. `Link` objekt vytvořený LinkModule může být předán ImageModule pro vytvoření obrázkového odkazu. `Image` objekt vytvořený ImageModule může být předán FigureModule pro vytvoření obrázku s popiskem. Tyto objekty zapouzdřují veškeré potřebné informace a poskytují společné rozhraní. + +Reference systém umožňuje oddělit definici od použití. LinkModule poskytuje metody `addReference()` a `getReference()` pro správu slovníku pojmenovaných odkazů. Uživatel může v jedné části dokumentu definovat referenci a v jiné ji použít. ImageModule má analogický systém pro reference na obrázky. Moduly používající reference volají factory metody, které samy kontrolují, zda jde o referenci nebo přímou hodnotu. + +Element handlers mohou volat jiné element handlery. PhraseModule při zpracování `phrase/span` s odkazem vytvoří `Link` objekt a zavolá element handler LinkModule pro vytvoření odkazu. Tím deleguje odpovědnost za vytvoření a konfiguraci odkazu na specializovaný modul. + +Vztahy mezi moduly jsou typicky jednostranné. PhraseModule zná LinkModule a ImageModule, protože vytváří odkazy a obrázky. Ale LinkModule a ImageModule neznají PhraseModule. To udržuje závislosti jednoduché a umožňuje snadné nahrazení nebo rozšíření modulů. + + +DOM reprezentace +**************** + +`HtmlElement` reprezentuje jeden uzel v DOM stromu a poskytuje rozhraní pro jeho manipulaci a zpracování. + +Základní struktura elementu obsahuje název tagu, asociativní pole atributů a pole potomků. Potomci mohou být další instance `HtmlElement` nebo prostě textové řetězce. Tato kombinace umožňuje reprezentovat libovolnou HTML strukturu. + +Název elementu se nastavuje a získává přes metody `setName()` a `getName()`. Speciální hodnota null jako název znamená transparentní element, který nemá tagy, jen jeho obsah. + +Atributy jsou veřejně přístupné přes property `$attrs` jako asociativní pole. Hodnoty mohou být řetězce, čísla, boolean nebo pole. Boolean true znamená atribut bez hodnoty (jako checked), false nebo null znamená atribut se vůbec nevykreslí. Pokud je hodnota pole, různé prvky se spojí podle typu atributu - pro class mezerami, pro style středníky. Metoda `setAttribute()` nastaví hodnotu atributu. Metoda `getAttribute()` vrací hodnotu atributu nebo null. + +Potomci se spravují přes několik metod. Metoda `add()` přidává potomka na konec. Metoda `insert()` vkládá potomka na specifikovanou pozici, volitelně nahrazuje existujícího potomka. Metoda `create()` vytváří nový `HtmlElement` jako potomka a vrací ho pro další manipulaci. Metoda `removeChildren()` odstraní všechny potomky. + +Element implementuje ArrayAccess interface, takže s potomky lze pracovat jako s polem. Zápis `$el[0]` vrací prvního potomka, `$el[0] = $child` nastaví prvního potomka. Tento přístup je pohodlný pro rychlou manipulaci s konkrétními potomky. + +Metoda `toString()` prochází element a jeho potomky rekurzivně a sestavuje řetězcovou reprezentaci. HTML tagy se okamžitě zamaskují pomocí `Texy::protect()`, takže do výsledku jde placeholder místo skutečných HTML znaků. + +Metody `toHtml()` a `toText()` vrací výsledek nemaskovaný včetně post-processingu. + + +Parsování obsahu +================ + +`HtmlElement` může rekurzivně parsovat svůj obsah, čímž umožňuje postupné budování DOM stromu. + +Metoda `parseLine()` slouží k parsování inline syntaxí v řetězci. Vytvoří novou instanci LineParser s aktuálním elementem jako kontejnerem. Zavolá `parse()` na parseru s poskytnutým textem. LineParser postupně najde a zpracuje všechny inline syntaxe a výsledné elementy nebo řetězce přidá jako potomky aktuálního elementu. Metoda vrací použitý LineParser pro případné další použití. + +Metoda `parseBlock()` parsuje text jako blokový obsah. Vytvoří BlockParser a zavolá na něm `parse()`. BlockParser najde všechny blokové konstrukce v textu, zpracuje je a přidá jako potomky elementu. Text mezi bloky se zpracuje jako odstavce, které interně používají LineParser. Metoda přijímá boolean parametr indikující, zda text pochází z odsazeného bloku, což ovlivňuje zpracování odstavců. + +Tyto parsovací metody umožňují rekurzivní zpracování. Syntax handler může vytvořit element, nastavit jeho základní vlastnosti a pak zavolat `parseLine()` nebo `parseBlock()` pro zpracování obsahu. Výsledkem je, že obsah elementu prochází stejným procesem parsování jako hlavní dokument, včetně rozpoznávání syntaxí a vyvolávání handlerů. + + +Validace +======== + +`HtmlElement` poskytuje mechanismy pro validaci atributů a obsahu podle HTML DTD (Document Type Definition). + +DTD je statické pole definující pro každý HTML tag, které atributy jsou povolené a jaký obsah může obsahovat. Texy načítá DTD ze souboru při inicializaci a uloží ho do statického pole. Struktura DTD mapuje název tagu na dvojici - pole povolených atributů a pole povoleného obsahu. + +Metoda `validateAttrs()` kontroluje atributy elementu podle DTD. Pro daný tag získá seznam povolených atributů. Prochází všechny atributy elementu a ty, které nejsou v seznamu, odstraní. Speciální případy jsou atributy začínající data- nebo aria-, které jsou povolené, pokud je v DTD zástupný záznam `data-*` nebo `aria-*`. + +Tato validace se typicky volá při aplikaci modifikátorů metodou `decorate()`. Zajišťuje, že i když uživatel zadá modifikátor s neplatným atributem pro daný tag, atribut se do finálního HTML nedostane. To je důležité pro bezpečnost a správnost HTML. + +Metoda `validateChild()` kontroluje, zda daný potomek může být obsahem elementu. Přijímá potomka (`HtmlElement` nebo název tagu) a DTD. Pokud je element v DTD definován, metoda zkontroluje, zda potomek je v seznamu povoleného obsahu. Pokud ano, vrací true. Pokud ne, vrací false. + +Tato validace se může použít při dynamickém sestavování DOM stromu pro zajištění korektní struktury. Například paragraph element nesmí obsahovat blokové elementy, takže `validateChild()` by odmítlo přidat div do p. V praxi Texy tuto validaci používá omezeně, protože struktura generovaná moduly je typicky správná by design. + +Kombinace `validateAttrs()` a `validateChild()` poskytuje mechanismus pro zajištění validního HTML, i když vstup obsahuje nedůvěryhodná data nebo špatně formované konstrukce. Texy může být nakonfigurováno pro striktní validaci nebo může validaci vypnout pro maximální flexibilitu. + + +Modifikátory +************ + +Modifikátory poskytují způsob, jak přidat elementům dodatečné atributy, třídy, styly a zarovnání bez nutnosti psát přímé HTML. + +Základní formát modifikátoru je tečka následovaná kombinací různých částí v kulatých, hranatých a složených závorkách: `.(title)[class1 class2 #id]{style:value}<align>^valign`. Celý modifikátor se píše před nebo na konec konstrukce, na kterou se aplikuje. Například `"**text** .(Důležité)[highlight]{color:red}"` vytvoří tučný text se třídou highlight, červenou barvou a title atributem Důležité. + +Kulaté závorky obsahují title atribut nebo alt text. Text uvnitř se použije jako hodnota title atributu na výsledném elementu. Pokud element je obrázek, může se použít jako alt text. Uvnitř kulatých závorek je možné escapovat závorku zpětným lomítkem. + +Hranaté závorky obsahují CSS třídy a volitelně ID. Třídy se píší jako slova oddělená mezerami. ID se píše s prefixem mřížky. Například `[main-content selected #article-5]` nastaví dvě třídy a jedno ID. Pokud je ID uvedeno vícekrát, použije se poslední. + +Složené závorky obsahují CSS styly nebo HTML atributy. Styly se píší ve standardním CSS formátu property:value. Více stylů se odděluje středníky. Některé property jsou rozpoznány jako HTML atributy - například `{href:url}` se převede na atribut href, ne na CSS style. To umožňuje nastavit atributy, které není možné vyjádřit jinak. + +Zarovnání se zadává pomocí speciálních znaků. `<` znamená vlevo, `>` vpravo, `=` pro do bloku, `<>` pro na střed. Vertikální zarovnání používá `^` pro nahoru, `-` pro střed a `_` pro dolů. Tyto zkratky se převádějí buď na CSS třídy nebo inline styly podle konfigurace. + +Části modifikátoru mohou být v libovolném pořadí a některé mohou být vynechány. Platný je modifikátor obsahující jen třídy `.[highlight]`, jen title `.(Poznámka)` nebo jen styl `.{color:blue}`. Parser rozpozná jednotlivé části podle ohraničujících znaků. + + +Modifier třída +============== + +Třída `Modifier` slouží k parsování a uchovávání informací z modifikátoru. + +Instance `Modifier` se typicky vytváří syntax handler, který předá konstruktoru text modifikátoru extrahovaný z regex matche. Konstruktor zavolá metodu `setProperties()`, která parsuje text a naplní properties objektu. + +Veřejné properties obsahují jednotlivé části modifikátoru. Property `$id` obsahuje ID elementu jako řetězec nebo null. Property `$classes` je asociativní pole, kde klíče jsou názvy tříd a hodnoty jsou true. Property `$styles` je asociativní pole mapující CSS property na hodnoty. Property `$attrs` je asociativní pole s HTML atributy, které nejsou styly ani třídy. + +Dvě speciální properties `$hAlign` a `$vAlign` obsahují horizontální a vertikální zarovnání jako řetězce `left`, `right`, `center`, `justify` nebo `top`, `middle`, `bottom`. Tyto hodnoty se později převádějí na CSS třídy nebo styly podle konfigurace Texy. + +Property `$title` obsahuje text z kulatých závorek, který se použije jako title atribut nebo alt text u obrázků. Text je automaticky unescapován z HTML entit a zbaven escapovaných závorek. + + +Aplikace na elementy +==================== + +`Modifier` objekt se aplikuje na `HtmlElement` pomocí metody `Modifier::decorate()`. + +Metoda `decorate()` přijímá instanci Texy a `HtmlElement` jako parametry. Postupně aplikuje jednotlivé části modifikátoru na element s ohledem na konfiguraci Texy, která může některé části zakázat nebo omezit. + +Aplikace atributů kontroluje, které atributy jsou povolené pro daný tag podle `Texy::$allowedTags` konfigurace. Pokud jsou všechny atributy povolené, zkopírují se všechny atributy z `Modifier` do elementu. Pokud je povolen jen seznam konkrétních atributů, zkopírují se pouze ty, které jsou na seznamu. + +Title atribut se vždy aplikuje, pokud je nastaven, ale text prochází typografickým post-processingem pro nahrazení uvozovek a dalších úprav. + +Aplikace tříd a ID kontroluje konfiguraci `Texy::$allowedClasses`. Pokud jsou všechny třídy povolené, přidají se všechny třídy z `Modifier` do elementu a nastaví se ID. Pokud je povolen jen seznam konkrétních tříd, přidají se pouze ty, které jsou na seznamu. ID se přidá, jen pokud je v seznamu povolen řetězec začínající mřížkou. + +Aplikace stylů probíhá podobně s kontrolou `Texy::$allowedStyles`. Povolené CSS properties se přidají do style atributu elementu. Pokud element již měl nějaké styly, modifikátorové styly se přidají nebo přepíšou existující. + +Zarovnání se aplikuje buď jako CSS třída nebo inline style. Pokud je v Texy konfigurováno `Texy::$alignClasses` mapování pro daný typ zarovnání, přidá se odpovídající CSS třída. Pokud ne, přidá se inline style s text-align nebo vertical-align property. + +Výsledkem je, že element má všechny atributy, třídy, styly a další vlastnosti z modifikátoru, ale pouze ty, které jsou povoleny aktuální konfigurací Texy. To zajišťuje bezpečnost při zpracování nedůvěryhodného vstupu. + + +Propagace modifikátorů +====================== + +Modifikátory procházejí systémem v několika fázích, přičemž si zachovávají flexibilitu a umožňují úpravy na různých úrovních. + +Syntax handler extrahuje text modifikátoru z regex matche a vytvoří novou instanci `Modifier` a naplní se jeho properties. + +`Modifier` objekt se předává jako parametr do element handlerů. Handler dostává již parsovaný objekt, ne surový text. To umožňuje handleru snadno přistupovat k jednotlivým částem modifikátoru - třídám, stylům, zarovnání. Handler může modifikátor upravit před aplikací, například přidat další třídy nebo změnit styly. + +Element handler vytváří `HtmlElement` a předá jej metodě `Modifier::decorate()`. V tomto okamžiku se modifikátor aplikuje na element. Metoda `decorate()` kontroluje konfigurace Texy a zajišťuje, že se aplikují pouze povolené části. + +V některých případech modul kombinuje více modifikátorů. Například TableModule parsuje modifikátory na úrovni tabulky, řádků i buněk. Modifikátor buňky je vlastně klon modifikátoru sloupce, na kterém se pak aplikují dodatečné úpravy z modifikátoru konkrétní buňky. To umožňuje výchozí styly pro celý sloupec s možností přepsání v jednotlivých buňkách. diff --git a/texy/cs/configuration.texy b/texy/cs/configuration.texy new file mode 100644 index 0000000000..04b3ee7400 --- /dev/null +++ b/texy/cs/configuration.texy @@ -0,0 +1,719 @@ +Konfigurace +*********** + +.[perex] +Kompletní průvodce konfigurací Texy. Naučíte se ovládat všechny moduly, nastavit bezpečnost a přizpůsobit Texy vašim potřebám. + +Texy se konfiguruje pomocí **public properties** hlavní třídy `Texy\Texy` a jejích **modulů**. Každý modul je zodpovědný za zpracování konkrétní části syntaxe (obrázky, odkazy, nadpisy...). + +Základní přístup: + +```php +$texy = new Texy\Texy; + +// Konfigurace hlavní třídy +$texy->allowedTags = Texy\Texy::NONE; + +// Konfigurace modulu +$texy->imageModule->root = '/images/'; +``` + + +Třída Texy\Texy .[#texy-class] +============================== + +Hlavní třída obsahuje globální nastavení a vlastnosti ovlivňující celé zpracování. + + +Povolené syntaxe ($allowed) .{toc: $allowed} +-------------------------------------------- + +Pole `$allowed` kontroluje, které části syntaxe Texy jsou aktivní: + +```php +// Výchozí: vše povoleno kromě emotikonů a frází ins/del/sup/sub +$texy->allowed['image'] = true; +$texy->allowed['emoticon'] = false; + +// Vypnout obrázky +$texy->allowed['image'] = false; + +// Vypnout HTML značky ve vstupu +$texy->allowed['html/tag'] = false; +$texy->allowed['html/comment'] = false; + +// Vypnout různé typy odkazů +$texy->allowed['link/reference'] = false; +$texy->allowed['link/email'] = false; +$texy->allowed['link/url'] = false; +``` + +**Kompletní seznam syntaxí:** + +|--- +| Klíč | Výchozí | Popis +|--- +| `image` | `true` | Obrázky `[* img.jpg *]` +| `figure` | `true` | Obrázky s popiskou +| `link/reference` | `true` | Reference `[ref]` +| `link/email` | `true` | E-mailové adresy +| `link/url` | `true` | Automatické URL +| `link/definition` | `true` | Definice referencí +| `heading/underlined` | `true` | Podtržené nadpisy +| `heading/surrounded` | `true` | Ohraničené nadpisy +| `horizline` | `true` | Horizontální čáry +| `blockquote` | `true` | Citace +| `list` | `true` | Seznamy +| `list/definition` | `true` | Definiční seznamy +| `table` | `true` | Tabulky +| `phrase/strong` | `true` | Tučné písmo `**text**` +| `phrase/em` | `true` | Kurzíva `//text//` +| `phrase/em-alt` | `true` | Kurzíva `*text*` +| `phrase/code` | `true` | Kód ```text``` +| `phrase/ins` | `false` | Vložený text `++text++` +| `phrase/del` | `false` | Smazaný text `--text--` +| `phrase/sup` | `false` | Horní index `^^text^^` +| `phrase/sub` | `false` | Dolní index `__text__` +| `html/tag` | `true` | HTML značky ve vstupu +| `html/comment` | `true` | HTML komentáře +| `emoticon` | `false` | Emotikony `:-)`, `:-(` +| `blocks` | `true` | Bloky `/-- \--` +| `typography` | `true` | Typografické úpravy +| `longwords` | `true` | Dělení dlouhých slov + + +Povolené HTML značky ($allowedTags) .{toc: $allowedTags} +-------------------------------------------------------- + +Kontroluje, které HTML značky mohou být ve výstupu (a na vstupu): + +```php +// Výchozí je whitelist všech validních HTML5 značek; Texy::ALL povolí libovolnou značku +$texy->allowedTags = Texy\Texy::ALL; + +// Zakázat všechny HTML značky +$texy->allowedTags = Texy\Texy::NONE; + +// Povolit jen konkrétní značky +$texy->allowedTags = [ + 'strong' => [], // <strong> bez atributů + 'a' => ['href', 'title'], // <a> s atributy + 'img' => Texy\Texy::ALL, // <img> s jakýmikoliv atributy +]; +``` + +**Formáty:** +- `Texy::ALL` - povoleny jsou všechny značky +- `Texy::NONE` - povolena není žádná značka +- Pole - povolené značky jako klíče, povolené atributy jako hodnoty + + +Povolené CSS třídy ($allowedClasses) .{toc: $allowedClasses} +------------------------------------------------------------ + +Kontroluje, které CSS třídy a ID mohou být použity: + +```php +// Výchozí: všechny třídy a ID povoleny +$texy->allowedClasses = Texy\Texy::ALL; + +// Zakázat třídy a ID +$texy->allowedClasses = Texy\Texy::NONE; + +// Povolit konkrétní třídy a ID +$texy->allowedClasses = [ + 'highlight', + 'important', + '#main', // ID začínají # + '#sidebar', +]; +``` + +Použití: +```texy +Text s třídou .[highlight] + +Text s ID .[#main] +``` + + +Povolené CSS styly ($allowedStyles) .{toc: $allowedStyles} +---------------------------------------------------------- + +Kontroluje, které inline CSS vlastnosti mohou být použity: + +```php +// Výchozí: všechny styly povoleny +$texy->allowedStyles = Texy\Texy::ALL; + +// Zakázat inline styly +$texy->allowedStyles = Texy\Texy::NONE; + +// Povolit konkrétní CSS vlastnosti +$texy->allowedStyles = [ + 'color', + 'background-color', + 'font-size', +]; +``` + +Použití: +```texy +Text s barvou .{color: red} +``` + + +CSS třídy pro zarovnání ($alignClasses) .{toc: $alignClasses} +------------------------------------------------------------- + +Místo inline stylů `style="text-align:left"` můžete použít CSS třídy: + +```php +// Výchozí: všechny hodnoty null (použijí se inline styly) +$texy->alignClasses = [ + 'left' => null, + 'right' => null, + 'center' => null, + 'justify' => null, + 'top' => null, + 'middle' => null, + 'bottom' => null, +]; + +// Nastavit třídy +$texy->alignClasses['left'] = 'text-left'; +$texy->alignClasses['right'] = 'text-right'; +$texy->alignClasses['center'] = 'text-center'; +``` + +Použití: +```texy +Text zarovnaný doleva .< + +Text zarovnaný doprava .> +``` + +S nastaveným `alignClasses` vygeneruje `<p class="text-left">` místo `<p style="text-align:left">`. + + +Další vlastnosti +---------------- + +```php +// Spojování řádků do odstavců (výchozí: true) +$texy->mergeLines = true; + +// Šířka tabulátoru (výchozí: 8) +$texy->tabWidth = 8; + +// Maskování emailů před roboty (výchozí: true) +$texy->obfuscateEmail = true; + +// Odstraňování měkkých spojovníků (výchozí: true) +$texy->removeSoftHyphens = true; + +// Element pro netextové odstavce (výchozí: 'div') +$texy->nontextParagraph = 'div'; +``` + + +Moduly +====== + +Každý modul zpracovává konkrétní část syntaxe. Moduly jsou přístupné jako public properties třídy `Texy\Texy`. + + +HeadingModule +------------- + +Zpracovává nadpisy (podtržené i ohraničené). + +```php +// Úroveň nejvyššího nadpisu (výchozí: 1) +$texy->headingModule->top = 1; // <h1> + +// Generovat automatická ID (výchozí: false) +$texy->headingModule->generateID = true; + +// Prefix pro generovaná ID (výchozí: 'toc-') +$texy->headingModule->idPrefix = 'section-'; + +// Více znaků = vyšší nadpis? (výchozí: true) +$texy->headingModule->moreMeansHigher = true; + +// Režim vyvažování (výchozí: DYNAMIC) +$texy->headingModule->balancing = Texy\Modules\HeadingModule::DYNAMIC; +``` + +Po zpracování: + +```php +// První nadpis (pro <title>) +echo $texy->headingModule->title; + +// Obsah (Table of Contents) +print_r($texy->headingModule->TOC); +``` + + +PhraseModule +------------ + +Zpracovává inline formátování (tučné, kurzíva, odkazy v textu...). + +```php +// HTML značky pro jednotlivé fráze (výchozí: viz níže) +$texy->phraseModule->tags = [ + 'phrase/strong' => 'strong', + 'phrase/em' => 'em', + 'phrase/code' => 'code', + // ... další +]; + +// Povolit odkazy ve frázích (výchozí: true) +$texy->phraseModule->linksAllowed = true; +``` + + +LinkModule +---------- + +Zpracovává odkazy, reference a URL. + +```php +// Kořenová cesta pro odkazy (výchozí: null) +$texy->linkModule->root = '/articles/'; + +// CSS třída pro odkazy na obrázky (deprecated) +$texy->linkModule->imageClass = 'image-link'; + +// Vždy přidávat rel="nofollow" (výchozí: false) +$texy->linkModule->forceNoFollow = false; + +// Zkracovat URL na čitelnější formu (výchozí: true) +$texy->linkModule->shorten = true; +``` + +**Reference:** + +```php +// Přidat referenci +$link = new Texy\Link('https://example.com'); +$link->modifier->title = 'Ukázková stránka'; +$link->label = 'Příklad'; +$texy->linkModule->addReference('example', $link); +``` + +Použití: +```texy +Odkaz na [example] +``` + + +ImageModule +----------- + +Zpracovává obrázky. + +```php +// Kořenová cesta pro obrázky (výchozí: 'images/') +$texy->imageModule->root = '/assets/images/'; + +// Kořenová cesta pro linkované obrázky (deprecated) +$texy->imageModule->linkedRoot = '/assets/images/full/'; + +// Fyzická cesta na disku (pro zjištění rozměrů) +$texy->imageModule->fileRoot = __DIR__ . '/public/images/'; + +// CSS třída pro plovoucí obrázky (výchozí: null) +$texy->imageModule->leftClass = 'float-left'; +$texy->imageModule->rightClass = 'float-right'; + +// Výchozí alternativní text (deprecated) +$texy->imageModule->defaultAlt = 'Obrázek'; +``` + +**Reference:** + +```php +// Přidat referenci +$image = new Texy\Image; +$image->URL = 'photo.jpg'; +$image->modifier->title = 'Fotografie'; +$texy->imageModule->addReference('photo', $image); +``` + + +FigureModule +------------ + +Zpracovává obrázky s popiskou. + +```php +// HTML element (výchozí: 'div') +$texy->figureModule->tagName = 'figure'; + +// CSS třída (výchozí: 'figure') +$texy->figureModule->class = 'photo-figure'; + +// Třídy pro plovoucí obrázky (výchozí: null) +$texy->figureModule->leftClass = 'figure-left'; +$texy->figureModule->rightClass = 'figure-right'; + +// Offset pro výpočet šířky (deprecated) +$texy->figureModule->widthDelta = 20; + +// Vyžadovat popisku (deprecated) +$texy->figureModule->requireCaption = true; +``` + + +ListModule +---------- + +Zpracovává odrážkové, číslované a definiční seznamy. + +```php +// Definice odrážek a stylu (výchozí: viz zdrojový kód) +$texy->listModule->bullets = [ + '*' => ['\*[\ \t]', 0, ''], + '-' => ['[\x{2013}-](?![>-])', 0, ''], + // ... další +]; +``` + + +TableModule +----------- + +Zpracovává tabulky. + +```php +// CSS třídy pro řádky (výchozí: null) +$texy->tableModule->oddClass = 'odd'; +$texy->tableModule->evenClass = 'even'; +``` + +*Poznámka: `oddClass` a `evenClass` jsou deprecated.* + + +HorizLineModule +--------------- + +Zpracovává horizontální čáry. + +```php +// CSS třídy podle typu (výchozí: null) +$texy->horizLineModule->classes = [ + '-' => 'hr-line', + '*' => 'hr-star', +]; +``` + + +TypographyModule +---------------- + +Zpracovává typografické úpravy. + +```php +// Locale (výchozí: 'cs') +$texy->typographyModule->locale = 'en'; +``` + +**Podporované locales:** +- `cs` - české uvozovky „text“ a ‚text‘ +- `en` - anglické uvozovky “text” a ‘text’ +- `fr` - francouzské uvozovky «text» a ‹text› +- `de` - německé uvozovky „text“ a ‚text‘ +- `pl` - polské uvozovky „text” a ‚text’ + + +LongWordsModule +--------------- + +Rozděluje dlouhá slova pomocí `­`. + +```php +// Maximální délka slova (výchozí: 20) +$texy->longWordsModule->wordLimit = 25; +``` + + +EmoticonModule +-------------- + +Nahrazuje emotikony za obrázky nebo Unicode znaky. + +```php +// CSS třída (výchozí: null) +$texy->emoticonModule->class = 'emoji'; + +// Cesta k obrázkům (deprecated) +$texy->emoticonModule->root = '/images/smilies/'; +$texy->emoticonModule->fileRoot = __DIR__ . '/public/smilies/'; + +// Definice emotikonů (výchozí: základní sada) +$texy->emoticonModule->icons = [ + ':-)' => '🙂', + ':-(' => '☹', + ';-)' => '😉', + // ... nebo cesty k obrázkům + ':cool:' => 'cool.gif', +]; +``` + + +HtmlModule +---------- + +Zpracovává HTML značky a komentáře ve vstupním textu. + +```php +// Zobrazit HTML komentáře na výstupu (výchozí: true) +$texy->htmlModule->passComment = true; +``` + + +HtmlOutputModule +---------------- + +Formátuje výstupní HTML. + +```php +// Formátovat výstup (odsazení) (výchozí: true) +$texy->htmlOutputModule->indent = true; + +// Základní odsazení (výchozí: 0) +$texy->htmlOutputModule->baseIndent = 0; + +// Maximální šířka řádku (výchozí: 80) +$texy->htmlOutputModule->lineWrap = 100; + +// Zachovat mezery v těchto elementech (výchozí: seznam) +$texy->htmlOutputModule->preserveSpaces = [ + 'textarea', 'pre', 'script', 'code', 'samp', 'kbd', +]; +``` + + +ScriptModule +------------ + +Zpracovává volání `{{makro}}`. + +```php +// Oddělovač argumentů (výchozí: ',') +$texy->scriptModule->separator = ';'; +``` + + +Třída Texy\Configurator .{toc: Texy\Configurator} +================================================= + +Předpřipravené konfigurační sady pro časté případy použití. + + +safeMode() - Bezpečný režim .{toc: safeMode()} +---------------------------------------------- + +Konfigurace pro zpracování **nedůvěryhodného obsahu** od uživatelů. + +```php +Texy\Configurator::safeMode($texy); +``` + +**Co dělá:** +- Zakáže třídy a ID (`$allowedClasses = NONE`) +- Zakáže inline styly (`$allowedStyles = NONE`) +- Povolí jen bezpečné HTML značky: + +```php +[ + 'a' => ['href', 'title'], + 'abbr' => ['title'], + 'b' => [], + 'br' => [], + 'cite' => [], + 'code' => [], + 'em' => [], + 'i' => [], + 'strong' => [], + 'sub' => [], + 'sup' => [], + 'q' => [], + 'small' => [], +] +``` + +- Filtruje URL schémata (jen `http:`, `https:`, `ftp:`, `mailto:`) +- Zakáže obrázky +- Zakáže definice referencí +- Zakáže HTML komentáře +- Přidá `rel="nofollow"` ke všem odkazům + + +disableLinks() - Vypnout odkazy .{toc: disableLinks()} +------------------------------------------------------ + +Zakáže všechny typy odkazů. + +```php +Texy\Configurator::disableLinks($texy); +``` + +**Co dělá:** +- Zakáže všechny typy odkazů (`link/reference`, `link/email`, `link/url`, `link/definition`) +- Zakáže odkazy ve frázích (`phraseModule->linksAllowed = false`) +- Odebere `<a>` z povolených značek + + +disableImages() - Vypnout obrázky .{toc: disableImages()} +--------------------------------------------------------- + +Zakáže všechny typy obrázků. + +```php +Texy\Configurator::disableImages($texy); +``` + +**Co dělá:** +- Zakáže obrázky (`image`, `figure`, `image/definition`) +- Odebere `<img>`, `<object>`, `<embed>`, `<applet>` z povolených značek + + +Bezpečnost +========== + +Texy bere bezpečnost vážně, ale jeho ochrany proti běžným útokům aktivuje bezpečný režim - zapněte ho přes `Configurator::safeMode()`, kdykoli zpracováváte nedůvěryhodný vstup. + + +Ochrana proti XSS +----------------- + +Cross-Site Scripting (XSS) je útok, kdy útočník vloží škodlivý JavaScript do stránky. + +**Příklad útoků, které bezpečný režim zablokuje:** + +```texy +Pokus o útok: <script>alert('XSS')</script> + +Pokus o útok: <img src=x onerror="alert('XSS')"> + +Pokus o útok: "klikni":javascript:alert('XSS') + +Pokus o útok: [* image.jpg onload="alert('XSS')" *] +``` + +V bezpečném režimu Texy: +- **Validuje HTML** - odstraní nepovolené značky a atributy +- **Filtruje URL** - povolí jen bezpečná schémata (`http:`, `https:`, `mailto:`, `ftp:`) +- **Escapuje obsah** - správně escapuje text v atributech +- **Sanitizuje atributy** - odstraní event handlery (`onclick`, `onerror`, ...) + +```php +$texy = new Texy\Texy; +Texy\Configurator::safeMode($texy); + +$input = '<script>alert("XSS")</script>'; +$output = $texy->process($input); + +// Výstup: prázdný (script tag odstraněn) +``` + + +Validace URL +------------ + +Texy kontroluje URL ve všech odkazech a obrázcích: + +```php +$texy = new Texy\Texy; + +// Nastavit povolená schémata (výchozí v safeMode) +$texy->urlSchemeFilters[Texy\Texy::FILTER_ANCHOR] = + '#https?:|ftp:|mailto:#Ai'; +$texy->urlSchemeFilters[Texy\Texy::FILTER_IMAGE] = + '#https?:#Ai'; +``` + +**Příklady blokovaných URL:** + +```texy +"útok":javascript:alert('XSS') // blokováno +"útok":data:text/html,<script> // blokováno +[* javascript:alert() *] // blokováno +``` + + +Filtrování HTML značek +---------------------- + +Kontrola přes `$allowedTags`: + +```php +$texy = new Texy\Texy; + +// Povolit jen bezpečné značky +$texy->allowedTags = [ + 'p' => [], + 'strong' => [], + 'em' => [], + 'a' => ['href', 'title'], // jen tyto atributy +]; + +$input = '<p>Text <script>alert()</script></p>'; +$output = $texy->process($input); + +// Výstup: <p>Text alert()</p> +// (script tag odstraněn) +``` + + +Praktický příklad +----------------- + +```php +function processComment(string $userInput): string +{ + $texy = new Texy\Texy; + + // Bezpečný režim + Texy\Configurator::safeMode($texy); + + // Dodatečná omezení + $texy->allowed['link/url'] = false; // zakázat auto-linky + $texy->allowed['html/tag'] = false; // zakázat HTML + + // Zpracovat + return $texy->process($userInput); +} + +// Použití +$comment = $_POST['comment']; +$html = processComment($comment); +echo $html; // bezpečný výstup +``` + + +Best practices +-------------- + +1. **Vždy použijte safeMode()** pro uživatelský obsah +2. **Validujte vstup** před předáním Texy (délka, formát) +3. **Limitujte HTML značky** podle potřeby +4. **Kontrolujte výstup** - i když je Texy bezpečné, kontrola navíc nikdy neuškodí +5. **Logujte podezřelé pokusy** - může vám to pomoci identifikovat útočníky + +```php +$texy = new Texy\Texy; +Texy\Configurator::safeMode($texy); + +// Logování +$texy->addHandler('htmlTag', function($invocation, $el, $isStart) { + if ($el->getName() === 'script') { + error_log('XSS attempt detected!'); + } + return $invocation->proceed(); +}); +``` diff --git a/texy/cs/custom-handlers.texy b/texy/cs/custom-handlers.texy new file mode 100644 index 0000000000..611c2f09e2 --- /dev/null +++ b/texy/cs/custom-handlers.texy @@ -0,0 +1,850 @@ +Úprava chování prvků +******************** + +.[perex] +Tato kapitola popisuje, jak můžete změnit chování **existujících prvků** v Texy - například upravit, jak se zpracovávají obrázky, odkazy nebo formátování. Pokud chcete přidat **zcela novou syntaxi**, kterou Texy standardně nezná, přečtěte si kapitolu [Přidání vlastní syntaxe |custom-syntax]. + +Představte si, že chcete, aby standardní syntaxe pro obrázky `[* URL *]` rozpoznávala speciální adresu `[* youtube:dQw4w9WgXcQ *]` a místo běžného obrázku vytvořila embedded přehrávač. + +Nebo chcete obarvovat výpisy zdrojového kódu pomocí syntax highlighteru. A tak dále. Přesně k tomu slouží **element handlery** - funkce, které Texy volá při zpracování konkrétních prvků. Například zaregistrujete handler pro element `image`, který zkontroluje URL, a pokud začíná `youtube:`, vrátí iframe místo standardního obrázku. Neměníte syntaxi, jen upravujete, co se s nalezenou konstrukcí stane. + + +Elementy a jejich handlery +========================== + +V terminologii Texy je **element** název pro typ prvku, který může být v dokumentu zpracován. Například `image` je element pro obrázky, `linkURL` pro odkazy, viz [#výchozí elementy]. Každý element má svůj **výchozí handler**, který je implementován v příslušném modulu a stará se o standardní zpracování. + +Když napíšete v textu `[* image.jpg *]`, parser najde tuto syntaxi, vytvoří objekt `Texy\Image` s daty o obrázku a zavolá všechny handlery zaregistrované pro element `image`. Pokud žádný vlastní handler není, zavolá se pouze výchozí handler z `ImageModule`, který vytvoří HTML tag `<img>`. + +Handler zaregistrujete voláním metody `addHandler()`: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + // zde bude vaše logika +}); +``` + +První parametr je název elementu, druhý je callback funkce. Callback dostává jako první parametr vždy objekt `Texy\HandlerInvocation`, následující parametry jsou specifické pro daný element. + +.[note] +Podrobné vysvětlení všech typů handlerů najdete v kapitole [Architektura a principy |architecture]. + + +Jak funguje zpracování +====================== + +Když Texy potřebuje zpracovat element, vytvoří objekt `HandlerInvocation` obsahující všechny zaregistrované handlery pro tento typ prvku. **Váš handler se zavolá jako první** a může: + +- **Delegovat** na další handler voláním `$invocation->proceed()` +- **Upravit vstup** voláním `proceed()` s modifikovanými parametry +- **Upravit výstup** zpracováním výsledku z `proceed()` +- **Přerušit řetěz** vrácením vlastního výsledku bez volání `proceed()` + +Metoda `proceed()` posune zpracování na další handler v řetězu. Pokud už žádný vlastní handler není, zavolá se výchozí implementace z modulu. To znamená, že váš handler má absolutní kontrolu - může rozhodnout, zda se vůbec zavolá výchozí logika. + +Tento mechanismus se nazývá **chain of responsibility** (řetěz zodpovědnosti): + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + // 1. Upravíme vstupní data před zpracováním + $image->modifier->title = 'Modified title'; + + // 2. Zavoláme další handler nebo výchozí zpracování + $element = $invocation->proceed($image, $link); + + // 3. Upravíme výsledný HTML element + $element->attrs['loading'] = 'lazy'; + + return $element; +}); +``` + +Pořadí vykonávání je od **posledně registrovaného k prvnímu**. Pokud modul zaregistruje svůj výchozí handler při konstrukci a vy pak zaregistrujete vlastní handler, váš handler se zavolá první. To vám umožňuje přepsat nebo obalit výchozí chování. + + +Výchozí elementy +================ + +Texy poskytuje několik předpřipravených elementů, pro které můžete registrovat vlastní handlery. Zde je jejich seznam s parametry, které dostává handler. + + +image +----- + +Zpracovává obrázky. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +): Texy\HtmlElement|string|null +``` + +Parametr `$image` obsahuje URL, rozměry a modifikátory. Parametr `$link` je zadán, pokud je obrázek odkazem (syntaxe `[* img *]:url`). + + +linkReference +------------- + +Zpracovává referenční odkazy typu `[ref]`. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, + string $content, +): Texy\HtmlElement|string|null +``` + +Parametr `$link` obsahuje URL a modifikátory načtené z definice reference. Parametr `$content` je HTML obsah odkazu (již zpracovaný parsováním inline syntaxí). + + +linkEmail +--------- + +Zpracovává automaticky rozpoznané emailové adresy v textu. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, +): Texy\HtmlElement|string|null +``` + +Parametr `$link` obsahuje emailovou adresu v property `URL`. + + +linkURL +------- + +Zpracovává automaticky rozpoznané URL v textu. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, +): Texy\HtmlElement|string|null +``` + +Parametr `$link` obsahuje nalezenou URL. + + +phrase +------ + +Zpracovává inline formátování. + +```php +function( + Texy\HandlerInvocation $invocation, + string $phrase, + string $content, + Texy\Modifier $modifier, + ?Texy\Link $link, +): Texy\HtmlElement|string|null +``` + +Parametr `$phrase` je název syntaxe jako `phrase/strong` nebo `phrase/em`. Parametr `$content` je text uvnitř formátování. Parametr `$modifier` obsahuje CSS třídy, styly a další modifikátory. Parametr `$link` je zadán, pokud má formátování připojený odkaz. + + +newReference +------------ + +Volá se, když parser najde referenci, která není definovaná. + +```php +function( + Texy\HandlerInvocation $invocation, + string $name, +): Texy\HtmlElement|string|null +``` + +Parametr `$name` je název reference. Handler může vytvořit odkaz dynamicky nebo vrátit `null` pro odmítnutí. + + +htmlComment +----------- + +Zpracovává HTML komentáře. + +```php +function( + Texy\HandlerInvocation $invocation, + string $content, +): string +``` + +Parametr `$content` je text mezi `<!--` a `-->`. + + +htmlTag +------- + +Zpracovává HTML tagy v textu. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\HtmlElement $el, + bool $isStart, + ?bool $forceEmpty, +): Texy\HtmlElement|string|null +``` + +Parametr `$el` je element s názvem a atributy. Parametr `$isStart` určuje, zda jde o otevírací tag. Parametr `$forceEmpty` vynutí prázdný element. + + +script +------ + +Zpracovává skripty `{{command: args}}`. + +```php +function( + Texy\HandlerInvocation $invocation, + string $command, + array $args, + ?string $raw, +): Texy\HtmlElement|string|null +``` + +Parametr `$command` je název příkazu. Parametr `$args` je pole argumentů. Parametr `$raw` je původní neparsovaný řetězec argumentů. + + +figure +------ + +Zpracovává obrázky s popiskou. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, + string $content, + Texy\Modifier $modifier, +): Texy\HtmlElement|null +``` + +Parametr `$content` je text popisky pod obrázkem. + + +heading +------- + +Zpracovává nadpisy. + +```php +function( + Texy\HandlerInvocation $invocation, + int $level, + string $content, + Texy\Modifier $modifier, + bool $isSurrounded, +): Texy\HtmlElement +``` + +Parametr `$level` je úroveň nadpisu (0-6). Parametr `$content` je text nadpisu. Parametr `$isSurrounded` určuje, zda jde o ohraničený nadpis (`###`) nebo podtržený. + + +horizline +--------- + +Zpracovává horizontální čáry. + +```php +function( + Texy\HandlerInvocation $invocation, + string $type, + Texy\Modifier $modifier, +): Texy\HtmlElement +``` + +Parametr `$type` je řetězec znaků použitých pro čáru (`---` nebo `***`). + + +block +----- + +Zpracovává speciální bloky `/--type` až `\--`. + +```php +function( + Texy\HandlerInvocation $invocation, + string $blocktype, + string $content, + ?string $param, + Texy\Modifier $modifier, +): Texy\HtmlElement|string|null +``` + +Parametr `$blocktype` je typ bloku s prefixem `block/`, např. `block/code` nebo `block/html`. Parametr `$content` je obsah bloku. Parametr `$param` je volitelný parametr za typem (např. jazyk u kódu). + + +emoticon +-------- + +Zpracovává emotikony (smajlíky). + +```php +function( + Texy\HandlerInvocation $invocation, + string $emoticon, + string $raw, +): Texy\HtmlElement|string +``` + +Parametr `$emoticon` je rozpoznaný emotikon (např. `:-)` nebo `:-(`). Parametr `$raw` je původní text včetně případných opakujících se znaků (např. `:-)))))`). + +.[note] +Emotikony jsou ve výchozím nastavení **vypnuté**. Zapnete je pomocí `$texy->allowed['emoticon'] = true;` + + +Výchozí eventy +============== + +Texy poskytuje několik předpřipravených eventů, pro které můžete registrovat handlery. Říká se jim **notification handlery**. Na rozdíl od element handlerů tyto handlery **nic nevrací**. Používají se pro vedlejší efekty jako logování, sběr statistik nebo úpravy již vytvořeného DOM stromu. + + +beforeParse +----------- + +Volá se před začátkem parsování textu. Umožňuje provést předzpracování nebo načíst definice. + +```php +function( + Texy\Texy $texy, + string &$text, + bool $isSingleLine, +): void +``` + +Parametr `$text` je předán referencí, takže ho můžete upravit. Parametr `$isSingleLine` určuje, zda se parsuje jeden řádek nebo celý dokument. + + +afterParse +---------- + +Volá se po dokončení parsování, před konverzí DOM stromu na HTML. Umožňuje upravit vytvořený DOM. + +```php +function( + Texy\Texy $texy, + Texy\HtmlElement $DOM, + bool $isSingleLine, +): void +``` + +Parametr `$DOM` je kořenový element dokumentu, který můžete procházet a upravovat. + + +afterList +--------- + +Volá se po vytvoření seznamu (číslovaného nebo nečíslovaného). + +```php +function( + Texy\BlockParser $parser, + Texy\HtmlElement $element, + Texy\Modifier $modifier, +): void +``` + +Parametr `$element` je vytvořený element `<ul>` nebo `<ol>`. Parametr `$modifier` obsahuje modifikátory aplikované na celý seznam. + + +afterDefinitionList +------------------- + +Volá se po vytvoření definičního seznamu. + +```php +function( + Texy\BlockParser $parser, + Texy\HtmlElement $element, + Texy\Modifier $modifier, +): void +``` + +Parametr `$element` je vytvořený element `<dl>`. + + +afterTable +---------- + +Volá se po vytvoření tabulky. + +```php +function( + Texy\BlockParser $parser, + Texy\HtmlElement $element, + Texy\Modifier $modifier, +): void +``` + +Parametr `$element` je vytvořený element `<table>`. + + +afterBlockquote +--------------- + +Volá se po vytvoření citace. + +```php +function( + Texy\BlockParser $parser, + Texy\HtmlElement $element, + Texy\Modifier $modifier, +): void +``` + +Parametr `$element` je vytvořený element `<blockquote>`. + + +Základní použití +================ + +Nejjednodušší element handler jen deleguje na výchozí zpracování: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + return $invocation->proceed(); +}); +``` + +Tento handler nic nemění, ale ukazuje základní kostru. Všechny parametry předá dál a vrátí výsledek. + + +Úprava vstupních dat +-------------------- + +Handler může upravit data před jejich zpracováním: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + // přidáme default rozměry, pokud nejsou zadané + $image->width ??= 800; + $image->height ??= 600; + return $invocation->proceed(); +}); +``` + +Změny provedené na objektech `$image` nebo `$link` se projeví v dalším zpracování, včetně výchozího handleru. + + +Úprava výstupního elementu +-------------------------- + +Handler může upravit HTML element vrácený z `proceed()`: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + $element = $invocation->proceed(); + + if ($element) { + // přidáme lazy loading + $element->attrs['loading'] = 'lazy'; + + // přidáme CSS třídu + $element->attrs['class'][] = 'responsive'; + } + + return $element; +}); +``` + + +Podmíněné zpracování +-------------------- + +Handler může zpracovat pouze určité případy a ostatní delegovat: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + // speciální zpracování pro YouTube videa + if (str_starts_with($image->URL, 'youtube:')) { + $id = substr($image->URL, 8); + $iframe = sprintf( + '<iframe src="https://youtube.com/embed/%s"></iframe>', + htmlspecialchars($id) + ); + return $invocation->getTexy() + ->protect($iframe, Texy\Texy::CONTENT_BLOCK); + } + + // ostatní obrázky zpracujeme standardně + return $invocation->proceed(); +}); +``` + + +Přerušení zpracování +-------------------- + +Handler může odmítnout zpracování vrácením `null`: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + // zakážeme externí obrázky + if (str_contains($image->URL, '://')) { + return null; + } + + return $invocation->proceed(); +}); +``` + + +Praktické příklady +================== + +Následující příklady ukazují reálné případy použití element handlerů. + + +YouTube embed +------------- + +Převod speciální syntaxe na embedded video: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + if (str_starts_with($image->URL, 'youtube:')) { + $id = substr($image->URL, 8); + $width = $image->width ?: 560; + $height = $image->height ?: 315; + + $iframe = sprintf( + '<iframe width="%d" height="%d" ' + . 'src="https://youtube.com/embed/%s" ' + . 'frameborder="0" allowfullscreen></iframe>', + $width, $height, htmlspecialchars($id) + ); + + $texy = $invocation->getTexy(); + return $texy->protect($iframe, $texy::CONTENT_BLOCK); + } + + return $invocation->proceed(); +}); +``` + +Použití v textu: + +```texy +[* youtube:dQw4w9WgXcQ 640x360 *] +``` + + +Galerie obrázků +--------------- + +Obalení obrázků do speciálního divu pro lightbox: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + $element = $invocation->proceed(); + + // pokud má obrázek třídu 'gallery' + if (isset($image->modifier->classes['gallery'])) { + // obalíme do divu s lightbox atributy + $wrapper = new Texy\HtmlElement('div'); + $wrapper->attrs['class'][] = 'lightbox-item'; + $wrapper->attrs['data-src'] = $image->URL; + $wrapper->add($element); + + return $wrapper; + } + + return $element; +}); +``` + +Použití: + +```texy +[* image.jpg .[gallery] *] +``` + + +Validace odkazů +--------------- + +Kontrola, zda odkazy nalezené v textu vedou na povolené domény: + +```php +$allowedDomains = ['example.com', 'trusted.org']; + +$texy->addHandler('linkURL', function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, +) use ($allowedDomains) { + $host = parse_url($link->URL, PHP_URL_HOST); + + // pokud doména není v whitelistu, nepovolíme odkaz + if ($host && !in_array($host, $allowedDomains, true)) { + return null; + } + + return $invocation->proceed(); +}); +``` + + +Automatické rel="nofollow" +-------------------------- + +Přidání `nofollow` všem externím odkazům nalezeným v textu: + +```php +$texy->addHandler('linkURL', function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, +) { + $element = $invocation->proceed(); + + // pokud odkaz obsahuje // (tedy je externí) + if (str_contains($link->URL, '://')) { + $element->attrs['rel'] = 'nofollow'; + } + + return $element; +}); +``` + + +Syntax highlighting +------------------- + +Integrace knihovny pro zvýraznění syntaxe: + +```php +$texy->addHandler('block', function( + Texy\HandlerInvocation $invocation, + string $blocktype, + string $content, + ?string $param, + Texy\Modifier $modifier, +) { + // zpracujeme pouze bloky typu 'code' + if ($blocktype !== 'block/code') { + return $invocation->proceed(); + } + + // aplikujeme syntax highlighting + $highlighter = new MyHighlighter(); + $highlighted = $highlighter->highlight($content, $param); + + $el = new Texy\HtmlElement('pre'); + $modifier->decorate($invocation->getTexy(), $el); + $el->attrs['class'][] = 'language-' . $param; + + $code = new Texy\HtmlElement('code'); + $code->add($highlighted); + $el->add($code); + + return $el; +}); +``` + + +Lazy loading +------------ + +Projdeme všechny obrázky a přidáme lazy loading: + +```php +$texy->addHandler('afterParse', function( + Texy\Texy $texy, + Texy\HtmlElement $DOM, + bool $isSingleLine, +) { + foreach ($DOM->getIterator() as $child) { + if ($child instanceof Texy\HtmlElement + && $child->getName() === 'img' + ) { + $child->attrs['loading'] = 'lazy'; + } + } +}); +``` + + +Logování použitých prvků +------------------------ + +Sběr statistik o použitých prvcích v dokumentu: + +```php +$stats = []; + +$texy->addHandler('beforeParse', function( + Texy\Texy $texy, + string &$text, + bool $isSingleLine, +) use (&$stats) { + $stats = ['images' => 0, 'links' => 0, 'headings' => 0]; +}); + +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) use (&$stats) { + $stats['images']++; + return $invocation->proceed(); +}); + +$texy->addHandler('linkURL', function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, +) use (&$stats) { + $stats['links']++; + return $invocation->proceed(); +}); + +$texy->addHandler('heading', function( + Texy\HandlerInvocation $invocation, + int $level, + string $content, + Texy\Modifier $modifier, + bool $isSurrounded, +) use (&$stats) { + $stats['headings']++; + return $invocation->proceed(); +}); +``` + + +Pomocné třídy +============= + +Při práci s handlery budete pracovat s několika důležitými třídami. Zde je jejich přehled s nejdůležitějšími vlastnostmi. + + +Texy\Image +---------- + +Reprezentuje obrázek s jeho parametry: + +```php +$image->URL; // ?string - cesta k obrázku +$image->linkedURL; // ?string - URL odkazu (pokud je obrázek odkazem) +$image->width; // ?int - šířka v pixelech +$image->height; // ?int - výška v pixelech +$image->asMax; // bool - zda jsou rozměry maximální +$image->modifier; // Modifier - CSS třídy, styly, atributy +$image->name; // ?string - název reference +``` + + +Texy\Link +--------- + +Reprezentuje odkaz s jeho parametry: + +```php +$link->URL; // ?string - cílová URL +$link->raw; // string - původní URL (před normalizací) +$link->modifier; // Modifier - CSS třídy, styly, atributy +$link->type; // int - typ odkazu (COMMON, BRACKET, IMAGE) +$link->label; // ?string - text odkazu (u referencí) +$link->name; // ?string - název reference +``` + +Konstanty pro typ odkazu: + +```php +Texy\Link::COMMON; // běžný odkaz +Texy\Link::BRACKET; // referenční odkaz [ref] +Texy\Link::IMAGE; // odkaz z obrázku [* img *] +``` + + +Texy\HtmlElement +---------------- + +Reprezentuje HTML element s jeho atributy a obsahem: + +```php +$el = new Texy\HtmlElement('div'); + +// práce s názvem elementu +$el->getName(); // vrací 'div' +$el->setName('section'); // změní na 'section' + +// práce s atributy +$el->attrs['id'] = 'main'; +$el->attrs['class'][] = 'container'; +$el->attrs['style']['color'] = 'red'; + +// práce s obsahem +$el->setText('text'); // nastaví textový obsah +$el->getText(); // vrací textový obsah +$el->add($child); // přidá potomka +$el->insert(0, $child); // vloží potomka na pozici + +// parsování obsahu +$el->parseLine($texy, $text); // parsuje inline text +$el->parseBlock($texy, $text); // parsuje blokový text + +// konverze na HTML +$el->toString($texy); // interní reprezentace +$el->toHtml($texy); // finální HTML +``` + + +Texy\Modifier +------------- + +Reprezentuje modifikátory CSS tříd, stylů a atributů: + +```php +$mod->id; // ?string - HTML id +$mod->classes; // array - pole CSS tříd +$mod->styles; // array - pole CSS stylů +$mod->attrs; // array - HTML atributy +$mod->hAlign; // ?string - horizontální zarovnání (left, right, center, justify) +$mod->vAlign; // ?string - vertikální zarovnání (top, middle, bottom) +$mod->title; // ?string - title atribut nebo alt pro obrázky + +// aplikace modifikátoru na element +$mod->decorate($texy, $element); +``` diff --git a/texy/cs/custom-syntax.texy b/texy/cs/custom-syntax.texy new file mode 100644 index 0000000000..d874fc4f96 --- /dev/null +++ b/texy/cs/custom-syntax.texy @@ -0,0 +1,519 @@ +Přidání vlastní syntaxe +*********************** + +.[perex] +Tato kapitola popisuje, jak přidat do Texy **zcela nové markup konstrukce**, které standardně neexistují. Pokud chcete pouze změnit chování existujících prvků (například upravit zpracování obrázků nebo odkazů), přečtěte si kapitolu [Úprava chování prvků |custom-handlers]. + +Představte si, že chcete v dokumentaci automaticky vytvářet odkazy na uživatelské profily zápisem `@@username`. Nebo potřebujete speciální bloky pro upozornění typu `:::warning`. Texy tyto konstrukce nezná a nemůžete je vytvořit úpravou existujících prvků. + +Vlastní syntaxe vám umožní definovat nové markup konstrukce. Zadáte, jak má konstrukce vypadat (pomocí regulárního výrazu), a napíšete funkci, která ji zpracuje. Texy pak vaši syntaxi rozpozná stejně jako své standardní konstrukce. + + +Registrace syntaxe +================== + +Texy poskytuje dvě metody pro registraci vlastní syntaxe podle toho, zda jde o inline nebo blokový prvek. + + +Line syntaxe +------------ + +Line syntaxe slouží pro inline konstrukce uvnitř řádků textu. Registrujete ji metodou `registerLinePattern()`: + +```php +$texy->registerLinePattern( + callable $handler, + string $pattern, + string $name, + ?string $againTest = null, +); +``` + +**Parametr `$handler`** je callback funkce, která se zavolá při nálezu syntaxe. Může to být název funkce, anonymní funkce nebo pole `[$object, 'method']`. + +**Parametr `$pattern`** je regulární výraz (PCRE), který definuje, jak vaše syntaxe vypadá v textu. Pattern by **neměl být kotvený** na začátek řádku (`^`), protože se hledá kdekoliv v textu. Použijte capturing groups pro zachycení dat, která potřebujete zpracovat. + +**Parametr `$name`** je unikátní název syntaxe. Používá se v poli `$texy->allowed` pro zapnutí/vypnutí a předává se do handleru pro identifikaci. Doporučujeme používat prefixový styl jako `custom/username` nebo `myapp/profile`. + +**Parametr `$againTest`** je volitelný regex pro optimalizaci. Pokud hlavní pattern na dané pozici neodpovídá, Texy podle `$againTest` rozhodne, zda má smysl pattern hledat dál v textu; jakmile `$againTest` už nikde napřed neodpovídá, pattern se z hledání vyřadí. To výrazně zrychlí zpracování, pokud máte složitý pattern a používá se jen zřídka. + +Příklad registrace: + +```php +$texy->registerLinePattern( + 'usernameHandler', + '#@@([a-z0-9_]+)#i', + 'custom/username', +); +``` + + +Block syntaxe +------------- + +Block syntaxe slouží pro víceřádkové blokové konstrukce. Registrujete ji metodou `registerBlockPattern()`: + +```php +$texy->registerBlockPattern( + callable $handler, + string $pattern, + string $name, +); +``` + +Parametry `$handler` a `$name` mají stejný význam jako u line syntaxí. + +**Parametr `$pattern`** je regulární výraz, který **musí být kotvený** na začátek řádku (`^`) a často i na konec (`$`). BlockParser automaticky přidá modifikátor `m` (multiline), takže ho do patternu nepřidávejte (kotvu `^` si píšete sami). Pattern by měl odpovídat celému bloku nebo alespoň jeho začátku. + +Příklad registrace: + +```php +$texy->registerBlockPattern( + 'alertHandler', + '#^:::(warning|info|danger)\n(.+)$#s', + 'custom/alert', +); +``` + + +Syntax handler +============== + +Syntax handler je funkce volaná parserem, když najde výskyt vaší syntaxe v textu. Jeho úkolem je zpracovat nalezená data a vrátit HTML element nebo řetězec. + +Podrobné vysvětlení role syntax handleru v architektuře Texy najdete v kapitole [Architektura a principy |architecture#syntax-handler]. + + +Pro line syntaxe +---------------- + +Signatura syntax handleru pro line syntaxe: + +```php +function( + Texy\LineParser $parser, + array $matches, + string $name, +): Texy\HtmlElement|string|null +``` + +**Parametr `$parser`** poskytuje přístup k parseru a Texy objektu. Nejčastěji použijete `$parser->getTexy()` pro získání Texy instance. + +**Parametr `$matches`** obsahuje výsledky regex matche. `$matches[0]` je celý nalezený řetězec, `$matches[1]`, `$matches[2]` atd. jsou capturing groups z vašeho patternu. + +**Parametr `$name`** je název syntaxe, který jste zadali při registraci. Užitečné, pokud jeden handler zpracovává více syntaxí. + +**Návratová hodnota** může být `Texy\HtmlElement` pro strukturovaný HTML výstup, `string` pro přímý HTML kód (který musíte ochránit metodou `protect()`), nebo `null` pro odmítnutí zpracování. + +Handler může nastavit `$parser->again = true`, pokud chce, aby se obsah vytvořeného elementu znovu parsoval pro nalezení vnořených syntaxí. + + +Pro block syntaxe +----------------- + +Signatura syntax handleru pro block syntaxe: + +```php +function( + Texy\BlockParser $parser, + array $matches, + string $name, +): Texy\HtmlElement|string|null +``` + +Parametry mají stejný význam jako u line syntaxí, jen dostáváte `Texy\BlockParser` místo `LineParser`. + +BlockParser poskytuje metody pro práci s víceřádkovými strukturami: + +- **`$parser->next($pattern, &$matches)`** - porovná další řádek s patternem a vrátí true/false +- **`$parser->moveBackward($lines)`** - vrátí se o zadaný počet řádků zpět +- **`$parser->isIndented()`** - vrací true, pokud je aktuální blok odsazený + + +LineParser API +============== + +Při práci s line syntaxemi máte k dispozici několik užitečných vlastností a metod. + +**Property `$again`** řídí, zda se má právě zpracovaná syntaxe hledat znovu na stejné pozici po zpracování. Výchozí hodnota je `false`. Nastavte na `true`, pokud vytváříte element s obsahem, který může obsahovat další syntaxe: + +```php +function( + Texy\LineParser $parser, + array $matches, + string $name, +): Texy\HtmlElement +{ + $el = new Texy\HtmlElement('span'); + $el->setText($matches[1]); + + // obsah může obsahovat další formátování + $parser->again = true; + + return $el; +} +``` + +**Metoda `getTexy()`** vrací instanci Texy objektu, což potřebujete pro práci s `protect()` nebo přístup ke konfiguraci. + + +BlockParser API +=============== + +Při práci s block syntaxemi máte k dispozici metody pro práci s víceřádkovými strukturami. + +**Metoda `next($pattern, &$matches)`** zkusí porovnat další řádek v textu se zadaným patternem. Pokud uspěje, naplní `$matches` výsledkem a posune interní pozici za tento řádek. Vrací `true` při úspěchu, `false` při neúspěchu: + +```php +while ($parser->next('#^\-\s+(.+)$#', $matches)) { + // zpracuj další položku seznamu + $item = $matches[1]; +} +``` + +**Metoda `moveBackward($lines = 1)`** vrátí interní pozici o zadaný počet řádků zpět. Užitečné, když váš pattern zabral víc než začátek bloku a chcete se vrátit na začátek: + +```php +// pattern zabral 3 řádky, ale chceme číst od prvního +$parser->moveBackward(2); +``` + +**Metoda `isIndented()`** vrací `true`, pokud je aktuální blok odsazený (začíná mezerou nebo tabulátorem). To naznačuje, že jde o vnořený obsah. + + +Praktické příklady +================== + +Následující příklady ukazují reálné případy použití vlastních syntaxí. + + +Uživatelské profily +------------------- + +Automatické vytváření odkazů na profily zápisem `@@username`: + +```php +$texy->registerLinePattern( + function( + Texy\LineParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $username = $matches[1]; + + $el = new Texy\HtmlElement('a'); + $el->attrs['href'] = '/user/' . urlencode($username); + $el->attrs['class'][] = 'user-profile'; + $el->setText('@' . $username); + + return $el; + }, + '#@@([a-z0-9_]+)#i', + 'custom/username' +); +``` + +Použití v textu: + +```texy +Podívejte se na profil @@johndoe nebo @@jane_smith. +``` + + +Alert boxy +---------- + +Speciální bloky pro upozornění s různými typy: + +```php +$texy->registerBlockPattern( + function( + Texy\BlockParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $type = $matches[1]; // warning, info, danger + $content = $matches[2]; + + $el = new Texy\HtmlElement('div'); + $el->attrs['class'][] = 'alert'; + $el->attrs['class'][] = 'alert-' . $type; + + $texy = $parser->getTexy(); + $el->parseBlock($texy, trim($content)); + + return $el; + }, + '#^:::(warning|info|danger)\n(.+?)(?=\n:::|$)#s', + 'custom/alert' +); +``` + +Použití v textu: + +```texy +:::warning +Toto je důležité upozornění! +::: + +:::info +Pro informaci: aktualizace proběhne zítra. +::: +``` + + +Hashtagy +-------- + +Automatické vytváření odkazů z hashtagů: + +```php +$texy->registerLinePattern( + function( + Texy\LineParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $tag = $matches[1]; + + $el = new Texy\HtmlElement('a'); + $el->attrs['href'] = '/tag/' . urlencode($tag); + $el->attrs['class'][] = 'hashtag'; + $el->setText('#' . $tag); + + return $el; + }, + '#\#([a-z0-9_]+)#i', + 'custom/hashtag', + '#\##' // optimalizace - hledej jen pokud je # v textu +); +``` + +Použití: + +```texy +Článek o #php a #webdesign. +``` + + +Zkratky +------- + +Automatické rozbalení zkratek s vysvětlením: + +```php +$abbreviations = [ + 'HTML' => 'HyperText Markup Language', + 'CSS' => 'Cascading Style Sheets', + 'PHP' => 'PHP: Hypertext Preprocessor', +]; + +$texy->registerLinePattern( + function( + Texy\LineParser $parser, + array $matches, + string $name + ) use ($abbreviations): ?Texy\HtmlElement + { + $abbr = $matches[1]; + + if (!isset($abbreviations[$abbr])) { + return null; // neznámá zkratka + } + + $el = new Texy\HtmlElement('abbr'); + $el->attrs['title'] = $abbreviations[$abbr]; + $el->setText($abbr); + + return $el; + }, + '#\b([A-Z]{2,})\b#', + 'custom/abbreviation' +); +``` + + +Inline ikony +------------ + +Vkládání ikon pomocí speciální syntaxe: + +```php +$texy->registerLinePattern( + function( + Texy\LineParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $icon = $matches[1]; + + $el = new Texy\HtmlElement('i'); + $el->attrs['class'][] = 'icon'; + $el->attrs['class'][] = 'icon-' . $icon; + $el->attrs['aria-hidden'] = 'true'; + + return $el; + }, + '#:icon-([a-z-]+):#', + 'custom/icon' +); +``` + +Použití: + +```texy +Klikněte na tlačítko :icon-download: pro stažení. +``` + + +Poznámkový blok +--------------- + +Blok pro poznámky pod čarou: + +```php +$texy->registerBlockPattern( + function( + Texy\BlockParser $parser, + array $matches, + string $name + ): Texy\HtmlElement + { + $parser->moveBackward(); + + $content = ''; + while ($parser->next('#^NOTE:\s*(.+)$#', $matches)) { + $content .= $matches[1] . "\n"; + } + + $el = new Texy\HtmlElement('aside'); + $el->attrs['class'][] = 'note'; + + $texy = $parser->getTexy(); + $el->parseBlock($texy, trim($content)); + + return $el; + }, + '#^NOTE:\s*(.+)$#', + 'custom/note' +); +``` + +Použití: + +```texy +NOTE: Toto je důležitá poznámka. +NOTE: Může být víceřádková. +``` + + +Vlastní citace s autorem +------------------------ + +Rozšířená syntaxe pro citace s uvedením autora: + +```php +$texy->registerBlockPattern( + function( + Texy\BlockParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $author = $matches[1]; + $quote = $matches[2]; + + $blockquote = new Texy\HtmlElement('blockquote'); + + $texy = $parser->getTexy(); + $blockquote->parseBlock($texy, trim($quote)); + + $cite = new Texy\HtmlElement('cite'); + $cite->setText($author); + $blockquote->add($cite); + + return $blockquote; + }, + '#^QUOTE\[([^\]]+)\]:\n(.+?)(?=\n\n|$)#s', + 'custom/quote' +); +``` + +Použití: + +```texy +QUOTE[Albert Einstein]: +Fantazie je důležitější než vědění, +protože vědění je omezené. +``` + + +Galerie obrázků +--------------- + +Speciální blok pro vytvoření galerie z více obrázků: + +```php +$texy->registerBlockPattern( + function( + Texy\BlockParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $parser->moveBackward(); + + $gallery = new Texy\HtmlElement('div'); + $gallery->attrs['class'][] = 'gallery'; + + while ($parser->next('#^\[G\]\s*(.+)$#', $matches)) { + $img = new Texy\HtmlElement('img'); + $img->attrs['src'] = trim($matches[1]); + $img->attrs['loading'] = 'lazy'; + $gallery->add($img); + } + + return $gallery; + }, + '#^\[G\]\s*(.+)$#', + 'custom/gallery' +); +``` + +Použití: + +```texy +[G] image1.jpg +[G] image2.jpg +[G] image3.jpg +``` + + +Kolize syntaxí +============== + +Když registrujete vlastní syntaxi, musíte dávat pozor, aby nekolidovala s existujícími syntaxemi Texy nebo s jinými vlastními syntaxemi. + +**Pořadí registrace záleží.** Line syntaxe se hledají v pořadí, jak byly registrovány. Pokud více syntaxí může odpovídat na stejné pozici, vyhrává ta, která byla registrována dříve. Proto registrujte specifičtější syntaxe před obecnějšími. + +**Buďte specifičtí v patterns.** Čím konkrétnější je váš pattern, tím menší je riziko kolize. Pattern `#\#\w+#` zabere i `#hashtag`, což by mohlo kolidovat s nadpisy. Lepší je `#(?<=\s)\#[a-z0-9_]+#i`, který vyžaduje mezeru před hashtagem. + +**Testujte kombinace.** Vyzkoušejte, jak vaše syntaxe funguje v kombinaci s existujícími konstrukcemi. Co se stane, když je váš markup uvnitř odkazu? Co když je uvnitř code bloku? + +**Používejte prefixované názvy.** Místo `username` použijte `custom/username` nebo `myapp/username`. To zabrání konfliktům, pokud by Texy v budoucnu přidalo syntaxi stejného názvu. + + +Best practices +============== + +**Vracejte `null` při neúspěchu.** Pokud handler zjistí, že nemůže nebo nechce zpracovat daný match (například neznámá zkratka), vraťte `null`. Parser pak zkusí další syntaxe. + +**Používejte `protect()` pro HTML.** Pokud vracíte přímo HTML řetězec místo `HtmlElement`, musíte ho ochránit pomocí `$texy->protect($html, Texy::CONTENT_...)`. Jinak bude escapován. + +**Nastavte `$parser->again` správně.** U line syntaxí, které vytvářejí element s textovým obsahem, jenž může obsahovat další syntaxe (formátování, odkazy), nastavte `$parser->again = true`. + +**Respektujte `$texy->allowed`.** Pokud vytváříte modul s více syntaxemi, kontrolujte `$texy->allowed[$name]` před registrací patternu nebo v handleru před zpracováním. diff --git a/texy/cs/develop.texy b/texy/cs/develop.texy new file mode 100644 index 0000000000..70929a3a93 --- /dev/null +++ b/texy/cs/develop.texy @@ -0,0 +1,35 @@ +Pro programátory +**************** + +.[perex] +Vítejte v programátorské dokumentaci Texy! Tato sekce vás provede od základního použití až po pokročilé techniky rozšíření a vlastní syntaxe. + + +[Rychlý start | quickstart] +--------------------------- + +Instalace, první použití a základní konfigurace. Za 5 minut budete mít Texy funkční. + + +[Konfigurace | configuration] +----------------------------- + +Kompletní přehled všech modulů, jejich vlastností a konfiguračních možností. Nastavení bezpečnosti, povolených značek, stylů a tříd. + + +[Úprava chování prvků | custom-handlers] +---------------------------------------- + +Naučte se měnit chování existující syntaxe. YouTube embedování, syntax highlighting, vlastní validace. + + +[Přidání vlastní syntaxe | custom-syntax] +----------------------------------------- + +Vytvoření zcela nových syntaktických prvků. + + +[Architektura a principy | architecture] +---------------------------------------- + +Pochopení toho, jak Texy interně funguje. Průběh parsování, moduly, hledání patternů, mechanismus protect/unprotect. diff --git a/texy/cs/priklady-vyuziti.texy b/texy/cs/priklady-vyuziti.texy deleted file mode 100644 index f71b9616dc..0000000000 --- a/texy/cs/priklady-vyuziti.texy +++ /dev/null @@ -1,26 +0,0 @@ -Příklady využití -**************** - - -Běžní uživatelé .[#bfu] ------------------------ - -Texy původně vznikl jako nástroj, který umožnil i uživatelům neznalým HTML **snadno editovat obsah** webových stránek. Záměrem bylo vytvořit silnou **alternativu k WYSIWYG** editorům, které se v praxi [ukazují jako neefektivní | texy-vs-wysiwyg]. - -Běžný uživatel se může plně věnovat psaní "čistého textu .(například v Poznámkovém bloku)[about]", který si formátuje velmi [přirozeným způsobem | syntax]. A nemusí se téměř nic nového učit. - - -Zkušení pisatelé .[#experienced] --------------------------------- - -Zkušení tvůrci internetového obsahu, jakými jsou například bloggeři, často jazyk HTML znají. Ale psát články přímo v něm je nepříjemné a proto si zjednodušují práci používáním chytrých editorů. Jedním z nich je i Texy Je navíc dostupný i přes webové rozhraní a nabízí **neobvykle vysoký konfort**. - -Díky Texy se autor může plně věnovat obsahu dokumentu a nemusí uvažovat nad HTML nebo typografickou úpravou. Texy dokáže psaní výrazně zjednodušit, přitom pokročilého uživatele nijak neomezuje - nabízí mu **vkládat i HTML značky** a CSS formátování. - - -Komentáře a diskuzní fóra .[#comments] --------------------------------------- - -Texy počítá i s nasazením jako formátovač příspěvků v diskuzních fórech a komentářích. Přispívatelé mohou používat jednotnou a **intuitivní syntaxi** a programátorům **odpadne náročná práce** na vlastním formátovači. - -Tato oblast použití je charakteristická tím, že vyžaduje mnohem **přísnější kontrolu** vstupů. Texy proto obsahuje mechanismy, které zakáží nebo omezí použití určitých HTML značek (a jejich atributů), kaskádových stylů a tříd atd. diff --git a/texy/cs/quickstart.texy b/texy/cs/quickstart.texy new file mode 100644 index 0000000000..4fdab06cc3 --- /dev/null +++ b/texy/cs/quickstart.texy @@ -0,0 +1,205 @@ +Rychlý start +************ + +.[perex] +Naučte se pracovat s Texy za pár minut. Tato stránka vás provede instalací, prvním použitím a základní konfigurací. + + +Instalace +========= + +Texy využívá moderních vlastností PHP a vyžaduje minimálně verzi 8.1. + +Nejjednodušší způsob instalace je přes Composer: + +```bash +composer require texy/texy +``` + +Composer automaticky stáhne Texy a všechny závislosti. + + +První použití +============= + + +Základní zpracování textu +------------------------- + +Vytvoření instance Texy a zpracování textu je extrémně jednoduché: + +```php +require __DIR__ . '/vendor/autoload.php'; + +$texy = new Texy\Texy; + +$text = 'Toto je **tučný text** a toto //kurzíva//.'; +$html = $texy->process($text); + +echo $html; +``` + +Výstup: +```latte +<p>Toto je <strong>tučný text</strong> a toto <em>kurzíva</em>.</p> +``` + +Metoda `process()` zpracuje celý text včetně blokových elementů (odstavce, nadpisy, seznamy, tabulky...). + + +Jednořádkový text +----------------- + +Pokud zpracováváte pouze jednořádkový text bez blokových elementů (například nadpisy v databázi, krátké popisky): + +```php +$texy = new Texy\Texy; + +$text = 'Odkaz na "homepage":https://example.com'; +$html = $texy->processLine($text); + +echo $html; +``` + +Výstup: +```latte +Odkaz na <a href="https://example.com">homepage</a> +``` + +Metoda `processLine()` nezabaluje výstup do odstavce `<p>` a zpracuje pouze inline elementy. + + +Základní konfigurace +==================== + +Texy funguje "out of the box", ale často budete chtít upravit základní nastavení. + + +Nastavení cest k obrázkům +------------------------- + +Pokud používáte relativní cesty k obrázkům, nastavte kořenový adresář: + +```php +$texy = new Texy\Texy; + +// Cesta na webu (přidá se před relativní URL) +$texy->imageModule->root = '/images/'; + +// Fyzická cesta na disku (pro zjištění rozměrů) +$texy->imageModule->fileRoot = __DIR__ . '/public/images/'; +``` + +Teď když napíšete `[* photo.jpg *]`, Texy vygeneruje `<img src="/images/photo.jpg">` a automaticky zjistí rozměry obrázku. + + +Nastavení cest k odkazům +------------------------ + +Podobně můžete nastavit kořenový adresář pro odkazy: + +```php +$texy->linkModule->root = '/articles/'; +``` + + +Povolení a zakázání syntaxí +--------------------------- + +Každou část syntaxe Texy lze vypnout nebo zapnout pomocí pole `$allowed`: + +```php +$texy = new Texy\Texy; + +// Vypnout obrázky +$texy->allowed['image'] = false; + +// Vypnout HTML značky ve vstupu +$texy->allowed['html/tag'] = false; + +// Povolit emotikony (ve výchozím stavu vypnuté) +$texy->allowed['emoticon'] = true; +``` + +Kompletní seznam syntaxí najdete v [konfiguraci | configuration#allowed]. + + +Bezpečný režim pro uživatelský obsah +------------------------------------ + +Pokud zpracováváte obsah od uživatelů (komentáře, příspěvky na fóru), použijte bezpečný režim: + +```php +$texy = new Texy\Texy; +Texy\Configurator::safeMode($texy); + +$userInput = $_POST['comment']; +$html = $texy->process($userInput); +``` + +SafeMode: +- Povolí jen **bezpečné HTML značky** (`<strong>`, `<em>`, `<a>`, ...) +- Zakáže **třídy a ID** +- Zakáže **inline styly** +- Zakáže **obrázky** +- Přidá `rel="nofollow"` k absolutním odkazům +- Filtruje **URL schémata** (jen `http:`, `https:`, `ftp:`, `mailto:`) + +Více o bezpečnosti v kapitole [Konfigurace - Bezpečnost |configuration#Bezpečnost]. + + +Kompletní příklad +================= + +```php +require __DIR__ . '/vendor/autoload.php'; + +$texy = new Texy\Texy; + +// Konfigurace +$texy->imageModule->root = '/images/'; +$texy->linkModule->root = '/'; +$texy->allowed['html/tag'] = false; + +// Text ke zpracování +$text = ' + + +Nadpis článku +============= + +Toto je **úvodní odstavec** s odkazem na "homepage":https://example.com. + +- První položka +- Druhá položka +- Třetí položka + +[* photo.jpg .(Fotografie) *] +'; + +// Zpracování +$html = $texy->process($text); + +// Výstup +echo $html; + +// Dodatečné informace +echo "Titulek stránky: " . $texy->headingModule->title; +print_r($texy->summary['links']); +print_r($texy->summary['images']); +``` + +Po zpracování máte k dispozici: +- `$texy->headingModule->title` - první nadpis (vhodné pro `<title>`) +- `$texy->summary['links']` - pole všech použitých odkazů +- `$texy->summary['images']` - pole všech použitých obrázků + + +Další kroky +=========== + +Teď už víte, jak Texy používat. Pokračujte: + +- **[Konfigurace | configuration]** - podrobné nastavení všech modulů +- **[Syntaxe | syntax]** - naučte se Texy markup +- **[Architektura | architecture]** - pochopte, jak Texy funguje uvnitř diff --git a/texy/cs/syntax-podrobne.texy b/texy/cs/syntax-podrobne.texy deleted file mode 100644 index d10b62068c..0000000000 --- a/texy/cs/syntax-podrobne.texy +++ /dev/null @@ -1,889 +0,0 @@ -Podrobný popis syntaxe -********************** - - -- [#Filozofie] -- [#Odstavce textu] -- [#Titulky] -- [#Horizontální čáry] -- [#Kód] -- [#Vypnutí Texy] -- [#Citace] -- [#Odkazy] -- [#Obrázky] -- [#Fráze] -- [#Přímé HTML] -- [#Seznamy] -- [#Modifikátory] -- [#Typografie] -- [#Rozdělení velmi dlouhých slov] -- [#Tabulky] - - -Filozofie -========= - -Nástroj Texy vznikl proto, aby nezkušeným uživatelům umožnil snadno editovat obsah webových stránek. Proto je i syntaxe maximálně intuitivní. Záměrem je, aby text v čisté (nezformátované) formě byl přehledný a jeho formát tušitelný. - -Dnes Texy výborně slouží i zkušeným znalcům jazyka HTML. Dovoluje volně kombinovat Texy zápis s HTML značkami. Zkušení uživatelé se tedy nemusí učit nový meta-jazyk a plně využít svých znalostí. Texy jim pouze zjednodušuje práci. - -Prvotní logikou syntaxe je **žádnou syntaxi nepoužívat**. Jen psát čistý text. Vkládání rozšířených informací, jako třeba CSS třídy nebo odkazy, nenaruší tok textu. A zapíší se způsobem, který snadno pochopí i netechnicky založení uživatelé. - - -Odstavce textu -============== - -Za odstavec se považuje jeden nebo více bezprostředně za sebou následujících řádků textu. Odstavečky jsou od sebe oddělené prázdným řádkem. - -/--code texy -První odstavec lorem ipsum dolor sit amet. - -Druhý odstavec, který tvoří jeden řádek. -A druhý řádek textu. Texy je spojí. -\-- - -/--texysource -První odstavec lorem ipsum dolor sit amet. - -Druhý odstavec, který tvoří jeden řádek. -A druhý řádek textu. Texy je spojí. -\-- - -*V editačním políčku webové stránky (textarea) není rozdělení odstavce na dva řádky patrné. Proto je i Texy považuje za jeden odstavec.* - -Zalomení řádku v odstavci docílíte vložením jedné mezery vlevo: - -/--code texy -Kdoví jestli - jestli jsou na měsíci vůbec nějaký stopy - a proč kope kolem sebe kdo se topí - jakej sval to Zemí otáčí -\-- - -/--texysource -Kdoví jestli - jestli jsou na měsíci vůbec nějaký stopy - a proč kope kolem sebe kdo se topí - jakej sval to Zemí otáčí -\-- - - -Titulky -======= - -Titulky je možné zapsat hned dvěma způsoby: **podtržením** nebo **předsazením**. - -Každý titulek má svůj stupeň. V případě **podtržení** o důležitosti titulku rozhoduje podtrhávací znak. Od nejvyšší po nejnižší jsou to tyto: `#` `*` `=` `-` - -/--code texy -Hlavní titulek -************** - - -Podtitulek -========== -\-- - -/--texysource -Hlavní titulek -************** - - -Podtitulek -========== -\-- - -U titulků zapsaných **předsazením** určuje úroveň počet předsazených znaků. A ty mohou být `#` nebo `=` - -Platí: čím více znaků, tím důležitější titulek (minimum jsou dva znaky, maximum sedm). - -/--code texy -=== Hlavní titulek === - -## Podtitulek -\-- - -Jak vidíte v případě podtitulku, znaky vpravo je možné vynechat. - -*Stupně titulků jsou vždy jen relativní! Tedy Texy najde nejvyšší použitý titulek a ostatní titulky relativně od něj odstupňuje.* - - -Horizontální čáry -================= - -Texy zná tyto způsoby zápisu: - - -/--code texy ------------- - -******** -\-- - - -/--texysource -------------- - -******** -\-- - - -Kód -=== - -Používá se pro vložení zdrojového kódu. Použitím přídavného modulu lze aktivovat i zvýrazňování syntaxe. - -/--code texy - /---code php - function reImage($matches) { - $content = $matches[1]; - $align = $matches[5]; - $href = $matches[6]; - } - \--- -\-- - -/--texysource - /---code php - function reImage($matches) { - $content = $matches[1]; - $align = $matches[5]; - $href = $matches[6]; - } - \--- -\-- - -*Všimněte si slova `php` pro označení jazyka.* - - -Vypnutí Texy -============ - -Klíčové slovo `html` nebo `text` ovlivňuje, jestli obsah bude chápán jako HTML (včetně značek), nebo prostý text. - -/--code texy - /---html - <em>příklad</em>: **this is not strong** - \--- - - - /---text - <em>příklad</em>: **this is not strong** - \--- -\-- - -Pro inline vypnutí Texy je možné použít dvojitý apostrof `''` a obalit s ním část textu, který nemá být Texy zpracováván. - -/--code texy - Příklad: ''**this is not strong**'' -\-- - - -Rozdělování do bloků (div) -========================== - -Tuto schopnost využijete při tvorbě složitějších dokumentů. - -/--code texy - /---div .[header] - - content of div - - \--- -\-- - -/--texysource - /---div .[header] - - content of div - - \--- -\-- - - -Je možné bloky i vnořovat: - -/--code texy - /---div .[header] - - ## This is a header. - - /---div - vnořený div - \--- - - Texy je sexy! - - \--- -\-- - -/--texysource - /---div .[header] - - ## This is a header. - - /---div - vnořený div - \--- - - Texy je sexy! - - \--- -\-- - - -Citace -====== - -Citace jsou odsazené, podobně jako v emailech, znakem `>` - -/--code texy -> This is a blockquote with two paragraphs. -> -> 640 K should be enough for everyone -\-- - -/--texysource -> This is a blockquote with two paragraphs. -> -> 640 K should be enough for everyone -\-- - - -Odkazy -====== - -Odkazy se zapisují tak, že odkazující text uzavřete do uvozovek a následujete dvojtečkou a URL. Texy se snaží inteligentně odhadnout konec URL. Můžete mu i pomoci tím, že URI uzavřete do hranatých závorek. Část `http://` není povinná. - -Jako odkaz je možné vkládat i emaily, Texy je transformuje do podoby, která by měla zmást spamboty. - -/--code texy -Look at homepage:[https://texy.info]. - -Do you know "La Trine":https://www.latrine.cz? - -"Write me":me@example.com -\-- - -/--texysource -Look at homepage:[https://texy.info]. - -Do you know "La Trine":https://www.latrine.cz? - -"Write me":me@example.com -\-- - - -Reference ---------- - -Aby se tok textu "neznečišťoval" vkládáním URL, je možné všechny adresy uvést na jednom místě a pak se na ně jen odkazovat. Tomu se říká reference. Kromě adresy je možné doplnit i text odkazu a [modifikátor | #modifier]. - -/--code texy - [homepage]: https://texy.info/ Texy .(homepage) - [nette]: http://nette.org - -This is [homepage] - -Look at "this site":[nette] -\-- - - -Obrázky -======= - -Zapisují se mezi hranaté závorky s hvězdičkou: - -/--code texy -[* image.gif *] -\-- - -/--texysource line -[* image.gif *] -\-- - -V textových odstavcích je často třeba zvolit, má-li být obrázek zarovnán k levému nebo pravému kraji. Toho docílíte pomocí znaku `<` a `>` použitého před pravou závorkou: - -/--code texy -[* image.gif <] Left-aligned image - -[* image.gif >] Right-aligned image -\-- - -/--texysource -[* image.gif <] Left-aligned image - -[* image.gif >] Right-aligned image -\-- - -*Poznámka: V uvedeném příkladu Texy použil pro zarovnání přímý styl. Je možné systém nakonfigurovat tak, aby místo přiřadil obrázkům zvolenou třídu.* - -*Poznámka: pro všechny (relativní) URL obrázků je možné nastavit výchozí adresář. V uvedeným příkladech to byl `images/`, proto v Texy není adresář uveden, zatímco ve vygenerovaném HTML ano.* - -*Poznámka: pokud není implicitně určen alternativní text (jak na to viz níže), použije Texy výchozí. Zde je to prosté `image`* - - -Rozměry -------- - -U lokálních obrázků Texy zjistí rozměry automaticky. Pokud je chcete určit ručně, zapište je takto: - -/--code texy -[* image.gif 10x20 *] -\-- - -/--texysource line -[* image.gif 10x20 *] -\-- - - -Modifikátory ------------- - -O nich se více dozvíte v [jiné kapitole | #modifier], ale neuškodí si ukázat, jak se u obrázků zapisují. Zkusme si modifikátor pro určení alternativního textu a třídy: - -/--code texy -[* image.gif .(alt text)[foto] *] -\-- - -/--texysource line -[* image.gif .(alt text)[foto] *] -\-- - - -Reference ---------- - -Ze stejných důvodů, jako u odkazů, je i obrázky možné zapisovat pomocí referencí. Je třeba definovat URL (nebo více URL oddělených `|`) a případně i modifikátory. - -/--code texy -What a beautiful girl [* picture*] ! - -[* picture*]: image.gif .(my girl) -\-- - -/--texysource -What a beautiful girl [* picture*] ! - -[* picture*]: image.gif .(my girl) -\-- - - -Obrázek s popiskou ------------------- - -Za obrázkem uveďte tři hvězdičky a následuje popiska: - - -/--code texy -[* image.gif *] *** Toto je *popiska* pod obrázkem -\-- - -/--texysource -[* image.gif *] *** Toto je *popiska* pod obrázkem -\-- - - -Fráze -===== - -Asi nejpoužívanější syntax v Texy. Téměř ve všech případech se používá zdvojený znak. - -/--code texy -//kurzíva// - -*taky kurzíva* - -**tučné** - -superscript^2 vs. subscript_2 -\-- - -/--texysource -//kurzíva// - -*taky kurzíva* - -**tučné** - -superscript^2 vs. subscript_2 -\-- - -/--div .[output] -//kurzíva// - -*taky kurzíva* - -**tučné** - -***nejsilněji zdůrazněné*** - -superscript^2 vs. subscript_2 -\-- - -Speciálním případem fráze je tzv. kód. Od ostatních se liší tím, že jeho obsah nebude nadále formátován a zobrazí se doslovně: - -/--code texy -Odstraňte `<br />` a entitu `&ndash` -\-- - -/--texysource -Odstraňte `<br />` a entitu `&ndash` -\-- - -*Poznámka: jestli se použije element `<code>` nebo jiný (případně žádný) je možné rozhodnout pouhou konfigurací Texy* - - -S modifikátorem ---------------- - -Je možné jej vložit do každé fráze, vždy těsně před uzavírací znak: - -/--code texy -**silný a zelený .{color:green}** jako Hulk -\-- - -/--texysource -**silný a zelený .{color:green}** jako Hulk -\-- - -/--div .[output] -**silný a zelený .{color:green}** jako Hulk -\-- - - -Přímé HTML -========== - -Texy není náhrada za HTML. Nehledá ani alternativní způsoby zápisu HTML. Cílem je zjednodušit psaní obsahu. Pokud se Vám zdá jednodušší zapsat některou strukturu přímo v HTML, můžete tak učinit. HTML značky jsou plně podporované. - -/--code texy -This <strong class=info>is strong</strong> text. -<br> This is not. -\-- - -/--texysource -This <strong class=info>is strong</strong> text. -<br> This is not. -\-- - -*Poznámka: všimněte si, že Texy upraví zápis atributů a značek tak, aby byly validní (i pro XHTML výstup). Stejně tak dbá na **well-formed zápis**!* - -*Poznámka: Rozhodování, která značky a které atributy můžou být v textu použity, je plně uživatelsky ovladatelné. Demonstruje to jeden příklad z distribuce.* - - -Seznamy -======= - -Odrážkové seznamy zapisujeme pomocí `*` `+` nebo `-`. Musí být zapsán hned na začátku řádku a za ním musí následovat mezera. - -/--code texy -- Red -- Green -- Blue -\-- - -/--texysource -- Red -- Green -- Blue -\-- - -/--div .[output] -- Red -- Green -- Blue -\-- - - -Číslované seznamy ------------------ - -Texy zná těchto pět způsobů zápisu (první dva jsou ekvivalentní): - -/--code texy -1) Učit se -2) Učit se -3) Učit se - -a) Dlouhý -b) Široký -c) Krátkozraký - -A) DOS -B) Windows -C) Linux - -I) Yesterday -II) Today -III) Tomorrow -\-- - -/--texysource -1) Učit se -2) Učit se -3) Učit se - -a) Dlouhý -b) Široký -c) Krátkozraký - -A) DOS -B) Windows -C) Linux - -I) Yesterday -II) Today -III) Tomorrow -\-- - -/--div .[output] -1) Učit se -2) Učit se -3) Učit se - -a) Dlouhý -b) Široký -c) Krátkozraký - -A) DOS -B) Windows -C) Linux - -I) Yesterday -II) Today -III) Tomorrow -\-- - - -Vnořené seznamy ---------------- - -/--code texy -a) Bird - I) Bird - - Red - - Green - - Blue - II) McHale - III) Parish -b) McHale -c) Parish - 1) Bird - 2) McHale - 3) Parish -\-- - -/--texysource -a) Bird - I) Bird - - Red - - Green - - Blue - II) McHale - III) Parish -b) McHale -c) Parish - 1) Bird - 2) McHale - 3) Parish -\-- - - -Definiční seznam ----------------- - -/--code texy -Koncert Divokej Bill: - - termín: 9. 12. 2004 - - místo: Hala Vodová, Brno - - Cena: 260 Kč -\-- - -/--texysource -Koncert Divokej Bill: - - termín: 9. 12. 2004 - - místo: Hala Vodová, Brno - - Cena: 260 Kč -\-- - -/--div .[output] -Koncert Divokej Bill: - - termín: 9. 12. 2004 - - místo: Hala Vodová, Brno - - Cena: 260 Kč -\-- - - -S modifikátorem ---------------- - -Modifikátor, který ovlivňuje celý seznam, se uvádí na řádku před ním. Ostatní (klasicky) na konci řádku: - -/--code texy -.{color:red} -triangl: .{color:blue} - - trojúhelník .{color:green} - - neladěný bicí hudební nástroj - - tringulační věž -\-- - -/--div .[output] -.{color:red} -triangl: .{color:blue} - - trojúhelník .{color:green} - - neladěný bicí hudební nástroj - - tringulační věž -\-- - - -Modifikátory -============ - -Nejsilnější zbraň Texy Lze použít tyto druhy modifikátorů: - -- (titulek) popisné, přidají objektu titulek (nebo alternativní text obrázkům) -- `[class1 class2 #id]` určující třídu a / nebo ID prvku -- {class:blue} přímý zápis stylu -- {target:_blank} nebo přímý zápis HTML atributů -- horizontální zarovnání: - - doleva < - - doprava > - - vycentrovaný <> - - do bloku = - -- vertikální zarovnání: (jen u tabulek) - - nahoru ^ - - na střed - - - dolů _ - -Modifikátory se zapisují spojitě (bez mezer) a **musí jim předcházet tečka**. Takže třeba `.(popis)[left]` nastavuje atribut title na `popis` a třídu na `left`. - -**Modifikátory je vždy zapisují zcela doprava**. - -Příklad použití modifikátoru na odstavci textu: - -/--code texy -Vycentrováno modifikátorem .<> - -Obarveno modifikátorem .{color:blue; lang: cs} -\-- - -/--texysource -Vycentrováno modifikátorem .<> - -Obarveno modifikátorem .{color:blue; lang: cs} -\-- - - -Typografie -========== - -Sem patří všechny úpravy a náhrady textu, které upravují jeho vzhled v souladu s typografickými pravidly a podobně: - -/--code texy -- "české" 'typografické' uvozovky -- pomlčka vs. spojovník: 10-15 vs. česko-slovenský -- pomlčka: jedna -- dvě -- typografický křížek u rozměrů 10 x 20 -- šipky <- a -> a <-> ; -- tři tečky... -- zachování HTML entit & -- náhrady(TM) nebo(R) za příslušné entity(C) -\-- - -/--div .[output] -- "české" 'typografické' uvozovky -- pomlčka vs. spojovník: 10-15 vs. česko-slovenský -- pomlčka: jedna -- dvě -- typografický křížek u rozměrů 10 x 20 -- šipky <- a -> a <-> ; -- tři tečky... -- zachování HTML entit & -- náhrady(TM) nebo(R) za příslušné (C)entity -\-- - -práce s mezerami: - -/--code texy -- vkládání nezalomitelných mezer za jednopísmenné předložky (v autě u okna) -- nedělitelné mezery u telefonních čísel +420 776 552 046 -\-- - -/--code html -vkládání nezalomitelných mezer za jednopísmenné předložky (v autě u okna) - -nedělitelné mezery u telefonních čísel +420 776 552 046 -\-- - -*Poznámka: Nahrazování se obvykle řídí dalšími pravidly, které určují, kdy -symbol nahradit a kdy ne. Například šipka `->` nemůže být na konci řádku atd. Proto nebuďte překvapeni, když v některých případech Texy náhradu neprovede. Pokud to považujete za chybu, dejte mi vědět.* - - -Zkratky, akronymy ------------------ - -Používá se zápisu s dvojitou kulatou závorkou: - -/--code texy -jednoslovné: NATO((North Atlantic Treaty Organisation)) - -víceslovné: "et al."((a další)) -\-- - -/--texysource -jednoslovné: NATO((North Atlantic Treaty Organisation)) - -víceslovné: "et al."((a další)) -\-- - - -Klikatelné URI --------------- - -Automatický převod URI do klikatelné formy (včetně emailů) - -/--code texy -další informace na www.texy.info a také ... -\-- - -/--div .[output] -další informace na www.texy.info a také ... -\-- - - -Rozdělení velmi dlouhých slov -============================= - -Velmi zajímavá a důležitá funkce Texy. Dlouhá slova mohou narušit vzhled stránky, proto je vhodné prohlížeči naznačit, kde je může zalomit. Texy tyto místa hledá s přihlédnutím k národním zvyklostem, tedy slovo rozděluje podle slabik: - -/--code texy -nejneobhospodařovávatelnějšími -\-- - -/--code html -nejneobhospoda­řovávatelnější­mi</p -\-- - -*Poznámka: limit délky slova je volitelný* - -*Poznámka: současné prohlížeče na jádru Gecko (Mozilla, Firefox) jsou k naznačenému dělení slepí. Doufám, že vývojáři tento nedostatek brzy odstraní. Nebo zkuste [tohle | https://forum.texy.info/cs/viewtopic.php?id=36]* - - -Tabulky -======= - -Příklad jednoduché tabulky, sloupce se oddělují znakem `|` - -/--code texy -| first col | second col | third col -| Adam | Eva | Franta -\-- - -A výsledek je: - -| first col | second col | third col -| Adam | Eva | Franta - - -Hlavičku tabulky můžeme definovat tímto zápisem: - -/--code texy -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Jesus | Christ | 33 -| Cecilie | Svobodova | 74 -\-- - -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Jesus | Christ | 33 -| Cecilie | Svobodova | 74 - -Pokud hlavičku netvoří řádek (řádky), můžeme ji definovat na úrovni buněk. Stačí vložit hvězdičku ihned po znaku `|` - - -/--code texy -|* First Name | Jesus | Cecilie -|* Last Name | Christ | Svobodova -|* Age | 33 | 74 -\-- - - -|* First Name | Jesus | Cecilie -|* Last Name | Christ | Svobodova -|* Age | 33 | 74 - - -Sloučení sloupců ----------------- - -všimněte si zdvojeného || - -/--code texy -|----------------------------- -| Name || Age -|---------------------------- -| Jesus | Christ | 33 -\-- - -|----------------------------- -| Name || Age -|---------------------------- -| Jesus | Christ | 33 - - -Sloučení řádků --------------- - -Všimněte si znaku `^` symbolizujícího směr nahoru: - - -/--code texy -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Bill || 50 -| ^| 52 -| Jim | Beam | 70 -\-- - -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Bill || 50 -| ^| 52 -| Jim | Beam | 70 - - -Modifikátory ------------- - -Platí tato pravidla: -- modifikátor ovlivňující celou tabulku se vkládá bezprostředně před tabulku -- ovlivňující řádek se vkládá na konec řádku -- ovlivňující sloupec se vkládá na začátek buňky (vlevo v buňce) -- a nakonec ovlivňující buňku se vkládá na konec buňky (pravo v buňce) - -Podívejte se na příklad. - -/--code texy -.(people) -| .{color: green} first col | second col .>| third col | .{font-style:italic} -| Adam | Eva .{color: blue}| Franta | -\-- - -Zde je: -- `.(people)` modifikátor tabulky -- `.{color: green}` modifikátor sloupce -- `.{font-style:italic}` modifikátor řádku -- `.{color: blue}` a také `.>` modifikátor buňky - -Takže výsledná tabulka vypadá takto: - - -.(people) -| .{color: green} first col | second col .>| third col | .{font-style:italic} -| Adam | Eva .{color: blue}| Franta | diff --git a/texy/cs/syntax.texy b/texy/cs/syntax.texy index 9a430f8080..270b6c6a6f 100644 --- a/texy/cs/syntax.texy +++ b/texy/cs/syntax.texy @@ -1,464 +1,891 @@ Syntaxe ******* ---> "Podrobný popis syntaxe":syntax-podrobne +.[perex] +Texy vznikl proto, aby nezkušeným uživatelům umožnil snadno editovat obsah webových stránek. Proto je i syntaxe intuitivní a přehledná. + + +Cheat Sheet +=========== + +| [#Formátování textu] | Syntax +|----------------------------------------------- +| [Tučný text |#Formátování textu] | .[text-code] ''**tučný text**'' +| [Kurzíva |#Formátování textu] | ''*kurzíva*'' nebo ''//kurzíva//'' +| [Inline code |#Formátování textu] | ''`kód`'' +| [#Odkazy] | ''"text":URL'' nebo ''[text](URL)'' +| [#Obrázky] | ''[* image.jpg *]'' +| [#Vypnutí formátování] | ''specialní znaky'' +|----------------------------------------------- +| Elementy +|----------------------------------------------- +| [#Podtržené nadpisy] | H1 <br> === +| [#Ohraničené nadpisy] | ''### H1'' <br> ## H2 +| [#Odrážkové seznamy] | ''- první'' <br> ''- druhá'' +| [#Číslované seznamy] | ''1) první'' <br> ''2) druhá'' +| [#Seznamy definic] | term: <br>   ''- první'' +| [#Citace] | ''> blockquote'' +| [#Horizontální čáry] | ''---'' +| [#Tabulky] | ''\| buňka \| buňka \|'' +| [Bloky kódu|#Předformátovaný text] | ''/--'' <br> ... <br> ''\--'' +|----------------------------------------------- +| Modifikátory .[#toc-modifikatory] +|-------------------------------------------------------- +| titulek | ''.(titulek)'' +| CSS třída | ''.[btn btn-primary]'' +| ID | ''.[#id]'' +| CSS styl nebo HTML atribut | ''.{color: blue}'' nebo ''.{target: _blank}'' +| horizontální zarovnání | ''.< .> .<> .='' +| vertikální zarovnání | ''.^ .- ._'' + + +Odstavce textu +============== + +Za odstavec považuje Texy jeden nebo více řádků textu, které následují těsně za sebou. Jakmile mezi nimi necháte **jeden prázdný řádek**, Texy automaticky pochopí, že má začít nový odstavec. + +To znamená, že Texy spojí řádky, které patří k sobě. Nemusíte se tak bát, že se vám věta zalomí uprostřed, když si zmenšíte okno editoru. + +```texy +Toto je první odstavec. Může mít klidně více řádků +a Texy je spojí do jednoho souvislého bloku textu. + +Až tady, po prázdném řádku, začíná úplně nový, druhý odstavec. +``` + +Spojování řádků lze nicméně vypnout v konfiguraci a pak se každý řádek považuje za samostatný odstavec: + +/--php +$texy->mergeLines = false; +\-- + + +Zalomení řádků +-------------- +Co když ale potřebujete text jen odřádkovat, aniž byste vytvářeli celý nový odstavec? To se typicky hodí u básní, textů písní nebo při psaní adresy. **Začněte nový řádek jednou mezerou**. -Nástroj Texy vznikl proto, aby nezkušeným uživatelům umožnil snadno editovat obsah webových stránek. Proto je i syntaxe maximálně intuitivní. Záměrem je, aby text v čisté (nezformátované) formě byl přehledný a jeho formát tušitelný. +```texy +Karel Novák, + U Tiché pošty 5 + 150 00 Praha 5 +``` -Dnes Texy výborně slouží i zkušeným znalcům jazyka HTML. Dovoluje volně kombinovat Texy zápis s HTML značkami. Zkušení uživatelé se tedy nemusí učit nový meta-jazyk a plně využít svých znalostí. Texy jim pouze zjednodušuje práci. -Prvotní logikou syntaxe je **žádnou syntaxi nepoužívat**. Jen psát čistý text. Vkládání rozšířených informací, jako třeba CSS třídy nebo odkazy, nenaruší tok textu. A zapíší se způsobem, který snadno pochopí i netechnicky založení uživatelé. +Stylování odstavců +------------------ +Někdy potřebujete celý odstavec nějak odlišit - například z něj udělat úvodní perex článku, vycentrovat ho nebo mu přiřadit specifický styl pro rámeček. K tomu slouží [#modifikátory], které můžete umístit buď na samostatný řádek **před** odstavec, nebo na konec jeho posledního řádku. -Odstavce textu .[#paragraph] -============================ +```texy +.[perex] +Toto je úvodní odstavec článku, který díky modifikátoru +dostane CSS třídu "perex" a může tak vypadat jinak než zbytek textu. -Za odstavec se považuje jeden nebo více bezprostředně za sebou následujících řádků textu. Odstavečky jsou od sebe oddělené prázdným řádkem. +Tento odstavec má zase přiřazené unikátní ID. .[#sekce-uvod] -/--code texy -První odstavec lorem ipsum dolor sit amet. +A tento odstavec bude vycentrován. .<> +``` -Druhý odstavec, který tvoří jeden řádek. -A druhý řádek textu. Texy je spojí. -\-- -Zalomení řádku v odstavci docílíte vložením jedné mezery vlevo: +Formátování textu +================= -/--code texy -Kdoví jestli - jestli jsou na měsíci vůbec nějaký stopy - a proč kope kolem sebe kdo se topí - jakej sval to Zemí otáčí +| syntax | výstup | ID syntaxe +|----------------------------------------------------------------------------- +| .[text-code] ''**tučný text**'' | **tučný text** | `phrase/strong` +| ''*kurzíva* nebo //kurzíva//'' | *kurzíva* | `phrase/em-alt`, `phrase/em` +| ''***tučná kurzíva***'' | ***tučná kurzíva*** | `phrase/strong+em` +| ''`inline kód`'' | `inline kód` | `phrase/code` +| ''x^2 … O_2'' | x^2 … O_2 | `phrase/sup-alt`, `phrase/sub-alt` +| ''x^^2^^ … O__2__'' | x^2 … O_2 | `phrase/sup`🔸, `phrase/sub`🔸 +| ''++vložený text++'' | <ins>vložený text</ins> | `phrase/ins`🔸 +| ''--smazaný text--'' | <del>smazaný text</del> | `phrase/del`🔸 +| ''>>citovaný text<<'' | >>citovaný text<< | `phrase/quote` +| ''"modrý text .{color: blue}"'' | "modrý text .{color: blue}" | `phrase/span` +| ''~modrý text .{color: blue}~'' | ~modrý text .{color: blue}~ | `phrase/span-alt` +| ''"et al."((a další))'' | "et al."((a další)) | `phrase/acronym` +| ''NBA((National Basketball Association))'' | NBA((National Basketball Association)) | `phrase/acronym-alt` + +Syntaxe označené 🔸 nejsou ve výchozím stavu povolené a musíte je zapnout. Příklad: + +/--php +$texy->allowed['phrase/ins'] = true; \-- +Pro jednoduché číselné indexy můžete použít zkrácenou syntaxi `x^2` a `O_2`, ale pro složitější případy je robustnější varianta s dvojitými znaky, nebo můžete použít HTML značky `<sup>` a `<sub>`. -Titulky .[#heading] -=================== +Uvnitř syntaktických znaků **nesmí být mezery**: -Titulky je možné zapsat hned dvěma způsoby: **podtržením** nebo **předsazením**. +```texy +Špatně: ** toto nebude tučné ** +Správně: **toto bude tučné** +``` -Každý titulek má svůj stupeň. V případě **podtržení** o důležitosti titulku rozhoduje podtrhávací znak. Od nejvyšší po nejnižší jsou to tyto: `#` `*` `=` `-` -/--code texy -Hlavní titulek -************** +Stylování textu +--------------- +Tohle je jedna z nejsilnějších vlastností Texy. Ke každému formátovanému textu můžete "přilepit" [#modifikátory] a přidat mu tak CSS třídu, ID nebo přímý styl. Modifikátor se vždy vkládá **těsně před uzavírací značku**: -Podtitulek -========== -\-- +```texy +Tento text je **silný a zelený .{color:green}** jako Hulk. -U titulků zapsaných **předsazením** určuje úroveň počet předsazených znaků. A ty mohou být `#` nebo `=` +Upozornění: --Tato funkce je zastaralá .[deprecated]-- +``` -Platí: čím více znaků, tím důležitější titulek (minimum jsou dva znaky, maximum sedm). +Pokud chcete použít modifikátor na text, ale nechcete jej zároveň dělat tučným nebo kurzívou, použijte jako obalovací značku uvozovky `"` nebo vlnovky `~`. Texy z toho vytvoří univerzální HTML značku `<span>` s vašimi styly: -/--code texy -=== Hlavní titulek === +```texy +Běžný text, ale "tento kousek je červený .{color: red}", a zbytek už ne. +``` -## Podtitulek -\-- -Jak vidíte v případě podtitulku, znaky vpravo je možné vynechat. +Formátování a odkazy v jednom +----------------------------- +Z formátovaného textu můžete udělat odkaz - jednoduše přidejte dvojtečku a URL adresu: -Horizontální čáry .[#horizline] -=============================== +```texy +Navštivte naši **novou galerii**:https://example.com/gallery +``` -Texy zná tyto způsoby zápisu: +Toto funguje pro tučný text, kurzívu i inline kód. -/--code texy ------------- +Psaní speciálních znaků +----------------------- -******** -\-- +Co když chcete napsat doslova `**text**` včetně hvězdiček, aniž by se z něj stal tučný text? Máte tři možnosti: +- zpětné lomítko je nejrychlejší způsob, jak "zneplatnit" jeden speciální znak `\**text\**` +- dvojité apostrofy [vypnou Texy|#Vypnutí formátování] pro celou frázi `''**text**''` +- můžete použít standardní HTML entity `**text**` -Vypnutí Texy .[#disable-texy] -============================= -Klíčové slovo `html` nebo `text` ovlivňuje, jestli obsah bude chápán jako HTML (včetně značek), nebo prostý text. +Odkazy +====== -/--code texy - /---html - <em>příklad</em>: **this is not strong** - \--- +Odkazy jsou duší internetu. V Texy je jejich tvorba navržena tak, aby byla co nejpřirozenější a nejpřehlednější přímo v textu. +Základní syntaxe pro odkaz je jednoduchá a skvěle čitelná. Odkazovaný text uzavřete do `"` (nebo jiných znaků pro [#formátování textu]) a hned připojíte dvojtečku a cílovou URL adresu: - /---text - <em>příklad</em>: **this is not strong** - \--- -\-- +```texy +Navštivte oficiální stránky projektu "Nette Framework":https://nette.org. -Pro inline vypnutí Texy je možné použít dvojitý apostrof `''` a obalit s ním část textu, který nemá být Texy zpracováván. +Pokud máte dotaz, "napište nám":info@example.com. +``` -/--code texy - Příklad: ''**this is not strong**'' -\-- +Výhodou je, že Texy je inteligentní a sám pozná, kde URL končí. Nemusíte se tedy bát, že by do odkazu omylem zahrnulo tečku nebo čárku na konci věty. Pokud ale URL obsahuje nestandardní znaky, můžete je uzavřít do hranatých závorek a tím přesně řeknete, kde adresa začíná a končí: +```texy +"Přečtěte si náš článek":[https://example.com/novinky?id=1&kategorie=články] +``` -Citace .[#blockquote] -===================== +ID syntaxe `phrase/span`, `phrase/span-alt` | [PhraseModule |configuration#phrasemodule] a [LinkModule |configuration#linkmodule] -Citace jsou odsazené, podobně jako v emailech, znakem `>` -/--code texy -> This is a blockquote with two paragraphs. -> -> 640 K should be enough for everyone -\-- +Alternativní syntaxe odkazů +--------------------------- +Jste zvyklí na formát, který používá Markdown nebo Wikipedia? Texy rozumí i jim. Můžete si vybrat styl, který vám nejvíce vyhovuje. -Odkazy .[#link] -=============== +```texy +[Text odkazu](https://adresa.cz) // Styl známý z Markdownu +[Text odkazu | https://adresa.cz] // Styl známý z MediaWiki +text:[cílová URL nebo reference] // Jednoslovný odkaz +``` -Odkazy se zapisují tak, že odkazující text uzavřete do uvozovek a následujete dvojtečkou a URL. Texy se snaží inteligentně odhadnout konec URL. Můžete mu i pomoci tím, že URI uzavřete do hranatých závorek. Část `http://` není povinná. +ID syntaxe `phrase/markdown`, `phrase/wikilink`, `phrase/quicklink` | [PhraseModule |configuration#phrasemodule] -Jako odkaz je možné vkládat i emaily, Texy je transformuje do podoby, která by měla zmást spamboty. -/--code texy -Look at homepage:[https://texy.info]. +Udržujte si pořádek s referencemi +--------------------------------- -Do you know "La Trine":https://www.latrine.cz? +Při psaní delších textů může být nepohodlné vkládat dlouhé URL adresy přímo do odstavců - zhoršuje to čitelnost a přehlednost. Pro tyto případy má Texy **referenční odkazy**. -"Write me":me@example.com +V textu použijete pouze krátký, snadno zapamatovatelný název reference. A na konci dokumentu pak všechny tyto reference přehledně definujete. -\-- +```texy +Doporučujeme si prostudovat "oficiální dokumentaci":[doc] a projít si "příklady syntaxe":[syntax]. +Celý projekt je postaven na [Nette]. +​[doc]: https://texy.nette.org/cs/ "Dokumentace Texy!" +​[syntax]: https://texy.nette.org/cs/syntax +​[Nette]: https://nette.org +``` -Obrázky .[#image] -================= +ID syntaxe `link/reference`, `link/definition` | [LinkModule |configuration#linkmodule] -Zapisují se mezi hranaté závorky s hvězdičkou: -/--code texy -[* image.gif .(alternativní text) *] -\-- +Automatické odkazy +------------------ + +Kdykoli do textu napíšete URL adresu (začínající na `http://`, `https://`, `www.`) nebo e-mail, Texy ji automaticky rozpozná a převede na klikatelný odkaz. Nemusíte dělat vůbec nic. +```texy +Náš web najdete na adrese www.example.com. +Pro podporu pište na support@example.com. +``` -V textových odstavcích je často třeba zvolit, má-li být obrázek zarovnán k levému nebo pravému kraji. Toho docílíte pomocí znaku `<` a `>` použitého před pravou závorkou: +ID syntaxe `link/url`, `link/email` | [LinkModule |configuration#linkmodule] + + +Stylování odkazů +---------------- -/--code texy -[* image.gif <] Left-aligned image. Lorem ipsum ... +S [#modifikátory] můžete odkazům snadno přidávat další vlastnosti: -[* image.gif >] Right-aligned image. Curabitur quam ... +```texy +"Externí odkaz .[external](Otevře se v novém okně){target:_blank}":https://google.com +``` + +Speciální třída `nofollow` přidá odkazu atribut `rel="nofollow"`, čímž dáváte vyhledávačům signál, aby tento odkaz nesledovaly. To se hodí například u odkazů v komentářích. + +```texy +"Odkaz, kterému nedůvěřuji .[nofollow]":https://example.com +``` + + +Automatické maskování e-mailů +----------------------------- + +Texy automaticky obfuskuje (maskuje) emailové adresy před spamboty: + +```latte +<a href="mailto:info@example.com">info@<!-- -->example.com</a> +``` + +Toto chování můžete vypnout: + +/--php +$texy->obfuscateEmail = false; \-- -Obrázek s popiskou +Přímé HTML +========== + +Texy je navržen tak, abyste HTML nemuseli psát vůbec. Ale co když narazíte na situaci, kdy je přímé vložení HTML značky jednodušší, nebo potřebujete vytvořit něco, na co syntaxe Texy nestačí? Žádný problém. Texy vám dává naprostou svobodu kombinovat oba světy. + +Můžete plynule přecházet mezi syntaxí Texy a čistým HTML, kdykoli se vám to hodí. + +```texy +Toto je **tučný text** v Texy a toto je <strong>tučný text</strong> pomocí HTML. + +<div class="info-box"> + <h3>Můžete vkládat i celé komplexní bloky</h3> +</div> +``` + +Možná si říkáte, že vkládání přímého HTML může být riskantní. Co když uděláte chybu nebo někdo vloží škodlivý kód? Texy na to myslí a funguje jako inteligentní filtr a pomocník: + +- **Opravuje chyby:** Texy zajistí, aby byl výsledný kód vždy validní a nerozbil vám stránku. +- **Hlídá bezpečnost:** Texy má ve výchozím stavu seznam povolených značek a jejich atributů. Pokud se v kódu objeví neznámá značka nebo potenciálně nebezpečný atribut (např. `onclick`), Texy ho bezpečně odstraní. Chrání tak váš web před XSS útoky. +- **Zajišťuje konzistentní výstup:** Bez ohledu na to, jaký HTML kód vložíte, Texy se postará, aby byl výsledek vždy správně strukturovaný (well-formed). + +Tento ochranný štít si můžete přizpůsobit. Pomocí konfigurace `$texy->allowedTags` můžete přesně definovat, které HTML značky a atributy jsou na vašem webu povoleny a které ne. + +Máte tak plnou kontrolu nad tím, jaké HTML mohou například redaktoři používat, a zajišťujete konzistenci a bezpečnost celého webu. Více informací naleznete v sekci "konfigurace":configuration#allowedtags. + +ID syntaxe `html/tag`, `html/comment` | [HtmlModule |configuration#htmlmodule] + + +Nadpisy +======= + +Texy vám nabízí dva elegantní a intuitivní způsoby, jak nadpisy vytvářet: **podtržené** a **ohraničené**. + + +Podtržené nadpisy +----------------- + +Tento styl připomíná psací stroj. Jednoduše pod nadpis vložte podtržení (alespoň 3 znaky). O důležitosti nadpisu rozhoduje podtrhávací znak. Od nejvyšší po nejnižší jsou to tyto: `#` `*` `=` `-` + +```texy +Toto je nejdůležitější nadpis celého dokumentu +​################################################ + +A toto je nadpis druhé úrovně +​****************************** +``` + +ID syntaxe `heading/underlined` | [HeadingModule |configuration#headingmodule] + + +Ohraničené nadpisy ------------------ -Za obrázkem uveďte tři hvězdičky a následuje popiska: +Tento způsob je velmi rychlý na psaní. Text nadpisu "zabalíte" mezi znaky `#` nebo `=`. Zde o úrovni nadpisu rozhoduje **počet** použitých znaků (2 až 7). Čím více znaků, tím důležitější nadpis. +```texy +==== Nejdůležitější nadpis (H1) -/--code texy -[* image.gif *] *** Toto je *popiska* pod obrázkem -\-- +=== Méně důležitý (H2) +== Ještě méně důležitý (H3) +``` -Fráze .[#phrase] -================ +Můžete použít ohraničení na obou stranách (pro lepší vizuální přehlednost) nebo jen na začátku. Texy si s oběma variantami poradí. -Asi nejpoužívanější syntax v Texy. Téměř ve všech případech se používá zdvojený znak. +ID syntaxe `heading/surrounded` | [HeadingModule |configuration#headingmodule] -/--code texy -//kurzíva// -**tučné** +Stylování nadpisů +----------------- -x^2 + y^3 -\-- +Ke každému nadpisu můžete přidat [#modifikátory]. To vám umožní přiřadit mu konkrétní CSS třídu pro stylování nebo unikátní ID, na které pak můžete odkazovat. +```texy +Nadpis s červenou barvou .[cerveny-nadpis] +​========================================== -/--div .[output] -//kurzíva// +### Nadpis s unikátním ID pro odkazování .[#kontakt] +``` -**tučné** -x^2 + y^3 -\-- +Automatické kotvy pro snadnou navigaci +-------------------------------------- +Nechcete vymýšlet ID pro každý nadpis ručně? Texy to umí udělat za vás! V konfiguraci můžete zapnout automatické generování ID pro všechny nadpisy. To je neuvěřitelně užitečné pro přímé odkazování na konkrétní sekce. -Texy lze i dočasně vypnout - obsah nebude formátován a zobrazí se doslovně: +```php +// Povolit automatické generování ID +$texy->headingModule->generateID = true; -/--code texy -Odstraňte ''<br />'' a entitu ''&ndash'' -\-- +// Volitelně nastavit předponu pro generovaná ID (např. "sekce-") +$texy->headingModule->idPrefix = 'toc-'; +``` +S tímto nastavením nadpis `## Moje kapitola` automaticky dostane například ID `id="toc-moje-kapitola"`, aniž byste museli cokoliv psát navíc. -Přímé HTML .[#html] -=================== -Texy není náhrada za HTML. Nehledá ani alternativní způsoby zápisu HTML. Cílem je zjednodušit psaní obsahu. Pokud se Vám zdá jednodušší zapsat některou strukturu přímo v HTML, můžete tak učinit. HTML značky jsou plně podporované. +Seznamy +======= -/--code texy -This <strong class=info>is strong</strong> text. -<br> This is not. -\-- +Odrážkové seznamy +----------------- -Seznamy .[#list] -================ +Pro rychlý výčet položek, u kterých nezáleží na pořadí, se skvěle hodí odrážkový seznam. Stačí každý řádek začít pomlčkou `-`, hvězdičkou `*` nebo pluskem `+` a mezerou. Všechny tři znaky fungují stejně, takže si můžete vybrat ten, který je vám nejsympatičtější. -Odrážkové seznamy zapisujeme pomocí `*` `+` nebo `-`. Musí být zapsán hned na začátku řádku a za ním musí následovat mezera. +```texy +Co je potřeba nakoupit: -/--code texy -- Red -- Green -- Blue -\-- +- Mléko +- Chleba +* Vejce ++ Máslo +``` + +ID syntaxe `list` | [ListModule |configuration#listmodule] Číslované seznamy ----------------- -Texy zná těchto pět způsobů zápisu (první dva jsou ekvivalentní): +Texy podporuje různé styly číslování: -/--code texy -1) Učit se -2) Učit se -3) Učit se +| `1.` | Arabské číslice (s tečkou) +| `1)` | Arabské číslice (se závorkou) +| `a)` | Malá písmena abecedy +| `A)` | Velká písmena abecedy +| `I)` | Římské číslice -a) Dlouhý -b) Široký -c) Krátkozraký +Kouzlo spočívá v tom, že se vůbec nemusíte starat o správné číslování. I když všechny řádky očíslujete jedničkou, Texy je automaticky přečísluje za vás. To je obrovská výhoda, když později potřebujete nějakou položku přidat, smazat nebo přesunout. -A) DOS -B) Windows -C) Linux -I) Yesterday -II) Today -III) Tomorrow -\-- +Vnořené a kombinované seznamy +----------------------------- + +Síla seznamů se naplno projeví, když je začnete kombinovat a vnořovat. Můžete tak vytvářet přehledné, víceúrovňové struktury. Vnoření vytvoříte jednoduše tak, že daný řádek odsadíte alespoň o **dvě mezery** (nebo jeden tabulátor). + +```texy +1) První kapitola + a) Podkapitola 1.1 + - První bod + - Druhý bod + b) Podkapitola 1.2 +2) Druhá kapitola + - Hlavní myšlenka + - Další poznámka +``` -Vnořené seznamy +Seznamy definic --------------- -/--code texy -a) Bird - I) Bird - - Red - - Green - - Blue - II) McHale - III) Parish -b) McHale -c) Parish - 1) Bird - 2) McHale - 3) Parish -\-- +Pro případy, kdy potřebujete vytvořit slovníček pojmů nebo přehledně vysvětlit několik termínů, je ideální definiční seznam. +Na první řádek napište termín, který chcete definovat, a zakončete ho dvojtečkou. Na další řádky pište jeho definici, přičemž každý řádek odsaďte a začněte pomlčkou `-`. -Definiční seznam ----------------- +```texy +HTML: + - Značkovací jazyk pro tvorbu webových stránek. + - Zkratka pro HyperText Markup Language. -Koncert "Divokej Bill":www.divokybill.cz: - - termín: 9. 12. 2004 - - místo: Hala Vodová, Brno - - Cena: 260 Kč +CSS: + - Jazyk pro popis způsobu zobrazení (stylování) stránek. + - Zkratka pro Cascading Style Sheets. +``` -/--code texy -Koncert Divokej Bill: - - termín: 9. 12. 2004 - - místo: Hala Vodová, Brno - - Cena: 260 Kč -\-- +ID syntaxe `list/definition` | [ListModule |configuration#listmodule] -Modifikátory .[#modifier] -========================= +Stylování seznamů +----------------- -Nejsilnější zbraň Texy. Lze použít tyto druhy modifikátorů: +Stejně jako u ostatních prvků v Texy, i seznamům můžete snadno přidávat [#modifikátory] pro změnu vzhledu. -- (titulek) popisné, přidají objektu titulek (nebo alternativní text obrázkům) -- `[class1 class2 #id]` určující třídu a / nebo ID prvku -- {class:blue} přímý zápis stylu -- {target:_blank} nebo přímý zápis HTML atributů -- horizontální zarovnání: - - doleva < - - doprava > - - vycentrovaný <> - - do bloku = +**Celý seznam:** Modifikátor napište na řádek **před** začátkem seznamu. -- vertikální zarovnání: (jen u tabulek) - - nahoru ^ - - na střed - - - dolů _ +```texy +.[barevny-seznam] +- První položka +- Druhá položka +``` -Modifikátory se zapisují spojitě (bez mezer) a **musí jim předcházet tečka**. Takže třeba `.(popis)[left]` nastavuje atribut title na `popis` a třídu na `left`. +**Jednotlivá položka:** Modifikátor přidejte na **konec** řádku dané položky nebo definičního termínu. -**Modifikátory je vždy zapisují zcela doprava**. +```texy +- Běžná položka +- Tato položka je důležitá! .{font-weight: bold} +- Další běžná položka +``` -Příklad použití modifikátoru na odstavci textu: -/--code texy -Vycentrováno modifikátorem .<> +Obrázky +======= -Obarveno modifikátorem .{color:blue; lang: cs} -\-- +Základní syntaxe je velmi jednoduchá. Cestu k obrázku (ať už lokálnímu souboru, nebo URL adrese) stačí uzavřít do hranatých závorek s hvězdičkou: -/--code html -<p style="text-align:center">Vycentrováno modifikátorem</p> +```texy +[* obrazek.jpg *] +[* https://domena.cz/logo.png *] +``` -<p style="color:blue" lang="cs">Obarveno modifikátorem</p> -\-- +Často budete chtít, aby text obrázek obtékal. K tomu slouží jednoduché zarovnávací značky, které se vkládají před uzavírací závorku: +```texy +[* obrazek.jpg <] Tento text bude plynule obtékat obrázek z pravé strany. -Typografie .[#typography] -========================= +[* obrazek.jpg >] V tomto případě bude text naopak obtékat obrázek z levé strany. -Sem patří všechny úpravy a náhrady textu, které upravují jeho vzhled v souladu s typografickými pravidly a podobně: +[* velky-obrazek.jpg *] +Tento text bude pokračovat pod obrázkem, který neobtéká. +``` -/--code texy -- "české" 'typografické' uvozovky -- pomlčka vs. spojovník: 10-15 vs. česko-slovenský -- pomlčka: jedna -- dvě -- typografický křížek u rozměrů 10 x 20 -- šipky <- a -> a <-> ; -- tři tečky... -- zachování HTML entit & -- náhrady(TM) nebo(R) za příslušné entity(C) -\-- +Správně vložený obrázek by měl mít i tzv. "alternativní text", který se zobrazí, pokud se obrázek nenačte. Pomocí [modifikátoru|#modifikátory] můžete přidat tento text i další prvky pro stylování. + +```texy +[* fotka-krajiny.jpg .(Krásná horská krajina při západu slunce)[main-photo] *] +``` + + +Rozměry obrázků +--------------- + +Texy umí u lokálních obrázků automaticky zjistit jejich rozměry (pokud je nastavena cesta `$texy->imageModule->fileRoot`) a doplnit je do HTML, což zrychluje načítání stránky. Pokud ale chcete rozměry nastavit ručně, máte několik možností: + +| `[* img.jpg 150x100 *]` | Přesná šířka 150px a výška 100px +| `[* img.jpg 150 *]` | Šířka bude 150px, výška se automaticky dopočítá se zachováním poměru stran +| `[* img.jpg ?x100 *]` | Výška bude 100px, šířka se automaticky dopočítá + + +Klikatelné obrázky +------------------ + +Chcete, aby se po kliknutí na malý náhled zobrazil velký obrázek? Nebo aby obrázek odkazoval na jinou stránku? Stačí za syntaxi obrázku přidat dvojtečku a cílovou URL. + +```texy +[* nahled.jpg *]:velky.jpg +[* logo-nette.png *]:https://nette.org +``` + +Pro galerie existuje i šikovná zkratka `::`. Ta automaticky vytvoří odkaz na stejný soubor umístěný na `$texy->imageModule->linkedRoot`. + + +Viditelný popisek pod obrázkem +------------------------------ + +Pokud chcete pod obrázek přidat viditelný popisek (např. jméno autora nebo popis scény), napište za něj tři hvězdičky `***` a text popisku. Texy z toho vytvoří figure strukturu - standardně `<div class="figure">`, nebo sémantické `<figure>` a `<figcaption>`, pokud nastavíte `figureModule->tagName = 'figure'`. + +```texy +[* fotka.jpg *] *** Toto je popisek. Může obsahovat i **další formátování**. +``` + + +Udržujte si pořádek s referencemi +--------------------------------- + +Pokud v textu používáte jeden obrázek vícekrát nebo chcete mít všechny definice obrázků přehledně na jednom místě, můžete použít reference. V textu použijete jen zástupný název a na konci souboru pak definujete, co tento název znamená. + +```texy +V našem logu [* firemni-logo *] je vidět symbol naší vize. + +​[* firemni-logo *]: /images/logo.svg 200x50 .(Logo naší společnosti) +``` + +Tento přístup výrazně zpřehledňuje hlavní text a usnadňuje správu obrázků. + +ID syntaxe `image/definition` | [ImageModule |configuration#imagemodule] -/--div .[output] -- "české" 'typografické' uvozovky -- pomlčka vs. spojovník: 10-15 vs. česko-slovenský -- pomlčka: jedna -- dvě -- typografický křížek u rozměrů 10 x 20 -- šipky <- a -> a <-> ; -- tři tečky... -- zachování HTML entit & -- náhrady(TM) nebo(R) za příslušné (C)entity + +Předformátovaný text +==================== + +V Texy můžete snadno vložit bloky kódu nebo jakýkoli předformátovaný text, u kterého chcete zajistit, aby se zobrazil přesně tak, jak ho napíšete - včetně všech mezer a konců řádků. To je ideální pro ukázky zdrojových kódů, logů nebo ASCII artu. + +Pro vložení takového bloku použijte ohraničení `/--` a `\--`: + +```texy +/-- +function hello() { + echo 'Hello World'; +} \-- +``` -práce s mezerami: +Aby byl váš kód ještě čitelnější, můžete Texy sdělit, v jakém programovacím jazyce je napsaný, a vytvořit si handler, který například obarví syntaxi, viz "ukázka":custom-handlers#syntax-highlighting. Stačí za úvodní značku `/--` přidat klíčové slovo `code` a název jazyka: -/--code texy -- vkládání nezalomitelných mezer za jednopísmenné předložky (v autě u okna) -- nedělitelné mezery u telefonních čísel +420 776 552 046 +```texy +/--code javascript +console.log('JavaScript'); \-- /--code html -vkládání nezalomitelných mezer za jednopísmenné předložky (v autě u okna) - -nedělitelné mezery u telefonních čísel +420 776 552 046 +<div>Tohle je HTML kód</div> \-- +``` -*Poznámka: Nahrazování se obvykle řídí dalšími pravidly, které určují, kdy -symbol nahradit a kdy ne. Například šipka `->` nemůže být na konci řádku atd. Proto nebuďte překvapeni, když v některých případech Texy náhradu neprovede.* +Obsahové bloky (divy) +===================== -Zkratky, akronymy .[#acronym] ------------------------------ +Texy umožňuje vytvářet obecné `<div>` bloky, díky kterým můžete snadno seskupovat obsah do logických celků a následně je stylovat: -Používá se zápisu s dvojitou kulatou závorkou: +Blok vytvoříte pomocí značek `/--div` a `\--`. Navíc můžete snadno přidat [#modifikátory]: -/--code texy -jednoslovné: NATO((North Atlantic Treaty Organisation)) +```texy +/--div .[important] +## Důležité upozornění -víceslovné: "et al."((a další)) +Tento text bude uzavřen v bloku `<div class="important">`. +Díky tomu ho můžete pomocí CSS nastylovat, aby byl výraznější. \-- +``` +Síla `<div>` bloků spočívá také v možnosti je vnořovat do sebe. Tím můžete vytvářet i složitější struktury přímo v Texy, aniž byste museli psát HTML ručně. -Klikatelné webové adresy ------------------------- +```texy +/--div .[outer] + Toto je vnější blok. -Automatický převod webových adres a emailů do klikatelné formy + /--div .[inner] + A toto je vnořený, vnitřní blok. + \-- -/--code texy -další informace na www.texy.info a také ... + Zde jsme opět ve vnějším bloku. \-- +``` +Díky této jednoduché syntaxi můžete udržovat svůj obsah přehledný a sémanticky správně strukturovaný. -/--div .[output] -další informace na www.texy.info a také ... -\-- +Vypnutí formátování +=================== -Rozdělení velmi dlouhých slov .[#longwords] -=========================================== +Někdy se může hodit Texy na chvíli "vypnout" a vložit kus textu, kde nemá Texy zpracovávat své značky. -Velmi zajímavá a důležitá funkce Texy. Dlouhá slova mohou narušit vzhled stránky, proto je vhodné prohlížeči naznačit, kde je může zalomit. Texy tyto místa hledá s přihlédnutím k národním zvyklostem, tedy slovo rozděluje podle slabik: +Pokud potřebujete vložit komplexnější HTML strukturu bez parsování Texy značek, použijte blok `/--html`: -/--code texy -nejneobhospodařovávatelnějšími -\-- +```texy +/--html +<em>Tento text bude zpracován jako HTML, takže bude kurzívou.</em> -/--code html -nejneobhospoda­řovávatelnější­mi</p +**Ale tyto hvězdičky Texy ignoruje, takže tučné nebudou.** \-- +``` -*Poznámka: limit délky slova je volitelný* +V případě, že chcete zobrazit text přesně tak, jak je napsán, a ignorovat veškeré značky (jak Texy, tak HTML), použijte blok `/--text`. Vše uvnitř tohoto bloku se zobrazí jako obyčejný text. +```texy +/--text +<em>Tento text se zobrazí i se značkami, kurzívou ale nebude.</em> -Tabulky .[#table] -================= +**Ani toto nebude tučné.** +\-- +``` -Příklad jednoduché tabulky, sloupce se oddělují znakem `|` +Co když ale nechcete vypínat Texy pro celý blok textu, ale jen pro krátkou frázi uprostřed věty? Pro tyto případy existuje elegantní a rychlé řešení: obalte daný text do **dvojitých apostrofů** `''`: -/--code texy -| first col | second col | third col -| Adam | Eva | Franta -\-- +```texy +Pokud chcete ukázat, jak se píše tučný text, napíšete: Syntaxe je ''**tučný text**''. +``` -A výsledek je: +Výsledkem nebude tučný text, ale doslova se vypíše řetězec `**tučný text**`. -| first col | second col | third col -| Adam | Eva | Franta +Tabulky +======= -Hlavičku tabulky můžeme definovat tímto zápisem: +Pro vytvoření tabulky začněte každý řádek znakem `|` a jednotlivé buňky oddělujte také tímto znakem. Texy si už samo pohlídá zarovnání a správné HTML. -/--code texy -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Jesus | Christ | 33 -| Cecilie | Svobodova | 74 -\-- +```texy +| Jan | Novák | Praha +| Eva | Svobodová | Brno +``` -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Jesus | Christ | 33 -| Cecilie | Svobodova | 74 +Výsledek bude přehledná a správně naformátovaná tabulka. + +ID syntaxe `table` | [TableModule |configuration#tablemodule] -Sloučení sloupců +Hlavička tabulky ---------------- -všimněte si zdvojeného || +Každá správná tabulka by měla mít hlavičku, která popisuje, co se v jednotlivých sloupcích nachází. Hlavičku vytvoříte tak, že ji od zbytku tabulky oddělíte řádkem obsahujícím pomlčky `-`. -/--code texy -| Name || Age -|---------------------------- -| Jesus | Christ | 33 -\-- +```texy +| Jméno | Věk | Město +|----------|-----|------- +| Jan | 25 | Praha +| Eva | 30 | Brno +``` -| Name || Age -|---------------------------- -| Jesus | Christ | 33 +Alternativně můžete definovat záhlaví pro jednotlivé řádky (například pokud máte v prvním sloupci popisky). Toho dosáhnete přidáním hvězdičky `*` hned za úvodní `|`. +```texy +|* Jméno | Jan | Eva +|* Věk | 25 | 30 +|* Město | Praha | Brno +``` -Sloučení řádků + +Sloučení buněk -------------- -Všimněte si znaku `^` symbolizujícího směr nahoru: +Někdy je potřeba spojit několik buněk dohromady, ať už ve sloupcích nebo v řádcích. +**Sloučení sloupců:** Pro horizontální spojení buněk jednoduše vynechejte oddělovač a místo něj použijte zdvojenou svislou čáru `||`. Buňka napravo se tím sloučí s buňkou nalevo od ní. -/--code texy -| First Name | Last Name | Age +```texy +| Jméno || Věk |---------------------------- -| Bill || 50 -| ^| 52 -| Jim | Beam | 70 -\-- +| Jan | Novák | 25 +``` + +**Sloučení řádků:** Pro vertikální spojení buněk použijte v buňce, kterou chcete připojit k té nad ní, symbol stříšky `^`. Ta Texy říká: "Tuto buňku spoj s tou nad ní." + +```texy +| Měsíc | Prodeje | +|---------|---------- +| Leden | 150 ks | +| Únor | ^| +| Březen | 210 ks | +``` + +V tomto příkladu bude buňka s prodeji pro leden a únor spojená. + +Takto lze sloučit několik buňek napříč řádky a sloupci: +```texy | First Name | Last Name | Age |---------------------------- | Bill || 50 | ^| 52 | Jim | Beam | 70 +``` + + +Stylování tabulek +----------------- + +Stejně jako u jiných prvků v Texy můžete i tabulkám a jejich částem přidávat [#modifikátory] pro změnu vzhledu (např. CSS třídy, styly nebo ID). + +**Celá tabulka:** Modifikátor pro celou tabulku umístěte na samostatný řádek těsně před ni. + +```texy +.[data-table table-striped] +| Hlavička 1 | Hlavička 2 +|------------|------------ +| data | data +``` + +**Jednotlivé řádky:** Chcete-li nastylovat konkrétní řádek, přidejte modifikátor na jeho konec. + +```texy +| Jméno | Stav +|-------|-------------- +| Petr | Schváleno +| Jana | Zamítnuto | .{background: #ffdddd} +``` + +**Jednotlivé sloupce:** Pro nastylování celého sloupce vložte modifikátor na začátek první buňky daného sloupce. + +```texy +| Jméno | .> Cena | Skladem +|----------------|-----------|--------- +| Produkt A | 1 200 Kč | Ano +| Produkt B | 850 Kč | Ne +``` + +**Konkrétní buňka:** Modifikátor pro jednu buňku napište přímo do ní, obvykle na konec jejího obsahu. + +```texy +| Úkol | Status +|----------------------|------------------------------------- +| Připravit podklady | Hotovo +| Zkontrolovat data | Probíhá .{color: orange; font-weight: bold} +``` + + +Citace +====== + +Potřebujete-li ve svém textu zdůraznit myšlenku někoho jiného, ocitovat zdroj nebo jen vizuálně oddělit blok textu, stačí začít řádek znakem `>`. + +```texy +> Toto je citace. Slouží ke zvýraznění důležité myšlenky nebo úryvku z jiného zdroje. +``` + +Citace nemusí být jen jeden odstavec. Pokud chcete pokračovat dalším odstavcem v rámci stejné citace, jednoduše vložte prázdný řádek, který také začíná znakem `>`. + +```texy +> Toto je první odstavec citace. Lorem ipsum dolor sit amet. +> +> A toto je druhý odstavec, který stále patří do stejné citace. +> Tímto způsobem můžete strukturovat i delší texty. +``` + +Texy dokonce podporuje vnořené citace, což se hodí, pokud citujete někoho, kdo sám někoho cituje. Pro každou další úroveň vnoření přidejte další znak `>`. + +```texy +> Toto je vnější, hlavní citace. +> +> > A toto je už vnořená citace druhé úrovně. +> +> Zde se text vrací zpět do hlavní citace. +``` + +Uvnitř citací můžete samozřejmě používat i další formátování, jako je **tučný text** nebo *kurzíva*. + + +Horizontální čáry +================= + +Někdy je potřeba text vizuálně rozdělit. K tomu skvěle slouží horizontální čára. Na samostatný řádek napište tři nebo více pomlček `---` nebo hvězdiček `***`. + +```texy +První část textu o nějakém tématu. + +*** + +Druhá část textu, která začíná po vizuálním oddělení. +``` + +Abyste vytvořili horizontální čáru, **musí jí předcházet prázdný řádek**. Pokud byste ji napsali hned pod text, Texy by si myslelo, že chcete vytvořit podtržený nadpis. + +ID syntaxe `horizline` | [HorizLineModule |configuration#horizlinemodule] + + +Typografie +========== + +Síla Texy nespočívá jen ve formátování, ale také v automatických typografických korekcích. Texy se postará o detaily, které dělají text profesionálním a dobře čitelným, a to vše podle českých typografických pravidel. Vy se tak můžete soustředit jen na obsah. + +**Uvozovky:** Nemusíte řešit, jak na klávesnici napsat správné typografické uvozovky. Texy to udělá za vás. + +Klasické ''"strojopisné uvozovky"'' automaticky převede na správné české „uvozovky“ a vnořené ‚uvozovky‘. Typ uvozovek závisí na nastavení locale: + +```php +$texy->typographyModule->locale = 'cs'; // české +$texy->typographyModule->locale = 'en'; // anglické +``` + +**Pomlčky a spojovníky:** Inteligentně rozpozná, kdy použít krátký spojovník (v dělených slovech), a kdy delší pomlčku - například v rozsazích (10-15) nebo mezi slovy. + +```texy +10-15 → 10–15 (en dash pro rozsahy) +česko-slovenský → česko-slovenský (spojovník zůstává) +slovo -- slovo → slovo – slovo (en dash mezi slovy) +slovo --- slovo → slovo — slovo (em dash) +``` + +**Nezlomitelné mezery**: Jednou z největších výhod je automatické vkládání pevných (nezlomitelných) mezer tam, kde je to potřeba. Tím zabraňuje, aby na konci řádku zůstala osamocená jednopísmenná slova (jako `k`, `s`, `v`, `z`), což je častý typografický prohřešek. + +```texy +// Vy napíšete: +Navštívil jsem hrad v Praze. + +// Texy zajistí, aby "v" nikdy nezůstalo na konci řádku: +Navštívil jsem hrad v Praze. +``` + +Stejně tak se postará o správné mezery v telefonních číslech nebo datech, aby se nezalamovala. + +```texy ++420 776 552 046 → +420 776 552 046 (všechny mezery pevné) +``` + +**Automatické symboly:** Texy vám usnadní i psaní často používaných symbolů. + +| Napíšete | Texy vygeneruje | Popis +|----- +| `...` | … | Výpustka +| `(c)` | © | Copyright +| `(r)` | ® | Registrovaná známka +| `(tm)` | ™ | Trademark +| `10 x 5` | 10 × 5 | Znak násobení +| `+-` | ± | Plus-mínus +| `<-` `->` `<->` | ← → ↔ | Šipky (obklopte mezerami) + +Díky těmto automatickým úpravám bude váš text vždy vypadat profesionálně, aniž byste museli znát složité klávesové zkratky nebo HTML entity. + + +Dělení dlouhých slov +-------------------- + +Znáte to - v textu se objeví dlouhé slovo, jako například "nejneobhospodařovávatelnějšími", a na úzké obrazovce mobilního telefonu rozbije celý layout stránky. Texy naštěstí nabízí elegantní řešení: dokáže do slova vložit neviditelné "měkké rozdělovníky" (`­`). Tyto rozdělovníky prohlížeči napoví, na kterých místech (mezi slabikami) může slovo bezpečně zalomit, pokud se na konec řádku nevejde. Pokud se slovo na řádek vejde celé, rozdělovníky zůstanou skryté a nic se nestane. + +```latte +nejneobhospoda­řovávatelnějšími +``` + +Díky tomu se váš text vždy krásně přizpůsobí jakékoliv šířce obrazovky bez nechtěného horizontálního posouvání. + +Protože se tato funkce nehodí pro všechny typy webů, je ve výchozím stavu vypnutá. Aktivovat ji můžete v konfiguraci: + +```php +$texy->allowed['longwords'] = true; + +// Nastavit minimální délku slova, od které se má dělit (např. 20 znaků) +$texy->longWordsModule->wordLimit = 20; +``` + +ID syntaxe `longwords` | [LongWordsModule |configuration#longwordsmodule] + + +Emotikony +========= + +Texy umí automaticky převádět klasické textové smajlíky na grafické emotikony. Jednoduše napište smajlíka tak, jak jste zvyklí, a Texy se postará o zbytek. + +| Napíšete | Texy vygeneruje +|----- +| `:-)` | 🙂 +| `:-(` | ☹ +| `;-)` | 😉 +| `:-D` | 😁 +| `:-P` | 😛 + +Podle konfigurace může Texy tyto zkratky převádět buď na moderní Unicode emoji (jako v tabulce výše), nebo na malé obrázky (`<img>`). + +Aby se předešlo nechtěným převodům například v technických textech, je tato funkce ve výchozím nastavení vypnutá. Pokud ji chcete používat, stačí ji jednoduše povolit: + +```php +$texy->allowed['emoticon'] = true; +``` + +Více informací o dostupných emotikonech a možnostech nastavení naleznete v [konfiguraci EmoticonModule |configuration#emoticonmodule]. + +ID syntaxe `emoticon` | [EmoticonModule |configuration#emoticonmodule] diff --git a/texy/cs/texy-vs-wysiwyg.texy b/texy/cs/texy-vs-wysiwyg.texy deleted file mode 100644 index c79d99679e..0000000000 --- a/texy/cs/texy-vs-wysiwyg.texy +++ /dev/null @@ -1,23 +0,0 @@ -Texy versus WYSIWYG editory -*************************** - - -WYSIWYG((What You See is What You Get)) editor zobrazuje dokument během editace takové podobě, v jaké bude vytištěn, zobrazen na webu atd. Příkladem takového editoru je Word. - -Tento druh editorů zažil v oblasti správy internetového obsahu skutečný boom. Kdo by také odolal jejich vizuálně atraktivnímu prostředí a myšoidnímu ovládání. Díky programátorům "šikovných komponent .[about](htmlArea, FCKeditor nebo tinyRTE)" se jejich implementace stala hračkou a dnes je nabízí každý CMS((Content Management Systém = Systém na správu obsahu)). V praxi se však ukazuje, jak jsou jejich **přednosti jen zdánlivé**. - - -WYSIWYG se pro web nehodí -------------------------- - -Web má totiž docela **jiná specifika** než tištěný dokument. Zatímco u tiskovin je prioritou vizuálním uspořádání (ze kterého si lidský mozek odvodí strukturu, tj. co je nadpis, co je text a co je popiska obrázku), u webu je základem struktura. - -S vizuálním editorem se vlastně neustále svádí boj. Nejprve bojuje tvůrce administračního rozhraní, který se jej snaží přinutit **generovat použitelný kód**. Poté s ním bojuje samotný uživatel, který se snaží s využitím plné škály dostupných barev a fontů vytvoří hrozivou stránku. Následuje snaha ořezat možnosti editoru, kterou střídá další touha uživatele využít jeho plný potenciál. - -Současné WYSIWYG editory se pro web zkrátka nehodí. Prohlašuji to s vědomím autora několika administračních rozhraní, které jej využívají. Potřeboval jsem přijít s vhodnějším nástrojem a tak vzniklo Texy - - -WYSIWYM -------- - -WYSIWYM znamená What You See Is What You Mean. diff --git a/texy/cs/try-settings.texy b/texy/cs/try-settings.texy index 66be081ed6..35ae3d7af0 100644 --- a/texy/cs/try-settings.texy +++ b/texy/cs/try-settings.texy @@ -21,9 +21,9 @@ function blockHandler( Texy\HandlerInvocation $invocation, string $blocktype, string $content, - string $lang, + ?string $lang, Texy\Modifier $modifier -): Texy\HtmlElement +): Texy\HtmlElement|string|null { if ($blocktype !== 'block/code') { // nothing to do diff --git a/texy/en/@home.texy b/texy/en/@home.texy index eebdc1e671..ca11b033d4 100644 --- a/texy/en/@home.texy +++ b/texy/en/@home.texy @@ -1,31 +1,120 @@ -What Is Texy ------------- +Texy! is sexy! +************** -Texy allows you to enter content using an **easy to read** [Texy syntax | syntax] which is filtered into *structurally valid* HTML. No knowledge of HTML is required. Texy is one of the most complex formatting tools. It allows adding of images, links, nested lists, tables and has full support for CSS. +.[perex] +Texy is a **powerful and secure markup processor** for PHP that converts simple text into valid HTML. Unlike other markup languages, Texy isn't just another Markdown variant - it's a **fully configurable system** that you can adapt to virtually any syntax. -Texy supports hyphenation of long words (which reflects language rules), clickable emails and URL (emails are obfuscated against spambots), national typographic single and double quotation marks, ellipses, em dashes, dimension sign, nonbreakable spaces (e.g. in phone numbers), acronyms, arrows and many others. -Texy is being developed by David Grudl since 2004. It is written in PHP. +Why Texy? +========= -Texy is licenced by BSD and GNU General Public License. Plugins for several content-management systems are available. +Security as a Priority +---------------------- -Features --------- +Texy is designed with security in mind. It automatically **protects against XSS attacks**, validates URLs, and filters dangerous HTML tags. The built-in `safeMode()` is ideal for processing user-generated content in comments or forums. -- simple, intuitive and human friendy markup -- generate clean and valid HTML code -- may be used together with syntax highlighter -- designed with proper typography in mind -- supports hyphenation +```php +Texy\Configurator::safeMode($texy); +// Texy is now safe for user-generated content +``` -Why Texy --------- +Full Configurability +-------------------- -- bulletproof - ensures the well-formedness of the resulting code -- predectable behaviour -- flexible configuration +Want to use Markdown syntax? Or need completely custom markup? **Texy can handle it.** You can: + +- Disable or enable any parts of the syntax +- Change default behavior using handlers +- Add entirely custom syntax elements +- Configure Texy to process Markdown or any other format + +```php +$texy = new Texy; +$texy->allowed['image'] = false; // disable images +$texy->allowed['phrase/strong'] = false; // disable bold text +``` + + +Advanced Typography +------------------- + +Texy provides **sophisticated typographic processing** that can be tailored to different languages. Depending on the configured locale, it automatically: + +- Inserts **non-breaking spaces** after single-letter prepositions and conjunctions +- Applies **word hyphenation** according to syllable rules +- Uses proper **typographic quotation marks** based on language conventions +- Applies proper **dash and hyphen distinction**: 10-15 (dash) vs. multi-word (hyphen) +- Adds **non-breaking spaces** in phone numbers: +420 776 552 046 + + +Valid and Well-Formed HTML +-------------------------- + +Texy always generates **valid HTML5 code**. It automatically corrects improperly nested tags, closes unclosed elements, and ensures proper document structure. The output is not only valid but also **beautifully formatted** with proper indentation. + + +What is Texy? +============= + +Texy is a **general-purpose markup text processor**. While it has its default syntax (similar to Markdown but much richer), you can completely change or extend it. + +**It's not just a parser** - Texy is a comprehensive system with modular architecture, where each module processes a specific part of the syntax (headings, links, images, tables...). Thanks to the handler system, you can intervene at any point in the processing and modify the result according to your needs. + + +Texy vs. Markdown +================= + +The basic syntax is similar, but Texy offers much more: + +|--------------------------- +| Feature | Markdown | Texy +|--------------------------- +| Bold text | `**text**` | `**text**` +| Italic | `*text*` or `_text_` | `*text*` or `//text//` +| Headings | `# Heading` | `# Heading` or underline +| Images | `![alt](url)` | `[* url *]` +| Tables | limited | full support including merging +| Modifiers | no | yes - `.{color:red}[class]` +| Typography | no | yes - quotes, dashes, spaces +| Word hyphenation | no | yes - by syllables +| Configurability | limited | complete - custom syntax +| Security | depends on impl. | built-in (safeMode) + +**Example of differences:** + +```texy +Markdown: +![Image](image.jpg) + +Texy: +[* image.jpg 300x200 .(Image caption)[photo] <] +``` + +Texy allows you to define dimensions, classes, alignment, and much more directly in the syntax. + + +When to Use Texy? +================= + +Texy is ideal for: + +**CMS systems** Need to safely process content from editors? Texy offers granular control over what users can use. + +**Blogs and documentation** Rich syntax for tables, images with captions, typography, and code with syntax highlighting. + +**Comments and discussion forums** SafeMode ensures users cannot insert dangerous code while still having text formatting available. + +**Projects with custom requirements** Need to embed YouTube videos? Special syntax for your macros? Custom markup language? With Texy, you can create it easily. + + +History +======= + +Texy was created by David Grudl **over 20 years ago** in 2004 as one of the first markup processors for PHP. Originally developed for **PHP 4**, it has undergone many updates throughout its long history and today fully leverages all the capabilities of **PHP 8**. + +Over two decades of active development mean a **proven and stable** library trusted by hundreds of projects. Texy is now a **mature solution** with extensive history, yet still actively maintained and modern. {{maintitle: Texy – human friendly markup for PHP}} diff --git a/texy/en/@menu.texy b/texy/en/@menu.texy index 080eb567c1..66743bb1f1 100644 --- a/texy/en/@menu.texy +++ b/texy/en/@menu.texy @@ -1,6 +1,6 @@ - [home | @home] -- [syntax briefly | syntax] -- [syntax in detail | syntax-full] -- [fiddle | https://fiddle.nette.org/texy/] -- [API | https://api.nette.org/texy/] -- [GitHub | https://github.com/dg/texy] +- [syntax | syntax] +- [for developers | develop] +- "Playground .[link-external]":https://fiddle.nette.org/texy/ +- "API .[link-external]":https://api.nette.org/texy/ +- "GitHub .[link-external]":https://github.com/dg/texy diff --git a/texy/en/@try.texy b/texy/en/@try.texy deleted file mode 100644 index 77c2fe7dac..0000000000 --- a/texy/en/@try.texy +++ /dev/null @@ -1,15 +0,0 @@ -Welcome! --------- - -You can use Texy if you like: -- **bold** font or *italic* -- and this is how to "link":https://texy.info -- see "syntax":[syntax] for more information - - -But you can also stay with HTML: -- like this <b>HTML</b> -- Or even <b class=xx>totally <i>stupid</b>, Texy will solve it - - -[syntax]: /en/syntax diff --git a/texy/en/architecture.texy b/texy/en/architecture.texy new file mode 100644 index 0000000000..53fde84ed1 --- /dev/null +++ b/texy/en/architecture.texy @@ -0,0 +1,425 @@ +Architecture and Principles +########################### + +.[perex] +Texy is a tool for converting text written in its own markup language into HTML. Unlike simple converters that process text linearly through a series of replacements, Texy uses a sophisticated system based on parsing, a modular architecture, and the gradual construction of a DOM tree. + +The basic processing flow consists of four main phases: + +1. Text preprocessing - normalization, adjustment of spaces and tabs, calling notification handlers for preparation +2. Parsing - recognizing syntaxes using regular expressions and gradually building the DOM tree +3. Post-processing - typographic adjustments, handling long words, well-forming HTML +4. Final assembly - converting the DOM tree into an HTML string + +The key difference from naive approaches is the separation of the syntax recognition phase from the processing phase. The parser first identifies where each syntactic construct is located in the text, and only then passes the found parts to individual modules for processing. This allows for nesting syntaxes and their gradual expansion. + +*Note: all classes are in the `Texy` namespace, so if the document mentions a class like `HtmlElement`, its full name is `Texy\HtmlElement`. Modules are in the `Texy\Modules` namespace* + + +Key Components +============== + +The Texy architecture consists of several main components, each with a clearly defined responsibility: + +The Texy class acts as the central orchestrator of the entire system. It contains references to all modules, manages registered syntaxes and handlers, maintains the processing state, and coordinates the individual conversion phases. It is the only place where the individual components are interconnected. + +**[Modules|#Modules]** represent functional units responsible for specific areas of the markup language. Each module, upon its construction, registers the syntaxes it recognizes and the element handlers that process them. For example, PhraseModule handles inline formatting like bold or italic text, while TableModule processes tables. Modules are designed as separate, reusable units with their own configuration accessible through public properties. + +**[Parsers|#Parsers]** exist in two variants depending on the type of content being processed. BlockParser processes block structures like paragraphs, headings, lists, or tables. It goes through the text line by line, looking for the beginnings of block constructs and passing them to *syntax handlers*. LineParser handles inline syntaxes within lines - links, images, text formatting. Unlike BlockParser, it allows for nesting syntaxes and their gradual expansion. + + +Basic Terminology +================= + +To correctly understand how Texy works, it is necessary to distinguish between several key concepts that frequently appear in the documentation. + +**Syntax** refers to a named syntactic construct of the markup language. Each syntax has a unique name, for example, `phrase/strong` for bold text or `image` for images. The syntax name is used to enable or disable it in the `Texy::$allowed` array and is passed as a parameter to syntax handlers to distinguish which specific syntax was found. + +**Pattern** is a regular expression that defines what the syntax looks like in the text. The pattern is an implementation detail of the syntax - the author of the syntax must write a regex that recognizes it, but from the perspective of a Texy user, the syntax name and its meaning are more important. One module typically registers multiple syntaxes with different patterns. + +**Syntax handler** is a function called by the parser when it finds an occurrence of a syntax in the text. It receives the found text and returns an `HtmlElement` or a string, which is inserted in the original place. The syntax handler is where the decision is made about what to do with the found syntax - it typically invokes an element handler for the actual processing. + +**Element** is an item for which an HTML representation is generated. For example, `image` is an element for images, `linkURL` for links, `phrase` for inline formatting. Each element has its default element handler that takes care of standard processing. + +**Element handler** is a function registered for a certain type of element and called through the HandlerInvocation system. A characteristic feature is the use of the `proceed()` method, which allows delegating processing to the next handler in the chain or to the module's default handler. Element handlers are used to modify or replace the default behavior. + +**Notification handler** is a function called to notify about a certain event. Unlike element handlers, it does not return any value and cannot influence the processing result. It is used for data preparation, logging, or modifying the already created DOM tree. + +The difference between the various handlers is key to understanding the architecture. A syntax handler is tightly coupled with the parser and a specific pattern - it addresses the question of *what to do when the parser finds this pattern*. Element handlers are at a higher level of abstraction - they address the question of *how to process this type of element*, regardless of which specific syntax created it. + + +Overall Processing Flow +======================= + +When Texy receives input text, it goes through the following processing procedure. + +During preprocessing, the text is normalized. Line endings are unified to the Unix format, spaces are standardized, and tabs are optionally replaced with spaces. Subsequently, *notification handlers* registered for the `beforeParse` event are invoked. These handlers can perform data preparation, such as loading reference definitions or adjusting the configuration based on the text content. + +The parsing itself begins with the creation of a root `HtmlElement`, which represents the document. Texy then decides whether to process the text as a single line or as a complete document with block structures. In the case of block processing, a BlockParser is created, which sequentially goes through the text and looks for individual block constructs. + +LineParser works differently than BlockParser. It does not traverse the text linearly but progressively searches for the nearest occurrence of any registered syntax. When it finds one, it calls the corresponding syntax handler, which creates the appropriate HTML element. This element is inserted back into the text using special masking, and the parser continues. This allows it to find and process syntaxes nested inside already processed constructs. + +After parsing is complete, a full DOM tree representing the document's structure is created. Texy invokes notification handlers for the `afterParse` event, which can perform final modifications to the tree, such as adding identifiers to headings or building a table of contents. + +Post-processing occurs during the conversion of the DOM tree to an HTML string. Each element is recursively converted to HTML code, during which typographic adjustments like replacing quotes, dashes, or inserting non-breaking spaces are applied. Furthermore, HTML well-forming is performed - automatic closing of tags, correction of improperly nested elements, and formatting and indentation of the code. + +The final phase is decoding all masked parts back to HTML tags, removing helper markers, and assembling the resulting HTML string. + + +Syntax System +************* + +In Texy terminology, a syntax represents a named syntactic construct of the markup language. It is an abstract concept connecting several elements: a unique name, a regular expression for recognition, and a method of processing. The syntax name serves as an identifier throughout the system - it is used in the `Texy::$allowed` array for enabling or disabling, passed to handlers to distinguish the type of construct, and appears in documentation and configuration files. + +Syntax naming conventions follow two main patterns. Simpler syntaxes have a single-word name corresponding to their purpose, for example, `image`, `table`, or `script`. More complex areas use hierarchical naming with a slash, for example, `phrase/strong`, `phrase/em`, or `link/reference`. The slash serves to logically group related syntaxes and facilitates bulk operations with them. + + +Line Syntax +=========== + +Line syntaxes are used to recognize inline elements within lines of text. Typically, this includes formatting like bold or italic text, links, images, or inline code. A characteristic of line syntaxes is that they can be nested within each other, and the parser expands them sequentially. + +A line syntax is registered by calling `Texy::registerLinePattern()` with several parameters. The first is the syntax handler, i.e., the callback called upon finding a match. The second parameter is the regular expression defining the syntax's appearance in the text. The third parameter is the syntax name used throughout the system. An optional fourth parameter is another regex to test if it's even worth searching for the pattern - it's used for optimization to avoid running a complex pattern on text that definitely cannot match. + +The pattern as a regular expression must adhere to certain rules. It must not be anchored to the beginning of the text because it is searched for anywhere in the line. It should be as specific as possible to avoid false matches. + +Inline syntaxes within lines of text are processed by the [LineParser|#LineParser]. When it finds a match, it calls the appropriate syntax handler. This handler receives three parameters. The first is the LineParser instance, which provides access to the Texy object and other contextual information. The second parameter is an array with the results of the regex match, including sub-expressions. The third parameter is the syntax name, which is useful when the same callback handles multiple syntaxes. The handler must return either an `HtmlElement`, a string, or null if it refuses to process. + + +Block Syntax +============ + +Block syntaxes recognize multi-line block constructs such as headings, lists, tables, quotes, or special blocks. Unlike line syntaxes, block syntaxes never overlap - each line of text belongs to at most one block construct. + +Registering a block syntax uses `Texy::registerBlockPattern()` with three parameters: a syntax handler, a regular expression, and the syntax name. The pattern as a regular expression must adhere to certain rules. It must match from the beginning of the line and often contains an anchor for the end of the line. BlockParser automatically adds the `m` (multiline) modifier, so the pattern should not contain it. + +Block syntaxes within a document are processed by the [BlockParser|#BlockParser]. When it finds a match, it calls the appropriate syntax handler. This handler receives similar parameters as with line syntaxes - a BlockParser instance, an array with the match, and the syntax name. It returns an `HtmlElement` representing the entire processed block, or null if it refuses processing. + + +Enabling and Disabling Syntax +============================= + +The `Texy::$allowed` array provides fine-grained control over which syntaxes are active in Texy. It is a simple yet powerful mechanism for configuring behavior without needing to change the modules' code. When you disable the `phrase/strong` syntax with this setting, the parser stops looking for the bold text construct: + +```php +$texy->allowed['phrase/strong'] = false; +``` + +The check is performed once at the beginning of parsing, so dynamically changing `$allowed` during processing has no effect. + +When constructing modules, a default value is set in `$allowed` for most syntaxes. Some syntaxes are enabled by default because they form the basis of the markup language. Others are disabled because they are advanced or potentially dangerous. For example, emoticons are disabled because not every document needs them, while basic formatting is enabled. + +Safe mode is a situation where you are processing untrusted input, such as user comments. You want to allow basic formatting but disable images, scripts, or HTML tags. `Texy\Configurator::safeMode()` sets `$allowed` for a safe combination of syntaxes. It disables images, reference definitions, and HTML comments, restricts HTML tags to a safe subset, and forces `rel="nofollow"`, while leaving links and basic formatting enabled. + + +Parsers +******* + + +Syntax Handler +============== + +As we mentioned in the previous section, LineParser or BlockParser goes through the text and looks for all registered patterns. When it finds a match, it calls the appropriate syntax handler and passes it information about the find - particularly an array with the results of the regex match. + +The syntax handler analyzes the found text and prepares the data for processing. It can extract parts of the text from regex groups, create helper objects like `Link` or `Image`, and parse modifiers. It also decides which element handler to invoke. It calls `Texy::invokeAroundHandlers()` with the element name and the prepared parameters. This begins their execution. The returned result is passed back to the syntax handler, which returns it to the parser. + + +Element Handler +=============== + +Element handlers implement the chain of responsibility pattern, which allows the final behavior to be composed from multiple layers. + +An element handler is registered by calling `Texy::addHandler()` with two parameters - the element name and the handler function. A single element name can have multiple handlers registered, which are then executed in order from the last registered to the first. + +The element name identifies the type of processing, for example, `phrase` for formatting, `image` for images, or `link` for links (note: this is different from syntax names). Sometimes, composite names like `linkReference` or `linkEmail` are used to distinguish different kinds of links. The names are more general than syntax names - while the `phrase/strong` syntax is a specific construct, the `phrase` element covers all kinds of inline formatting. + +Invoking an element handler uses the `Texy::invokeAroundHandlers()` method. This method receives the element name, the parser instance, and an array of parameters. It creates a HandlerInvocation object that encapsulates the entire chain of registered handlers. The first handler in the chain gets control and decides whether to call `HandlerInvocation::proceed()` to continue to the next handler or to return its own result. + +The HandlerInvocation object is key to understanding how the chaining works. It contains a stack of all handlers for the given element and the current position in this stack. When a handler calls `proceed()`, HandlerInvocation moves the position back one place in the stack and calls the next handler. If a handler calls `proceed()` with modified parameters, these new parameters are passed to all subsequent handlers. If a handler does not call `proceed()` at all, the chain is interrupted, and its return value becomes the result of the entire processing. + +The order of handler execution is from the last registered to the first. This means that a user-defined handler registered additionally gets control first and can decide whether to call the module's default handler at all. This order allows users to override the default behavior without needing to change the module's code. + +A typical use of an element handler looks like this. The handler checks the input parameters and decides if it wants to intervene in the processing. If so, it modifies the data, calls `proceed()` with the new parameters, and possibly modifies the returned result further. If the handler wants to completely replace the default processing, it creates its own result and returns it without calling `proceed()`. + + +Notification Handler +==================== + +Notification handlers represent a simpler, one-way communication mechanism. Unlike element handlers, they are not used for data transformation but for performing side actions. + +Registering a notification handler uses the same `Texy::addHandler()` method as element handlers. The difference is in how the handler is used - a notification handler returns no value and does not have access to HandlerInvocation. The first parameter is the event name. Descriptive names like `beforeParse` and `afterParse` are used for global events around parsing, or more specific ones like `afterTable`, `afterList`, `afterBlockquote` for events after a specific structure is created. The before/after prefix clearly indicates the timing of the event. + +Invoking notification handlers uses the `Texy::invokeHandlers()` method. This method simply calls all registered handlers in order and ignores their return values. Notification handlers receive the parameters passed during invocation but cannot change them for other handlers in the chain. + +Typical uses for notification handlers include several scenarios. A handler for the `beforeParse` event can load reference definitions from the text before parsing begins. A handler for `afterParse` can traverse the created DOM tree and add missing attributes or build a table of contents. Handlers like `afterTable` or `afterList` allow modules to perform final adjustments to the created structures. + +An important difference from element handlers is that notification handlers cannot prevent further processing. All registered handlers are always executed; none can break the chain. This is intended behavior - notification handlers are about side effects, not flow control. + + +LineParser +========== + +LineParser processes inline syntaxes within lines of text in a sequential manner that allows for nesting and complex interactions between syntaxes. + +The basic principle lies in finding the first occurrence of any syntax. In each iteration, it goes through all syntaxes and determines which one matches closest to the current position in the text. This syntax *wins* and is processed. If multiple syntaxes match at the same position, the one that was registered earlier wins - this is a priority based on registration order. + +When the parser finds the nearest match, it calls the corresponding syntax handler. This handler returns a result, which can be an `HtmlElement` or a string. This result then overwrites the found match in the text. + +Then, it searches again from the current position. This system ensures that the parser always sees the current state of the text. When we replace a match with new text that may contain other syntaxes, these syntaxes will be found in the next iteration. + +The `$again` property on the LineParser object is used for fine-grained control over whether the just-matched syntax should be searched for again at the same position after processing the current match. The default value is false, which says: *It no longer makes sense to look for this same syntax at this position. Move on.* + +The traversal ends when the parser reaches the end of the text or when no syntax has any more matches. The result is text where all recognizable syntaxes have been processed and replaced with their results, ready for final conversion. + + +Nesting +------- + +The ability to process nested syntaxes is one of the key features of LineParser and presents a fundamental challenge - how to prevent already processed HTML tags from being mistakenly interpreted as another syntax to be processed. + +When the parser processes text containing nested syntaxes, it first finds the outer construct. For example, in the text `"link **bold** text":URL`, the parser first finds the syntax for a link with quotes. The pattern for this syntax matches the entire string from the first quote to the colon and URL. The syntax handler creates an `HtmlElement` for the `<a>` tag, and the content `link **bold** text` is added as a child of the element. This string is inserted back into the text, and the parser continues searching for other syntaxes (`**bold**`, which represents bold text). + +But now it has a problem - there are also HTML tags in the text, which could match as the beginning of another syntax. The parser would start processing the already finished HTML tags as if they were part of the original text. + +We don't want the parser to see HTML tags. We need some way to distinguish already processed parts from parts waiting to be processed. The `Texy::protect()` method solves these problems in an elegant way - it replaces HTML tags with a unique placeholder composed of control characters - special bytes outside of printable ASCII. + +So, when an `HtmlElement` is converted to a string (using `toString()`), the result doesn't look like `<a href="...">link **bold** text</a>`, but for example, like `\x17\x18\x19\x17link **bold** text\x17\x18\x1A\x17`. + +Thus, during parsing, there are never actual HTML tags present in the text. Instead, there are only placeholders. But the inner text remains, and the parser sees it normally and can search for other syntaxes within it. This allows for gradual nesting - the outer syntax is masked, but its content is still accessible for inner syntaxes. + +At the end of processing, the `Texy::unProtect()` method goes through the resulting HTML string and replaces all placeholders with their actual values. Only at this moment do the actual HTML tags get into the output. + + +Masking Levels +-------------- + +Different types of content use different control characters for their placeholders, which allows syntaxes to selectively decide what they can contain. + +- `Texy::CONTENT_MARKUP` denotes regular HTML markup like tags for formatting or links. It is the most common type and is used by most inline elements. The placeholder begins and ends with `\x17`. +- `Texy::CONTENT_REPLACED` denotes content that has been replaced by something else, typically images or other replaced elements. It uses `\x16` as a marker. +- `Texy::CONTENT_TEXTUAL` denotes text that has been escaped or otherwise treated to prevent processing. It is used for constructs like code or notexy, where we want to display the original text including markup symbols, not their interpretation. +- `Texy::CONTENT_BLOCK` denotes block elements. It is the lowest level in the hierarchy. It uses `\x14` as a marker. + +The hierarchy of these types is not just a convention but has a practical consequence. The constant Patterns::MARK is defined as `\x14-\x1F`, i.e., a range covering all these types plus a reserve. Syntaxes use this constant in their patterns to exclude masked parts. + +Different syntaxes may have different requirements for what placeholders they can contain. A pattern that wants to see only plain text without any masked parts will use the exclusion `[^\x14-\x1F]`. This will reject all placeholders of all types. An example is the pattern for images - an image URL should not contain any HTML tags or blocks. + +A pattern that accepts lower levels but rejects higher ones will use a narrower range. For example, `[^\x17-\x1F]` will only reject `CONTENT_MARKUP` and above, but will accept `CONTENT_BLOCK`, `CONTENT_TEXTUAL`, and `CONTENT_REPLACED`. This is useful if we want to allow blocks but not inline markup. A practical example is TypographyModule, which performs typographic adjustments like replacing quotes or inserting non-breaking spaces. These adjustments should be applied to regular text, but not inside code blocks or preformatted text. + + +Syntax Collisions +----------------- + +A collision occurs when multiple syntaxes can match at the same position, and the system must choose one of them. + +A typical example is different lengths of the same symbol. The `phrase/strong+em` syntax uses three asterisks for a combination of bold and italics. The `phrase/strong` syntax uses two asterisks for bold text alone. The `phrase/em-alt` syntax uses one asterisk for italics. When the parser finds text starting with three asterisks, all three syntaxes can technically match. + +PhraseModule resolves this collision by registering syntaxes in order from longest to shortest. First, it registers `phrase/strong+em` with a pattern for three asterisks. Then `phrase/strong` with a pattern for two asterisks. Finally, `phrase/em-alt` with a pattern for one asterisk. Thanks to this order, when three asterisks are found, `phrase/strong+em` is processed first, and the shorter syntaxes don't get a chance. + +Another example is links in different formats. The `phrase/wikilink` syntax uses a pattern for `[text|url]`. The `link/reference` syntax uses a pattern for `[ref]`. Both start with an opening square bracket. If the text contains `[text|url]`, both patterns can technically start to match. + +The solution, again, is the specificity of the patterns. The pattern for `phrase/wikilink` is more specific - it requires a vertical bar inside the brackets. If the text contains a vertical bar, `phrase/wikilink` will match. If not, the pattern will fail, and `link/reference` gets a chance. The order of registration also plays a role here - `phrase/wikilink` should be registered before `link/reference`. + + +BlockParser +=========== + +BlockParser uses a fundamentally different approach to processing that reflects the nature of block constructs. The basic difference is the absence of intertwining. While LineParser allows syntaxes to be nested within each other and gradually expanded, BlockParser works with the assumption that each block is a separate unit. A single line or a group of lines belongs to at most one block. Blocks do not overlap, cross, or nest at the BlockParser level. + +BlockParser starts by finding all blocks, or rather their beginnings. The parser goes through all registered block syntaxes and finds all their occurrences. If multiple syntaxes match at the same position, the registration order is used - the earlier registered syntax takes precedence. + + +API for Syntax Handler +---------------------- + +BlockParser provides syntax handlers with an API for working with multi-line structures. + +The `BlockParser::moveBackward()` method is used to return to previous lines. It accepts the number of lines to go back. The parser moves its internal position towards the beginning of the text until it passes the specified number of line endings. This allows the callback to start reading from the beginning of the structure, even if the pattern matched in the middle or at the end. + +The `BlockParser::next()` method is used to read the next line matching a certain pattern. It accepts a regex pattern (it automatically adds the `Am` modifiers) and a reference to a variable for the match result. If the next line in the text matches the provided pattern, the method fills the result, moves the internal position past this line, and returns true. If the next line does not match, the method returns false, and the position does not change. + + +Modules +******* + +Modules are the basic organizational unit in the Texy architecture. Each module encapsulates the complete functionality for a specific area of the markup language. + +The primary responsibility of a module is to register syntaxes. In its constructor, the module calls `Texy::registerLinePattern()` or `registerBlockPattern()` for all the syntaxes it wants to process. This tells the parser: *When you find these patterns, call me.* The module thus defines which constructs in the text it recognizes. + +The second responsibility is the implementation of element handlers. The module registers handlers for the elements that its syntaxes invoke. These handlers contain the logic for converting the found constructs into HTML elements. The element handler decides what element to create, what attributes to set, and how to process the content. + +The third responsibility is to provide configuration. Modules have public properties that allow Texy users to modify the module's behavior without needing to change its code. For example, ImageModule has properties for setting the root path to images or the default alt text. + +The fourth responsibility is managing module-specific state. For example, HeadingModule keeps track of all found headings in the TOC array for building a table of contents. LinkModule manages a dictionary of references for links. This state is private to the module, and other parts of the system do not access it directly. + +Modules are designed as independent units. Each module can function on its own and should not depend on the implementation details of other modules. Communication between modules occurs through shared objects like `Link` or `Image`, not through direct method calls. + + +Structure of a Typical Module +============================= + +Most modules in Texy follow a similar structure that reflects their role in the system. + +The module inherits from the base class Module, which provides access to the Texy object via the protected property `$texy`. The module's constructor accepts a Texy instance and stores it. This allows the module to access the configuration and call methods on the Texy object. + +All initialization takes place in the constructor. The module sets the default values of its configuration properties, and possibly sets default values in the `Texy::$allowed` array for its syntaxes. Then it registers its syntaxes by calling `registerLinePattern()` or `registerBlockPattern()`. Each registration associates a pattern, a syntax handler, and a syntax name. Finally, the module registers its element handlers by calling `addHandler()`. + +Syntax handlers are methods of the module that the parser calls when it finds a syntax. These methods typically extract parts from the regex match, create helper objects, and invoke element handlers. The syntax handler decides which element handler to invoke and what parameters to pass. + +Element handlers are methods that implement the actual processing. They receive a HandlerInvocation object as the first parameter, followed by parameters specific to the given element. The element handler creates an `HtmlElement`, applies modifiers, processes the content, and returns the result. This is where the final form of the HTML is decided. + +Public properties serve as the interface for configuration. A Texy user can set these properties to customize the module's behavior. The properties are typically primitive types or arrays, not complex objects, to keep configuration simple. + + +Overview of Key Modules +======================= + +The standard distribution of Texy includes several modules covering various aspects of the markup language. + +- **PhraseModule** processes inline text formatting. It registers syntaxes for bold text, italics, inserted and deleted text, superscript, subscript, code, and more. All these syntaxes invoke a common handler for the `phrase` element, and the handler distinguishes which tag to create based on the syntax name. The module allows configuring which tags are used for each type of formatting. + +- **LinkModule** manages links in the document. It registers syntaxes for various link formats - explicit URLs, email addresses, references to defined links. It provides factory methods for creating `Link` objects and manages a dictionary of references. The module allows configuring the root for relative links, automatic `rel="nofollow"` for external links, and shortening of long URLs. + +- **ImageModule** processes images in a similar way to how LinkModule handles links. It registers syntax for inline images and manages a dictionary of references to defined images. It provides factory methods for creating `Image` objects and automatic detection of image dimensions. Configurable options include paths to images, default alt text, and CSS classes for alignment. + +- **HeadingModule** recognizes headings in various formats - underlined with dashes or equal signs, surrounded by hash marks. It collects all headings into a TOC array for a possible table of contents. It allows configuring the generation of IDs, the top level of headings, and the level balancing mode. + +- **ListModule** processes lists - unordered, ordered, and definition lists. It recognizes different types of bullets and automatically detects nesting based on indentation. It allows configuring which characters serve as bullets and what HTML lists to generate. + +- **TableModule** is one of the most complex modules. It recognizes tables with headers, bodies, captions, and supports colspan and rowspan. It processes modifiers for both rows and cells. + +- **BlockModule** processes special blocks delimited by `/--` and `\--`. It supports various block types - code for code, html for direct HTML, div for a generic container. It allows users to define custom handlers for their own block types. + +- **TypographyModule** performs post-processing for typographic adjustments. It replaces three dots with an ellipsis, double dashes with an en-dash, straight quotes with typographic ones, and inserts non-breaking spaces. It operates at the level of the final string between block elements. + +- **HtmlOutputModule** formats the final HTML output. It ensures well-formed HTML by automatically closing tags, correcting incorrect nesting, indenting the code, and wrapping long lines. It allows configuring the indentation level and line width. + + +Interaction Between Modules +=========================== + +Although modules are designed to be independent, in some cases they need to cooperate. + +Shared objects are the main communication mechanism. A `Link` object created by LinkModule can be passed to ImageModule to create an image link. An `Image` object created by ImageModule can be passed to FigureModule to create an image with a caption. These objects encapsulate all necessary information and provide a common interface. + +The reference system allows separating definition from use. LinkModule provides `addReference()` and `getReference()` methods for managing a dictionary of named links. A user can define a reference in one part of the document and use it in another. ImageModule has an analogous system for image references. Modules using references call factory methods that themselves check whether it is a reference or a direct value. + +Element handlers can call other element handlers. When PhraseModule processes a `phrase/span` with a link, it creates a `Link` object and calls the LinkModule's element handler to create the link. This delegates the responsibility for creating and configuring the link to the specialized module. + +Relationships between modules are typically one-sided. PhraseModule knows about LinkModule and ImageModule because it creates links and images. But LinkModule and ImageModule do not know about PhraseModule. This keeps dependencies simple and allows for easy replacement or extension of modules. + + +DOM Representation +****************** + +`HtmlElement` represents a single node in the DOM tree and provides an interface for its manipulation and processing. + +The basic structure of an element includes a tag name, an associative array of attributes, and an array of children. The children can be other `HtmlElement` instances or simply text strings. This combination allows for representing any HTML structure. + +The element name is set and retrieved via the `setName()` and `getName()` methods. A special value of null as the name means a transparent element, which has no tags, only its content. + +Attributes are publicly accessible via the `$attrs` property as an associative array. Values can be strings, numbers, booleans, or arrays. A boolean `true` means an attribute without a value (like `checked`), while `false` or `null` means the attribute will not be rendered at all. If the value is an array, the different elements are joined according to the attribute type - for `class` with spaces, for `style` with semicolons. The `setAttribute()` method sets the value of an attribute. The `getAttribute()` method returns the value of an attribute or null. + +Children are managed through several methods. The `add()` method adds a child to the end. The `insert()` method inserts a child at a specified position, optionally replacing an existing child. The `create()` method creates a new `HtmlElement` as a child and returns it for further manipulation. The `removeChildren()` method removes all children. + +The element implements the ArrayAccess interface, so children can be worked with like an array. The notation `$el[0]` returns the first child, `$el[0] = $child` sets the first child. This approach is convenient for quick manipulation of specific children. + +The `toString()` method recursively traverses the element and its children and builds a string representation. HTML tags are immediately masked using `Texy::protect()`, so a placeholder is inserted into the result instead of actual HTML characters. + +The `toHtml()` and `toText()` methods return the unmasked result including post-processing. + + +Parsing Content +=============== + +`HtmlElement` can recursively parse its content, allowing for the gradual building of the DOM tree. + +The `parseLine()` method is used to parse inline syntaxes in a string. It creates a new instance of LineParser with the current element as the container. It calls `parse()` on the parser with the provided text. LineParser sequentially finds and processes all inline syntaxes, and the resulting elements or strings are added as children of the current element. The method returns the used LineParser for possible further use. + +The `parseBlock()` method parses text as block content. It creates a BlockParser and calls `parse()` on it. BlockParser finds all block constructs in the text, processes them, and adds them as children of the element. Text between blocks is processed as paragraphs, which internally use LineParser. The method accepts a boolean parameter indicating whether the text comes from an indented block, which affects the processing of paragraphs. + +These parsing methods allow for recursive processing. A syntax handler can create an element, set its basic properties, and then call `parseLine()` or `parseBlock()` to process the content. The result is that the element's content goes through the same parsing process as the main document, including syntax recognition and handler invocation. + + +Validation +========== + +`HtmlElement` provides mechanisms for validating attributes and content according to the HTML DTD (Document Type Definition). + +The DTD is a static array defining for each HTML tag which attributes are allowed and what content it can contain. Texy loads the DTD from a file upon initialization and stores it in a static array. The DTD structure maps a tag name to a pair - an array of allowed attributes and an array of allowed content. + +The `validateAttrs()` method checks the element's attributes against the DTD. For a given tag, it gets the list of allowed attributes. It goes through all the element's attributes and removes those that are not on the list. Special cases are attributes starting with `data-` or `aria-`, which are allowed if a placeholder entry `data-*` or `aria-*` is in the DTD. + +This validation is typically called when applying modifiers with the `decorate()` method. It ensures that even if a user specifies a modifier with an invalid attribute for a given tag, the attribute does not get into the final HTML. This is important for security and HTML correctness. + +The `validateChild()` method checks whether a given child can be the content of the element. It accepts a child (`HtmlElement` or a tag name) and the DTD. If the element is defined in the DTD, the method checks if the child is in the list of allowed content. If so, it returns true. If not, it returns false. + +This validation can be used when dynamically building a DOM tree to ensure a correct structure. For example, a paragraph element must not contain block elements, so `validateChild()` would refuse to add a `div` into a `p`. In practice, Texy uses this validation to a limited extent, as the structure generated by the modules is typically correct by design. + +The combination of `validateAttrs()` and `validateChild()` provides a mechanism for ensuring valid HTML, even if the input contains untrusted data or poorly formed constructs. Texy can be configured for strict validation or can disable validation for maximum flexibility. + + +Modifiers +********* + +Modifiers provide a way to add additional attributes, classes, styles, and alignment to elements without having to write direct HTML. + +The basic format of a modifier is a dot followed by a combination of different parts in round, square, and curly brackets: `.(title)[class1 class2 #id]{style:value}<align>^valign`. The entire modifier is written before or at the end of the construct to which it applies. For example, `"**text** .(Important)[highlight]{color:red}"` creates bold text with the class `highlight`, red color, and a title attribute "Important". + +Round brackets contain the title attribute or alt text. The text inside is used as the value of the title attribute on the resulting element. If the element is an image, it can be used as alt text. Inside the round brackets, it is possible to escape a bracket with a backslash. + +Square brackets contain CSS classes and optionally an ID. Classes are written as words separated by spaces. An ID is written with a hash prefix. For example, `[main-content selected #article-5]` sets two classes and one ID. If an ID is specified multiple times, the last one is used. + +Curly brackets contain CSS styles or HTML attributes. Styles are written in the standard CSS format `property:value`. Multiple styles are separated by semicolons. Some properties are recognized as HTML attributes - for example, `{href:url}` is converted to an `href` attribute, not a CSS style. This allows setting attributes that cannot be expressed otherwise. + +Alignment is specified using special characters. `<` means left, `>` right, `=` for justify, `<>` for center. Vertical alignment uses `^` for top, `-` for middle, and `_` for bottom. These shortcuts are converted to either CSS classes or inline styles depending on the configuration. + +The parts of the modifier can be in any order, and some can be omitted. A modifier containing only classes `.[highlight]`, only a title `.(Note)`, or only a style `.{color:blue}` is valid. The parser recognizes the individual parts by their delimiting characters. + + +Modifier Class +============== + +The `Modifier` class is used to parse and store information from a modifier. + +An instance of `Modifier` is typically created by a syntax handler, which passes the modifier text extracted from a regex match to the constructor. The constructor calls the `setProperties()` method, which parses the text and populates the object's properties. + +Public properties contain the individual parts of the modifier. The `$id` property contains the element's ID as a string or null. The `$classes` property is an associative array where keys are class names and values are true. The `$styles` property is an associative array mapping CSS properties to values. The `$attrs` property is an associative array with HTML attributes that are not styles or classes. + +Two special properties, `$hAlign` and `$vAlign`, contain the horizontal and vertical alignment as strings `left`, `right`, `center`, `justify` or `top`, `middle`, `bottom`. These values are later converted to CSS classes or styles according to the Texy configuration. + +The `$title` property contains the text from the round brackets, which is used as the title attribute or alt text for images. The text is automatically unescaped from HTML entities and stripped of escaped brackets. + + +Application to Elements +======================= + +A `Modifier` object is applied to an `HtmlElement` using the `Modifier::decorate()` method. + +The `decorate()` method accepts a Texy instance and an `HtmlElement` as parameters. It sequentially applies the individual parts of the modifier to the element, taking into account the Texy configuration, which may prohibit or restrict some parts. + +The application of attributes checks which attributes are allowed for the given tag according to the `Texy::$allowedTags` configuration. If all attributes are allowed, all attributes from the `Modifier` are copied to the element. If only a list of specific attributes is allowed, only those that are on the list are copied. + +The title attribute is always applied if it is set, but the text undergoes typographic post-processing to replace quotes and other adjustments. + +The application of classes and ID checks the `Texy::$allowedClasses` configuration. If all classes are allowed, all classes from the `Modifier` are added to the element, and the ID is set. If only a list of specific classes is allowed, only those that are on the list are added. The ID is added only if a string starting with a hash is on the allowed list. + +The application of styles proceeds similarly, with a check of `Texy::$allowedStyles`. Allowed CSS properties are added to the element's style attribute. If the element already had some styles, the modifier's styles are added or overwrite existing ones. + +Alignment is applied either as a CSS class or an inline style. If a mapping is configured in Texy's `Texy::$alignClasses` for the given alignment type, the corresponding CSS class is added. If not, an inline style with the `text-align` or `vertical-align` property is added. + +The result is that the element has all the attributes, classes, styles, and other properties from the modifier, but only those that are allowed by the current Texy configuration. This ensures safety when processing untrusted input. + + +Propagation of Modifiers +======================== + +Modifiers pass through the system in several phases, maintaining flexibility and allowing for modifications at different levels. + +The syntax handler extracts the modifier text from the regex match and creates a new `Modifier` instance, populating its properties. + +The `Modifier` object is passed as a parameter to element handlers. The handler receives the already parsed object, not the raw text. This allows the handler to easily access the individual parts of the modifier - classes, styles, alignment. The handler can modify the modifier before application, for example, by adding more classes or changing styles. + +The element handler creates an `HtmlElement` and passes it to the `Modifier::decorate()` method. At this point, the modifier is applied to the element. The `decorate()` method checks the Texy configurations and ensures that only allowed parts are applied. + +In some cases, a module combines multiple modifiers. For example, TableModule parses modifiers at the table, row, and cell levels. A cell's modifier is actually a clone of the column's modifier, to which additional modifications from the specific cell's modifier are then applied. This allows for default styles for an entire column with the possibility of overriding them in individual cells. diff --git a/texy/en/configuration.texy b/texy/en/configuration.texy new file mode 100644 index 0000000000..5a29aefd63 --- /dev/null +++ b/texy/en/configuration.texy @@ -0,0 +1,719 @@ +Configuration +************* + +.[perex] +A complete guide to configuring Texy. Learn how to control all modules, set up security, and customize Texy to your needs. + +Texy is configured using **public properties** of the main `Texy\Texy` class and its **modules**. Each module is responsible for processing a specific part of the syntax (images, links, headings...). + +Basic approach: + +```php +$texy = new Texy\Texy; + +// Configuration of the main class +$texy->allowedTags = Texy\Texy::NONE; + +// Configuration of a module +$texy->imageModule->root = '/images/'; +``` + + +Texy\Texy Class .[#texy-class] +============================== + +The main class contains global settings and properties affecting the entire processing. + + +Allowed Syntax ($allowed) .{toc: $allowed} +------------------------------------------ + +The `$allowed` array controls which parts of Texy syntax are active: + +```php +// Default: all syntax allowed except emoticons and the ins/del/sup/sub phrases +$texy->allowed['image'] = true; +$texy->allowed['emoticon'] = false; + +// Disable images +$texy->allowed['image'] = false; + +// Disable HTML tags in input +$texy->allowed['html/tag'] = false; +$texy->allowed['html/comment'] = false; + +// Disable various types of links +$texy->allowed['link/reference'] = false; +$texy->allowed['link/email'] = false; +$texy->allowed['link/url'] = false; +``` + +**Complete list of syntax features:** + +|--- +| Key | Default | Description +|--- +| `image` | `true` | Images `[* img.jpg *]` +| `figure` | `true` | Images with caption +| `link/reference` | `true` | References `[ref]` +| `link/email` | `true` | Email addresses +| `link/url` | `true` | Automatic URLs +| `link/definition` | `true` | Reference definitions +| `heading/underlined` | `true` | Underlined headings +| `heading/surrounded` | `true` | Surrounded headings +| `horizline` | `true` | Horizontal lines +| `blockquote` | `true` | Quotes +| `list` | `true` | Lists +| `list/definition` | `true` | Definition lists +| `table` | `true` | Tables +| `phrase/strong` | `true` | Bold text `**text**` +| `phrase/em` | `true` | Italic `//text//` +| `phrase/em-alt` | `true` | Italic `*text*` +| `phrase/code` | `true` | Code ```text``` +| `phrase/ins` | `false` | Inserted text `++text++` +| `phrase/del` | `false` | Deleted text `--text--` +| `phrase/sup` | `false` | Superscript `^^text^^` +| `phrase/sub` | `false` | Subscript `__text__` +| `html/tag` | `true` | HTML tags in input +| `html/comment` | `true` | HTML comments +| `emoticon` | `false` | Emoticons `:-)`, `:-(` +| `blocks` | `true` | Blocks `/-- \--` +| `typography` | `true` | Typographic adjustments +| `longwords` | `true` | Breaking long words + + +Allowed HTML Tags ($allowedTags) .{toc: $allowedTags} +----------------------------------------------------- + +Controls which HTML tags are allowed in output (and input): + +```php +// The default is a whitelist of all valid HTML5 tags; Texy::ALL permits any tag +$texy->allowedTags = Texy\Texy::ALL; + +// Disable all HTML tags +$texy->allowedTags = Texy\Texy::NONE; + +// Allow only specific tags +$texy->allowedTags = [ + 'strong' => [], // <strong> without attributes + 'a' => ['href', 'title'], // <a> with attributes + 'img' => Texy\Texy::ALL, // <img> with any attributes +]; +``` + +**Formats:** +- `Texy::ALL` - all tags allowed +- `Texy::NONE` - no tags allowed +- Array - allowed tags as keys, allowed attributes as values + + +Allowed CSS Classes ($allowedClasses) .{toc: $allowedClasses} +------------------------------------------------------------- + +Controls which CSS classes and IDs can be used: + +```php +// Default: all classes and IDs allowed +$texy->allowedClasses = Texy\Texy::ALL; + +// Disable classes and IDs +$texy->allowedClasses = Texy\Texy::NONE; + +// Allow specific classes and IDs +$texy->allowedClasses = [ + 'highlight', + 'important', + '#main', // IDs start with # + '#sidebar', +]; +``` + +Usage: +```texy +Text with class .[highlight] + +Text with ID .[#main] +``` + + +Allowed CSS Styles ($allowedStyles) .{toc: $allowedStyles} +---------------------------------------------------------- + +Controls which inline CSS properties can be used: + +```php +// Default: all styles allowed +$texy->allowedStyles = Texy\Texy::ALL; + +// Disable inline styles +$texy->allowedStyles = Texy\Texy::NONE; + +// Allow specific CSS properties +$texy->allowedStyles = [ + 'color', + 'background-color', + 'font-size', +]; +``` + +Usage: +```texy +Text with color .{color: red} +``` + + +CSS Classes for Alignment ($alignClasses) .{toc: $alignClasses} +--------------------------------------------------------------- + +As an alternative to inline styles `style="text-align:left"`, you can use CSS classes: + +```php +// Default: all values null (uses inline styles) +$texy->alignClasses = [ + 'left' => null, + 'right' => null, + 'center' => null, + 'justify' => null, + 'top' => null, + 'middle' => null, + 'bottom' => null, +]; + +// Set classes +$texy->alignClasses['left'] = 'text-left'; +$texy->alignClasses['right'] = 'text-right'; +$texy->alignClasses['center'] = 'text-center'; +``` + +Usage: +```texy +Text aligned to the left .< + +Text aligned to the right .> +``` + +With `alignClasses` set, it generates `<p class="text-left">` instead of `<p style="text-align:left">`. + + +Additional Properties +--------------------- + +```php +// Merge lines into paragraphs (default: true) +$texy->mergeLines = true; + +// Tab width (default: 8) +$texy->tabWidth = 8; + +// Obfuscate emails from bots (default: true) +$texy->obfuscateEmail = true; + +// Remove soft hyphens (default: true) +$texy->removeSoftHyphens = true; + +// Element for non-textual paragraphs (default: 'div') +$texy->nontextParagraph = 'div'; +``` + + +Modules +======= + +Each module processes a specific part of the syntax. Modules are accessible as public properties of the `Texy\Texy` class. + + +HeadingModule +------------- + +Processes headings (both underlined and surrounded). + +```php +// Top heading level (default: 1) +$texy->headingModule->top = 1; // <h1> + +// Generate automatic IDs (default: false) +$texy->headingModule->generateID = true; + +// Prefix for generated IDs (default: 'toc-') +$texy->headingModule->idPrefix = 'section-'; + +// More characters = higher heading? (default: true) +$texy->headingModule->moreMeansHigher = true; + +// Balancing mode (default: DYNAMIC) +$texy->headingModule->balancing = Texy\Modules\HeadingModule::DYNAMIC; +``` + +After processing: + +```php +// First heading (for <title>) +echo $texy->headingModule->title; + +// Table of Contents +print_r($texy->headingModule->TOC); +``` + + +PhraseModule +------------ + +Processes inline formatting (bold, italic, links within text...). + +```php +// HTML tags for individual phrases (default: see below) +$texy->phraseModule->tags = [ + 'phrase/strong' => 'strong', + 'phrase/em' => 'em', + 'phrase/code' => 'code', + // ... more +]; + +// Allow links in phrases (default: true) +$texy->phraseModule->linksAllowed = true; +``` + + +LinkModule +---------- + +Processes links, references, and URLs. + +```php +// Root path for links (default: null) +$texy->linkModule->root = '/articles/'; + +// CSS class for image links (deprecated) +$texy->linkModule->imageClass = 'image-link'; + +// Always add rel="nofollow" (default: false) +$texy->linkModule->forceNoFollow = false; + +// Shorten URLs to a more readable form (default: true) +$texy->linkModule->shorten = true; +``` + +**References:** + +```php +// Add a reference +$link = new Texy\Link('https://example.com'); +$link->modifier->title = 'Example page'; +$link->label = 'Example'; +$texy->linkModule->addReference('example', $link); +``` + +Usage: +```texy +Link to [example] +``` + + +ImageModule +----------- + +Processes images. + +```php +// Root path for images (default: 'images/') +$texy->imageModule->root = '/assets/images/'; + +// Root path for linked images (deprecated) +$texy->imageModule->linkedRoot = '/assets/images/full/'; + +// Physical path on disk (to determine dimensions) +$texy->imageModule->fileRoot = __DIR__ . '/public/images/'; + +// CSS class for floating images (default: null) +$texy->imageModule->leftClass = 'float-left'; +$texy->imageModule->rightClass = 'float-right'; + +// Default alternative text (deprecated) +$texy->imageModule->defaultAlt = 'Image'; +``` + +**References:** + +```php +// Add a reference +$image = new Texy\Image; +$image->URL = 'photo.jpg'; +$image->modifier->title = 'Photo'; +$texy->imageModule->addReference('photo', $image); +``` + + +FigureModule +------------ + +Processes images with captions. + +```php +// HTML element (default: 'div') +$texy->figureModule->tagName = 'figure'; + +// CSS class (default: 'figure') +$texy->figureModule->class = 'photo-figure'; + +// Classes for floating images (default: null) +$texy->figureModule->leftClass = 'figure-left'; +$texy->figureModule->rightClass = 'figure-right'; + +// Offset for width calculation (deprecated) +$texy->figureModule->widthDelta = 20; + +// Require caption (deprecated) +$texy->figureModule->requireCaption = true; +``` + + +ListModule +---------- + +Processes bulleted, numbered, and definition lists. + +```php +// Patterns for list bullets (default: see source code) +$texy->listModule->bullets = [ + '*' => ['\*[\ \t]', 0, ''], + '-' => ['[\x{2013}-](?![>-])', 0, ''], + // ... more +]; +``` + + +TableModule +----------- + +Processes tables. + +```php +// CSS classes for rows (default: null) +$texy->tableModule->oddClass = 'odd'; +$texy->tableModule->evenClass = 'even'; +``` + +*Note: `oddClass` and `evenClass` are deprecated.* + + +HorizLineModule +--------------- + +Processes horizontal lines. + +```php +// CSS classes by type (default: null) +$texy->horizLineModule->classes = [ + '-' => 'hr-line', + '*' => 'hr-star', +]; +``` + + +TypographyModule +---------------- + +Processes typographic adjustments. + +```php +// Locale (default: 'cs') +$texy->typographyModule->locale = 'en'; +``` + +**Supported locales:** +- `cs` - Czech quotes „text“ and ‚text‘ +- `en` - English quotes “text” and ‘text’ +- `fr` - French quotes «text» and ‹text› +- `de` - German quotes „text“ and ‚text‘ +- `pl` - Polish quotes „text” and ‚text’ + + +LongWordsModule +--------------- + +Breaks long words using `­`. + +```php +// Maximum word length (default: 20) +$texy->longWordsModule->wordLimit = 25; +``` + + +EmoticonModule +-------------- + +Replaces emoticons with images or Unicode characters. + +```php +// CSS class (default: null) +$texy->emoticonModule->class = 'emoji'; + +// Path to images (deprecated) +$texy->emoticonModule->root = '/images/smilies/'; +$texy->emoticonModule->fileRoot = __DIR__ . '/public/smilies/'; + +// Emoticon definitions (default: basic set) +$texy->emoticonModule->icons = [ + ':-)' => '🙂', + ':-(' => '☹', + ';-)' => '😉', + // ... or paths to images + ':cool:' => 'cool.gif', +]; +``` + + +HtmlModule +---------- + +Processes HTML tags and comments in the input text. + +```php +// Display HTML comments in output (default: true) +$texy->htmlModule->passComment = true; +``` + + +HtmlOutputModule +---------------- + +Formats the output HTML. + +```php +// Format output with indentation (default: true) +$texy->htmlOutputModule->indent = true; + +// Base indentation level (default: 0) +$texy->htmlOutputModule->baseIndent = 0; + +// Maximum line width (default: 80) +$texy->htmlOutputModule->lineWrap = 100; + +// Preserve whitespace in these elements (default: list shown) +$texy->htmlOutputModule->preserveSpaces = [ + 'textarea', 'pre', 'script', 'code', 'samp', 'kbd', +]; +``` + + +ScriptModule +------------ + +Processes `{{macro}}` calls. + +```php +// Argument separator (default: ',') +$texy->scriptModule->separator = ';'; +``` + + +Texy\Configurator Class .{toc: Texy\Configurator} +================================================= + +Ready-made configuration sets for common use cases. + + +safeMode() - Safe Mode .{toc: safeMode()} +----------------------------------------- + +Configuration for processing **untrusted content** from users. + +```php +Texy\Configurator::safeMode($texy); +``` + +**What it does:** +- Disables classes and IDs (`$allowedClasses = NONE`) +- Disables inline styles (`$allowedStyles = NONE`) +- Allows only safe HTML tags: + +```php +[ + 'a' => ['href', 'title'], + 'abbr' => ['title'], + 'b' => [], + 'br' => [], + 'cite' => [], + 'code' => [], + 'em' => [], + 'i' => [], + 'strong' => [], + 'sub' => [], + 'sup' => [], + 'q' => [], + 'small' => [], +] +``` + +- Filters URL schemes (only `http:`, `https:`, `ftp:`, `mailto:`) +- Disables images +- Disables reference definitions +- Disables HTML comments +- Adds `rel="nofollow"` to all links + + +disableLinks() - Disable Links .{toc: disableLinks()} +----------------------------------------------------- + +Disables all types of links. + +```php +Texy\Configurator::disableLinks($texy); +``` + +**What it does:** +- Disables all types of links (`link/reference`, `link/email`, `link/url`, `link/definition`) +- Disables links in phrases (`phraseModule->linksAllowed = false`) +- Removes `<a>` from allowed tags + + +disableImages() - Disable Images .{toc: disableImages()} +-------------------------------------------------------- + +Disables all types of images. + +```php +Texy\Configurator::disableImages($texy); +``` + +**What it does:** +- Disables images (`image`, `figure`, `image/definition`) +- Removes `<img>`, `<object>`, `<embed>`, `<applet>` from allowed tags + + +Security +======== + +Texy takes security seriously, but its protections against common attacks are activated by safe mode - turn it on with `Configurator::safeMode()` whenever you process untrusted input. + + +Protection Against XSS +---------------------- + +Cross-Site Scripting (XSS) is an attack where an attacker injects malicious JavaScript into a page. + +**Examples of attacks that safe mode blocks:** + +```texy +Attack attempt: <script>alert('XSS')</script> + +Attack attempt: <img src=x onerror="alert('XSS')"> + +Attack attempt: "click":javascript:alert('XSS') + +Attack attempt: [* image.jpg onload="alert('XSS')" *] +``` + +In safe mode, Texy: +- **Validates HTML** - removes disallowed tags and attributes +- **Filters URLs** - allows only safe schemes (`http:`, `https:`, `mailto:`, `ftp:`) +- **Escapes content** - properly escapes text in attributes +- **Sanitizes attributes** - removes event handlers (`onclick`, `onerror`, ...) + +```php +$texy = new Texy\Texy; +Texy\Configurator::safeMode($texy); + +$input = '<script>alert("XSS")</script>'; +$output = $texy->process($input); + +// Output: empty (script tag removed) +``` + + +URL Validation +-------------- + +Texy checks URLs in all links and images: + +```php +$texy = new Texy\Texy; + +// Set allowed schemes (default in safeMode) +$texy->urlSchemeFilters[Texy\Texy::FILTER_ANCHOR] = + '#https?:|ftp:|mailto:#Ai'; +$texy->urlSchemeFilters[Texy\Texy::FILTER_IMAGE] = + '#https?:#Ai'; +``` + +**Examples of blocked URLs:** + +```texy +"attack":javascript:alert('XSS') // blocked +"attack":data:text/html,<script> // blocked +[* javascript:alert() *] // blocked +``` + + +Filtering HTML Tags +------------------- + +Control via `$allowedTags`: + +```php +$texy = new Texy\Texy; + +// Allow only safe tags +$texy->allowedTags = [ + 'p' => [], + 'strong' => [], + 'em' => [], + 'a' => ['href', 'title'], // only these attributes +]; + +$input = '<p>Text <script>alert()</script></p>'; +$output = $texy->process($input); + +// Output: <p>Text alert()</p> +// (script tag removed) +``` + + +Practical Example +----------------- + +```php +function processComment(string $userInput): string +{ + $texy = new Texy\Texy; + + // Safe mode + Texy\Configurator::safeMode($texy); + + // Additional restrictions + $texy->allowed['link/url'] = false; // disable auto-links + $texy->allowed['html/tag'] = false; // disable HTML + + // Process + return $texy->process($userInput); +} + +// Usage +$comment = $_POST['comment']; +$html = processComment($comment); +echo $html; // safe output +``` + + +Best Practices +-------------- + +1. **Always use safeMode()** for user content +2. **Validate input** before passing it to Texy (length, format) +3. **Limit HTML tags** as needed +4. **Check output** - even though Texy is safe, double-checking never hurts +5. **Log suspicious attempts** - can help you identify attackers + +```php +$texy = new Texy\Texy; +Texy\Configurator::safeMode($texy); + +// Logging +$texy->addHandler('htmlTag', function($invocation, $el, $isStart) { + if ($el->getName() === 'script') { + error_log('XSS attempt detected!'); + } + return $invocation->proceed(); +}); +``` diff --git a/texy/en/custom-handlers.texy b/texy/en/custom-handlers.texy new file mode 100644 index 0000000000..036f195832 --- /dev/null +++ b/texy/en/custom-handlers.texy @@ -0,0 +1,850 @@ +Modifying Element Behavior +************************** + +.[perex] +This chapter describes how you can change the behavior of **existing elements** in Texy - for example, modify how images, links or formatting are processed. If you want to add **completely new syntax** that Texy doesn't know by default, read the chapter [Adding Custom Syntax |custom-syntax]. + +Imagine you want the standard image syntax `[* URL *]` to recognize a special address `[* youtube:dQw4w9WgXcQ *]` and create an embedded player instead of a regular image. + +Or you want to colorize source code listings using a syntax highlighter. And so on. This is exactly what **element handlers** are for - functions that Texy calls when processing specific elements. For example, you register a handler for the `image` element that checks the URL, and if it starts with `youtube:`, returns an iframe instead of a standard image. You don't change the syntax, you just modify what happens with the found construct. + + +Elements and Their Handlers +=========================== + +In Texy terminology, an **element** is the name for a type of item that can be processed in a document. For example, `image` is an element for images, `linkURL` for links, see [default elements |#Default Elements]. Each element has its **default handler**, which is implemented in the corresponding module and handles standard processing. + +When you write `[* image.jpg *]` in text, the parser finds this syntax, creates a `Texy\Image` object with data about the image and calls all handlers registered for the `image` element. If there is no custom handler, only the default handler from `ImageModule` is called, which creates an HTML `<img>` tag. + +You register a handler by calling the `addHandler()` method: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + // your logic here +}); +``` + +The first parameter is the element name, the second is a callback function. The callback always receives a `Texy\HandlerInvocation` object as the first parameter, followed by parameters specific to the given element. + +.[note] +A detailed explanation of all handler types can be found in the chapter [Architecture and Principles |architecture]. + + +How Processing Works +==================== + +When Texy needs to process an element, it creates a `HandlerInvocation` object containing all registered handlers for this type of element. **Your handler is called first** and can: + +- **Delegate** to the next handler by calling `$invocation->proceed()` +- **Modify input** by calling `proceed()` with modified parameters +- **Modify output** by processing the result from `proceed()` +- **Break the chain** by returning its own result without calling `proceed()` + +The `proceed()` method moves processing to the next handler in the chain. If there are no more custom handlers, the default implementation from the module is called. This means your handler has absolute control - it can decide whether the default logic is called at all. + +This mechanism is called **chain of responsibility**: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + // 1. Modify input data before processing + $image->modifier->title = 'Modified title'; + + // 2. Call next handler or default processing + $element = $invocation->proceed($image, $link); + + // 3. Modify resulting HTML element + $element->attrs['loading'] = 'lazy'; + + return $element; +}); +``` + +The execution order is from **last registered to first**. If a module registers its default handler during construction and you then register a custom handler, your handler is called first. This allows you to override or wrap the default behavior. + + +Default Elements +================ + +Texy provides several predefined elements for which you can register custom handlers. Here is a list of them with the parameters that the handler receives. + + +image +----- + +Processes images. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +): Texy\HtmlElement|string|null +``` + +The `$image` parameter contains URL, dimensions and modifiers. The `$link` parameter is provided if the image is a link (syntax `[* img *]:url`). + + +linkReference +------------- + +Processes reference links of type `[ref]`. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, + string $content, +): Texy\HtmlElement|string|null +``` + +The `$link` parameter contains the URL and modifiers loaded from the reference definition. The `$content` parameter is the HTML content of the link (already processed by parsing inline syntax). + + +linkEmail +--------- + +Processes automatically recognized email addresses in text. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, +): Texy\HtmlElement|string|null +``` + +The `$link` parameter contains the email address in the `URL` property. + + +linkURL +------- + +Processes automatically recognized URLs in text. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, +): Texy\HtmlElement|string|null +``` + +The `$link` parameter contains the found URL. + + +phrase +------ + +Processes inline formatting. + +```php +function( + Texy\HandlerInvocation $invocation, + string $phrase, + string $content, + Texy\Modifier $modifier, + ?Texy\Link $link, +): Texy\HtmlElement|string|null +``` + +The `$phrase` parameter is the syntax name like `phrase/strong` or `phrase/em`. The `$content` parameter is the text inside the formatting. The `$modifier` parameter contains CSS classes, styles and other modifiers. The `$link` parameter is provided if the formatting has an attached link. + + +newReference +------------ + +Called when the parser finds a reference that is not defined. + +```php +function( + Texy\HandlerInvocation $invocation, + string $name, +): Texy\HtmlElement|string|null +``` + +The `$name` parameter is the reference name. The handler can create a link dynamically or return `null` to reject. + + +htmlComment +----------- + +Processes HTML comments. + +```php +function( + Texy\HandlerInvocation $invocation, + string $content, +): string +``` + +The `$content` parameter is the text between `<!--` and `-->`. + + +htmlTag +------- + +Processes HTML tags in text. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\HtmlElement $el, + bool $isStart, + ?bool $forceEmpty, +): Texy\HtmlElement|string|null +``` + +The `$el` parameter is an element with name and attributes. The `$isStart` parameter determines whether it is an opening tag. The `$forceEmpty` parameter forces an empty element. + + +script +------ + +Processes scripts `{{command: args}}`. + +```php +function( + Texy\HandlerInvocation $invocation, + string $command, + array $args, + ?string $raw, +): Texy\HtmlElement|string|null +``` + +The `$command` parameter is the command name. The `$args` parameter is an array of arguments. The `$raw` parameter is the original unparsed argument string. + + +figure +------ + +Processes images with captions. + +```php +function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, + string $content, + Texy\Modifier $modifier, +): Texy\HtmlElement|null +``` + +The `$content` parameter is the caption text below the image. + + +heading +------- + +Processes headings. + +```php +function( + Texy\HandlerInvocation $invocation, + int $level, + string $content, + Texy\Modifier $modifier, + bool $isSurrounded, +): Texy\HtmlElement +``` + +The `$level` parameter is the heading level (0-6). The `$content` parameter is the heading text. The `$isSurrounded` parameter determines whether it is a delimited heading (`###`) or underlined. + + +horizline +--------- + +Processes horizontal lines. + +```php +function( + Texy\HandlerInvocation $invocation, + string $type, + Texy\Modifier $modifier, +): Texy\HtmlElement +``` + +The `$type` parameter is the string of characters used for the line (`---` or `***`). + + +block +----- + +Processes special blocks `/--type` to `\--`. + +```php +function( + Texy\HandlerInvocation $invocation, + string $blocktype, + string $content, + ?string $param, + Texy\Modifier $modifier, +): Texy\HtmlElement|string|null +``` + +The `$blocktype` parameter is the block type with the `block/` prefix, e.g. `block/code` or `block/html`. The `$content` parameter is the block content. The `$param` parameter is an optional parameter after the type (e.g. language for code). + + +emoticon +-------- + +Processes emoticons (smileys). + +```php +function( + Texy\HandlerInvocation $invocation, + string $emoticon, + string $raw, +): Texy\HtmlElement|string +``` + +The `$emoticon` parameter is the recognized emoticon (e.g. `:-)` or `:-(` ). The `$raw` parameter is the original text including any repeating characters (e.g. `:-)))))` ). + +.[note] +Emoticons are **disabled** by default. You can enable them using `$texy->allowed['emoticon'] = true;` + + +Default Events +============== + +Texy provides several predefined events for which you can register handlers. These are called **notification handlers**. Unlike element handlers, these handlers **don't return anything**. They are used for side effects such as logging, collecting statistics, or modifying the already created DOM tree. + + +beforeParse +----------- + +Called before text parsing begins. Allows you to perform preprocessing or load definitions. + +```php +function( + Texy\Texy $texy, + string &$text, + bool $isSingleLine, +): void +``` + +The `$text` parameter is passed by reference, so you can modify it. The `$isSingleLine` parameter determines whether a single line or the entire document is being parsed. + + +afterParse +---------- + +Called after parsing is complete, before converting the DOM tree to HTML. Allows you to modify the created DOM. + +```php +function( + Texy\Texy $texy, + Texy\HtmlElement $DOM, + bool $isSingleLine, +): void +``` + +The `$DOM` parameter is the root element of the document, which you can traverse and modify. + + +afterList +--------- + +Called after a list (numbered or unnumbered) is created. + +```php +function( + Texy\BlockParser $parser, + Texy\HtmlElement $element, + Texy\Modifier $modifier, +): void +``` + +The `$element` parameter is the created `<ul>` or `<ol>` element. The `$modifier` parameter contains modifiers applied to the entire list. + + +afterDefinitionList +------------------- + +Called after a definition list is created. + +```php +function( + Texy\BlockParser $parser, + Texy\HtmlElement $element, + Texy\Modifier $modifier, +): void +``` + +The `$element` parameter is the created `<dl>` element. + + +afterTable +---------- + +Called after a table is created. + +```php +function( + Texy\BlockParser $parser, + Texy\HtmlElement $element, + Texy\Modifier $modifier, +): void +``` + +The `$element` parameter is the created `<table>` element. + + +afterBlockquote +--------------- + +Called after a blockquote is created. + +```php +function( + Texy\BlockParser $parser, + Texy\HtmlElement $element, + Texy\Modifier $modifier, +): void +``` + +The `$element` parameter is the created `<blockquote>` element. + + +Basic Usage +=========== + +The simplest element handler just delegates to the default processing: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + return $invocation->proceed(); +}); +``` + +This handler doesn't change anything, but shows the basic skeleton. It passes all parameters forward and returns the result. + + +Modifying Input Data +-------------------- + +A handler can modify data before processing: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + // add default dimensions if not specified + $image->width ??= 800; + $image->height ??= 600; + return $invocation->proceed(); +}); +``` + +Changes made to `$image` or `$link` objects will be reflected in further processing, including the default handler. + + +Modifying Output Element +------------------------ + +A handler can modify the HTML element returned from `proceed()`: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + $element = $invocation->proceed(); + + if ($element) { + // add lazy loading + $element->attrs['loading'] = 'lazy'; + + // add CSS class + $element->attrs['class'][] = 'responsive'; + } + + return $element; +}); +``` + + +Conditional Processing +---------------------- + +A handler can process only certain cases and delegate others: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + // special processing for YouTube videos + if (str_starts_with($image->URL, 'youtube:')) { + $id = substr($image->URL, 8); + $iframe = sprintf( + '<iframe src="https://youtube.com/embed/%s"></iframe>', + htmlspecialchars($id) + ); + return $invocation->getTexy() + ->protect($iframe, Texy\Texy::CONTENT_BLOCK); + } + + // process other images normally + return $invocation->proceed(); +}); +``` + + +Interrupting Processing +----------------------- + +A handler can refuse processing by returning `null`: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + // disallow external images + if (str_contains($image->URL, '://')) { + return null; + } + + return $invocation->proceed(); +}); +``` + + +Practical Examples +================== + +The following examples show real use cases for element handlers. + + +YouTube Embed +------------- + +Converting special syntax to embedded video: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + if (str_starts_with($image->URL, 'youtube:')) { + $id = substr($image->URL, 8); + $width = $image->width ?: 560; + $height = $image->height ?: 315; + + $iframe = sprintf( + '<iframe width="%d" height="%d" ' + . 'src="https://youtube.com/embed/%s" ' + . 'frameborder="0" allowfullscreen></iframe>', + $width, $height, htmlspecialchars($id) + ); + + $texy = $invocation->getTexy(); + return $texy->protect($iframe, $texy::CONTENT_BLOCK); + } + + return $invocation->proceed(); +}); +``` + +Usage in text: + +```texy +[* youtube:dQw4w9WgXcQ 640x360 *] +``` + + +Image Gallery +------------- + +Wrapping images in a special div for lightbox: + +```php +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) { + $element = $invocation->proceed(); + + // if image has 'gallery' class + if (isset($image->modifier->classes['gallery'])) { + // wrap in div with lightbox attributes + $wrapper = new Texy\HtmlElement('div'); + $wrapper->attrs['class'][] = 'lightbox-item'; + $wrapper->attrs['data-src'] = $image->URL; + $wrapper->add($element); + + return $wrapper; + } + + return $element; +}); +``` + +Usage: + +```texy +[* image.jpg .[gallery] *] +``` + + +Link Validation +--------------- + +Checking whether links found in text lead to allowed domains: + +```php +$allowedDomains = ['example.com', 'trusted.org']; + +$texy->addHandler('linkURL', function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, +) use ($allowedDomains) { + $host = parse_url($link->URL, PHP_URL_HOST); + + // if domain is not in whitelist, disallow link + if ($host && !in_array($host, $allowedDomains, true)) { + return null; + } + + return $invocation->proceed(); +}); +``` + + +Automatic rel="nofollow" +------------------------ + +Adding `nofollow` to all external links found in text: + +```php +$texy->addHandler('linkURL', function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, +) { + $element = $invocation->proceed(); + + // if link contains :// (i.e. is external) + if (str_contains($link->URL, '://')) { + $element->attrs['rel'] = 'nofollow'; + } + + return $element; +}); +``` + + +Syntax Highlighting +------------------- + +Integrating a syntax highlighting library: + +```php +$texy->addHandler('block', function( + Texy\HandlerInvocation $invocation, + string $blocktype, + string $content, + ?string $param, + Texy\Modifier $modifier, +) { + // process only 'code' type blocks + if ($blocktype !== 'block/code') { + return $invocation->proceed(); + } + + // apply syntax highlighting + $highlighter = new MyHighlighter(); + $highlighted = $highlighter->highlight($content, $param); + + $el = new Texy\HtmlElement('pre'); + $modifier->decorate($invocation->getTexy(), $el); + $el->attrs['class'][] = 'language-' . $param; + + $code = new Texy\HtmlElement('code'); + $code->add($highlighted); + $el->add($code); + + return $el; +}); +``` + + +Lazy Loading +------------ + +Iterating through all images and adding lazy loading: + +```php +$texy->addHandler('afterParse', function( + Texy\Texy $texy, + Texy\HtmlElement $DOM, + bool $isSingleLine, +) { + foreach ($DOM->getIterator() as $child) { + if ($child instanceof Texy\HtmlElement + && $child->getName() === 'img' + ) { + $child->attrs['loading'] = 'lazy'; + } + } +}); +``` + + +Logging Used Elements +--------------------- + +Collecting statistics about used elements in the document: + +```php +$stats = []; + +$texy->addHandler('beforeParse', function( + Texy\Texy $texy, + string &$text, + bool $isSingleLine, +) use (&$stats) { + $stats = ['images' => 0, 'links' => 0, 'headings' => 0]; +}); + +$texy->addHandler('image', function( + Texy\HandlerInvocation $invocation, + Texy\Image $image, + ?Texy\Link $link, +) use (&$stats) { + $stats['images']++; + return $invocation->proceed(); +}); + +$texy->addHandler('linkURL', function( + Texy\HandlerInvocation $invocation, + Texy\Link $link, +) use (&$stats) { + $stats['links']++; + return $invocation->proceed(); +}); + +$texy->addHandler('heading', function( + Texy\HandlerInvocation $invocation, + int $level, + string $content, + Texy\Modifier $modifier, + bool $isSurrounded, +) use (&$stats) { + $stats['headings']++; + return $invocation->proceed(); +}); +``` + + +Helper Classes +============== + +When working with handlers, you will work with several important classes. Here is an overview with their most important properties. + + +Texy\Image +---------- + +Represents an image with its parameters: + +```php +$image->URL; // ?string - path to image +$image->linkedURL; // ?string - link URL (if image is a link) +$image->width; // ?int - width in pixels +$image->height; // ?int - height in pixels +$image->asMax; // bool - whether dimensions are maximum +$image->modifier; // Modifier - CSS classes, styles, attributes +$image->name; // ?string - reference name +``` + + +Texy\Link +--------- + +Represents a link with its parameters: + +```php +$link->URL; // ?string - target URL +$link->raw; // string - original URL (before normalization) +$link->modifier; // Modifier - CSS classes, styles, attributes +$link->type; // int - link type (COMMON, BRACKET, IMAGE) +$link->label; // ?string - link text (for references) +$link->name; // ?string - reference name +``` + +Constants for link type: + +```php +Texy\Link::COMMON; // common link +Texy\Link::BRACKET; // reference link [ref] +Texy\Link::IMAGE; // link from image [* img *] +``` + + +Texy\HtmlElement +---------------- + +Represents an HTML element with its attributes and content: + +```php +$el = new Texy\HtmlElement('div'); + +// working with element name +$el->getName(); // returns 'div' +$el->setName('section'); // changes to 'section' + +// working with attributes +$el->attrs['id'] = 'main'; +$el->attrs['class'][] = 'container'; +$el->attrs['style']['color'] = 'red'; + +// working with content +$el->setText('text'); // sets text content +$el->getText(); // returns text content +$el->add($child); // adds child +$el->insert(0, $child); // inserts child at position + +// parsing content +$el->parseLine($texy, $text); // parses inline text +$el->parseBlock($texy, $text); // parses block text + +// conversion to HTML +$el->toString($texy); // internal representation +$el->toHtml($texy); // final HTML +``` + + +Texy\Modifier +------------- + +Represents modifiers for CSS classes, styles and attributes: + +```php +$mod->id; // ?string - HTML id +$mod->classes; // array - array of CSS classes +$mod->styles; // array - array of CSS styles +$mod->attrs; // array - HTML attributes +$mod->hAlign; // ?string - horizontal alignment (left, right, center, justify) +$mod->vAlign; // ?string - vertical alignment (top, middle, bottom) +$mod->title; // ?string - title attribute or alt for images + +// applying modifier to element +$mod->decorate($texy, $element); +``` diff --git a/texy/en/custom-syntax.texy b/texy/en/custom-syntax.texy new file mode 100644 index 0000000000..788cb9adee --- /dev/null +++ b/texy/en/custom-syntax.texy @@ -0,0 +1,519 @@ +Adding Custom Syntax +******************** + +.[perex] +This chapter describes how to add **completely new markup constructs** to Texy that don't exist by default. If you only want to modify the behavior of existing elements (for example, adjust image or link processing), read the chapter [Custom Element Behavior |custom-handlers]. + +Imagine you want to automatically create links to user profiles in documentation by writing `@@username`. Or you need special alert blocks like `:::warning`. Texy doesn't recognize these constructs, and you can't create them by modifying existing elements. + +Custom syntax allows you to define new markup constructs. You specify what the construct should look like (using a regular expression) and write a function to process it. Texy will then recognize your syntax just like its standard constructs. + + +Syntax Registration +=================== + +Texy provides two methods for registering custom syntax, depending on whether it's an inline or block element. + + +Line Syntax +----------- + +Line syntax is used for inline constructs within text lines. You register it using the `registerLinePattern()` method: + +```php +$texy->registerLinePattern( + callable $handler, + string $pattern, + string $name, + ?string $againTest = null, +); +``` + +**The `$handler` parameter** is a callback function that gets called when the syntax is found. It can be a function name, anonymous function, or array `[$object, 'method']`. + +**The `$pattern` parameter** is a regular expression (PCRE) that defines what your syntax looks like in the text. The pattern **should not be anchored** to the start of a line (`^`), since it's searched for anywhere in the text. Use capturing groups to capture the data you need to process. + +**The `$name` parameter** is a unique syntax name. It's used in the `$texy->allowed` array for enabling/disabling and passed to the handler for identification. We recommend using a prefix style like `custom/username` or `myapp/profile`. + +**The `$againTest` parameter** is an optional regex for optimization. If the main pattern fails to match at a position, Texy uses `$againTest` to decide whether it's worth searching for the pattern again further in the text; when `$againTest` no longer matches anywhere ahead, the pattern is dropped from the search. This significantly speeds up processing if you have a complex pattern that's rarely used. + +Registration example: + +```php +$texy->registerLinePattern( + 'usernameHandler', + '#@@([a-z0-9_]+)#i', + 'custom/username', +); +``` + + +Block Syntax +------------ + +Block syntax is used for multi-line block constructs. You register it using the `registerBlockPattern()` method: + +```php +$texy->registerBlockPattern( + callable $handler, + string $pattern, + string $name, +); +``` + +The `$handler` and `$name` parameters have the same meaning as for line syntax. + +**The `$pattern` parameter** is a regular expression that **must be anchored** to the start of a line (`^`) and often to the end (`$`) as well. BlockParser automatically adds the `m` (multiline) modifier, so don't add it to the pattern (you still write the `^` anchor yourself). The pattern should match the entire block or at least its beginning. + +Registration example: + +```php +$texy->registerBlockPattern( + 'alertHandler', + '#^:::(warning|info|danger)\n(.+)$#s', + 'custom/alert', +); +``` + + +Syntax Handler +============== + +A syntax handler is a function called by the parser when it finds an occurrence of your syntax in the text. Its job is to process the found data and return an HTML element or string. + +A detailed explanation of the syntax handler's role in Texy's architecture can be found in the chapter [Architecture and Principles |architecture#syntax-handler]. + + +For Line Syntax +--------------- + +Syntax handler signature for line syntax: + +```php +function( + Texy\LineParser $parser, + array $matches, + string $name, +): Texy\HtmlElement|string|null +``` + +**The `$parser` parameter** provides access to the parser and Texy object. You'll most often use `$parser->getTexy()` to get the Texy instance. + +**The `$matches` parameter** contains the regex match results. `$matches[0]` is the entire matched string, `$matches[1]`, `$matches[2]` etc. are the capturing groups from your pattern. + +**The `$name` parameter** is the syntax name you specified during registration. Useful if one handler processes multiple syntaxes. + +**The return value** can be `Texy\HtmlElement` for structured HTML output, `string` for direct HTML code (which you must protect), or `null` to refuse processing. + +The handler can set `$parser->again = true` if it wants the content of the created element to be parsed again to find nested syntaxes. + + +For Block Syntax +---------------- + +Syntax handler signature for block syntax: + +```php +function( + Texy\BlockParser $parser, + array $matches, + string $name, +): Texy\HtmlElement|string|null +``` + +The parameters have the same meaning as for line syntax, except you receive `Texy\BlockParser` instead of `LineParser`. + +BlockParser provides methods for working with multi-line structures: + +- **`$parser->next($pattern, &$matches)`** - matches the next line against the pattern and returns true/false +- **`$parser->moveBackward($lines)`** - moves back the specified number of lines +- **`$parser->isIndented()`** - returns true if the current block is indented + + +LineParser API +============== + +When working with line syntax, you have several useful properties and methods available. + +**The `$again` property** controls whether the currently processed syntax should be searched for again at the same position after processing. The default value is `false`. Set to `true` if you're creating an element with content that may contain other syntaxes: + +```php +function( + Texy\LineParser $parser, + array $matches, + string $name, +): Texy\HtmlElement +{ + $el = new Texy\HtmlElement('span'); + $el->setText($matches[1]); + + // content may contain additional formatting + $parser->again = true; + + return $el; +} +``` + +**The `getTexy()` method** returns the Texy object instance, which you need for working with `protect()` or accessing configuration. + + +BlockParser API +=============== + +When working with block syntax, you have methods available for working with multi-line structures. + +**The `next($pattern, &$matches)` method** tries to match the next line in the text against the specified pattern. If successful, it fills `$matches` with the result and moves the internal position past this line. Returns `true` on success, `false` on failure: + +```php +while ($parser->next('#^\-\s+(.+)$#', $matches)) { + // process next list item + $item = $matches[1]; +} +``` + +**The `moveBackward($lines = 1)` method** moves the internal position back the specified number of lines. Useful when your pattern matched more than the block's start and you want to return to the beginning: + +```php +// pattern matched 3 lines, but we want to read from the first +$parser->moveBackward(2); +``` + +**The `isIndented()` method** returns `true` if the current block is indented (starts with a space or tab). This indicates that it's nested content. + + +Practical Examples +================== + +The following examples demonstrate real use cases for custom syntax. + + +User Profiles +------------- + +Automatic creation of profile links by writing `@@username`: + +```php +$texy->registerLinePattern( + function( + Texy\LineParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $username = $matches[1]; + + $el = new Texy\HtmlElement('a'); + $el->attrs['href'] = '/user/' . urlencode($username); + $el->attrs['class'][] = 'user-profile'; + $el->setText('@' . $username); + + return $el; + }, + '#@@([a-z0-9_]+)#i', + 'custom/username' +); +``` + +Usage in text: + +```texy +Check out the profile of @@johndoe or @@jane_smith. +``` + + +Alert Boxes +----------- + +Special alert blocks with different types: + +```php +$texy->registerBlockPattern( + function( + Texy\BlockParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $type = $matches[1]; // warning, info, danger + $content = $matches[2]; + + $el = new Texy\HtmlElement('div'); + $el->attrs['class'][] = 'alert'; + $el->attrs['class'][] = 'alert-' . $type; + + $texy = $parser->getTexy(); + $el->parseBlock($texy, trim($content)); + + return $el; + }, + '#^:::(warning|info|danger)\n(.+?)(?=\n:::|$)#s', + 'custom/alert' +); +``` + +Usage in text: + +```texy +:::warning +This is an important warning! +::: + +:::info +For your information: the update will take place tomorrow. +::: +``` + + +Hashtags +-------- + +Automatic creation of links from hashtags: + +```php +$texy->registerLinePattern( + function( + Texy\LineParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $tag = $matches[1]; + + $el = new Texy\HtmlElement('a'); + $el->attrs['href'] = '/tag/' . urlencode($tag); + $el->attrs['class'][] = 'hashtag'; + $el->setText('#' . $tag); + + return $el; + }, + '#\#([a-z0-9_]+)#i', + 'custom/hashtag', + '#\##' // optimization - search only if # is in text +); +``` + +Usage: + +```texy +Article about #php and #webdesign. +``` + + +Abbreviations +------------- + +Automatic expansion of abbreviations with explanation: + +```php +$abbreviations = [ + 'HTML' => 'HyperText Markup Language', + 'CSS' => 'Cascading Style Sheets', + 'PHP' => 'PHP: Hypertext Preprocessor', +]; + +$texy->registerLinePattern( + function( + Texy\LineParser $parser, + array $matches, + string $name + ) use ($abbreviations): ?Texy\HtmlElement + { + $abbr = $matches[1]; + + if (!isset($abbreviations[$abbr])) { + return null; // unknown abbreviation + } + + $el = new Texy\HtmlElement('abbr'); + $el->attrs['title'] = $abbreviations[$abbr]; + $el->setText($abbr); + + return $el; + }, + '#\b([A-Z]{2,})\b#', + 'custom/abbreviation' +); +``` + + +Inline Icons +------------ + +Inserting icons using special syntax: + +```php +$texy->registerLinePattern( + function( + Texy\LineParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $icon = $matches[1]; + + $el = new Texy\HtmlElement('i'); + $el->attrs['class'][] = 'icon'; + $el->attrs['class'][] = 'icon-' . $icon; + $el->attrs['aria-hidden'] = 'true'; + + return $el; + }, + '#:icon-([a-z-]+):#', + 'custom/icon' +); +``` + +Usage: + +```texy +Click the button :icon-download: to download. +``` + + +Note Block +---------- + +Block for footnotes: + +```php +$texy->registerBlockPattern( + function( + Texy\BlockParser $parser, + array $matches, + string $name + ): Texy\HtmlElement + { + $parser->moveBackward(); + + $content = ''; + while ($parser->next('#^NOTE:\s*(.+)$#', $matches)) { + $content .= $matches[1] . "\n"; + } + + $el = new Texy\HtmlElement('aside'); + $el->attrs['class'][] = 'note'; + + $texy = $parser->getTexy(); + $el->parseBlock($texy, trim($content)); + + return $el; + }, + '#^NOTE:\s*(.+)$#', + 'custom/note' +); +``` + +Usage: + +```texy +NOTE: This is an important note. +NOTE: It can be multi-line. +``` + + +Custom Quotations with Author +----------------------------- + +Extended syntax for quotations with author attribution: + +```php +$texy->registerBlockPattern( + function( + Texy\BlockParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $author = $matches[1]; + $quote = $matches[2]; + + $blockquote = new Texy\HtmlElement('blockquote'); + + $texy = $parser->getTexy(); + $blockquote->parseBlock($texy, trim($quote)); + + $cite = new Texy\HtmlElement('cite'); + $cite->setText($author); + $blockquote->add($cite); + + return $blockquote; + }, + '#^QUOTE\[([^\]]+)\]:\n(.+?)(?=\n\n|$)#s', + 'custom/quote' +); +``` + +Usage: + +```texy +QUOTE[Albert Einstein]: +Imagination is more important than knowledge, +because knowledge is limited. +``` + + +Image Gallery +------------- + +Special block for creating a gallery from multiple images: + +```php +$texy->registerBlockPattern( + function( + Texy\BlockParser $parser, + array $matches, + string $name, + ): Texy\HtmlElement + { + $parser->moveBackward(); + + $gallery = new Texy\HtmlElement('div'); + $gallery->attrs['class'][] = 'gallery'; + + while ($parser->next('#^\[G\]\s*(.+)$#', $matches)) { + $img = new Texy\HtmlElement('img'); + $img->attrs['src'] = trim($matches[1]); + $img->attrs['loading'] = 'lazy'; + $gallery->add($img); + } + + return $gallery; + }, + '#^\[G\]\s*(.+)$#', + 'custom/gallery' +); +``` + +Usage: + +```texy +[G] image1.jpg +[G] image2.jpg +[G] image3.jpg +``` + + +Syntax Collisions +================= + +When registering custom syntax, you must be careful that it doesn't collide with existing Texy syntaxes or other custom syntaxes. + +**Registration order matters.** Line syntaxes are searched in the order they were registered. If multiple syntaxes can match at the same position, the one registered earlier wins. Therefore, register more specific syntaxes before more general ones. + +**Be specific in patterns.** The more concrete your pattern is, the lower the risk of collision. The pattern `#\#\w+#` also matches `#heading`, which could collide with headings. Better is `#(?<=\s)\#[a-z0-9_]+#i`, which requires a space before the hashtag. + +**Test combinations.** Try how your syntax works in combination with existing constructs. What happens when your markup is inside a link? What if it's inside a code block? + +**Use prefixed names.** Instead of `username`, use `custom/username` or `myapp/username`. This prevents conflicts if Texy adds syntax with the same name in the future. + + +Best Practices +============== + +**Return `null` on failure.** If the handler determines it can't or doesn't want to process the given match (for example, an unknown abbreviation), return `null`. The parser will then try other syntaxes. + +**Use `protect()` for HTML.** If you're returning a raw HTML string instead of `HtmlElement`, you must protect it using `$texy->protect($html, Texy::CONTENT_...)`. Otherwise it will be escaped. + +**Set `$parser->again` correctly.** For line syntaxes that create an element with text content that may contain other syntaxes (formatting, links), set `$parser->again = true`. + +**Respect `$texy->allowed`.** If you're creating a module with multiple syntaxes, check `$texy->allowed[$name]` before registering the pattern or in the handler before processing. diff --git a/texy/en/develop.texy b/texy/en/develop.texy new file mode 100644 index 0000000000..1b3c0eda6c --- /dev/null +++ b/texy/en/develop.texy @@ -0,0 +1,35 @@ +For Developers +************** + +.[perex] +Welcome to the Texy programmer documentation! This section will guide you from basic usage to advanced techniques of extension and custom syntax. + + +[Quick Start | quickstart] +-------------------------- + +Installation, first use and basic configuration. In 5 minutes you will have Texy functional. + + +[Configuration | configuration] +------------------------------- + +Complete overview of all modules, their properties and configuration options. Security settings, allowed tags, styles and classes. + + +[Custom Element Behavior | custom-handlers] +------------------------------------------- + +Learn how to change the behavior of existing syntax. YouTube embedding, syntax highlighting, custom validation. + + +[Adding Custom Syntax | custom-syntax] +-------------------------------------- + +Creating completely new syntactic elements. + + +[Architecture and Principles | architecture] +-------------------------------------------- + +Understanding how Texy works internally. Parsing flow, modules, pattern matching, protect/unprotect mechanism. diff --git a/texy/en/quickstart.texy b/texy/en/quickstart.texy new file mode 100644 index 0000000000..f650307890 --- /dev/null +++ b/texy/en/quickstart.texy @@ -0,0 +1,205 @@ +Quick Start +*********** + +.[perex] +Learn how to work with Texy in just a few minutes. This page guides you through installation, first use, and basic configuration. + + +Installation +============ + +Texy leverages modern PHP features and requires at least version 8.1. + +The simplest installation method is via Composer: + +```bash +composer require texy/texy +``` + +Composer will automatically download Texy and all its dependencies. + + +First Use +========= + + +Basic Text Processing +--------------------- + +Creating a Texy instance and processing text is extremely simple: + +```php +require __DIR__ . '/vendor/autoload.php'; + +$texy = new Texy\Texy; + +$text = 'This is **bold text** and this is //italic//.'; +$html = $texy->process($text); + +echo $html; +``` + +Output: +```latte +<p>This is <strong>bold text</strong> and this is <em>italic</em>.</p> +``` + +The `process()` method processes the entire text including block elements (paragraphs, headings, lists, tables...). + + +Single-line Text +---------------- + +If you're processing only single-line text without block elements (such as database headings or short descriptions): + +```php +$texy = new Texy\Texy; + +$text = 'Link to "homepage":https://example.com'; +$html = $texy->processLine($text); + +echo $html; +``` + +Output: +```latte +Link to <a href="https://example.com">homepage</a> +``` + +The `processLine()` method doesn't wrap the output in a `<p>` paragraph and processes only inline elements. + + +Basic Configuration +=================== + +Texy works out of the box, but you'll often want to adjust the basic settings. + + +Setting Image Paths +------------------- + +If you're using relative paths to images, set the root directory: + +```php +$texy = new Texy\Texy; + +// Web path (prepended to relative URLs) +$texy->imageModule->root = '/images/'; + +// Physical disk path (for determining dimensions) +$texy->imageModule->fileRoot = __DIR__ . '/public/images/'; +``` + +Now when you write `[* photo.jpg *]`, Texy will generate `<img src="/images/photo.jpg">` and automatically determine the image dimensions. + + +Setting Link Paths +------------------ + +Similarly, you can set the root directory for links: + +```php +$texy->linkModule->root = '/articles/'; +``` + + +Enabling and Disabling Syntaxes +------------------------------- + +Each part of the Texy syntax can be disabled or enabled using the `$allowed` array: + +```php +$texy = new Texy\Texy; + +// Disable images +$texy->allowed['image'] = false; + +// Disable HTML tags in input +$texy->allowed['html/tag'] = false; + +// Enable emoticons (disabled by default) +$texy->allowed['emoticon'] = true; +``` + +A complete list of syntax options can be found in [configuration | configuration#allowed]. + + +Safe Mode for User Content +-------------------------- + +If you're processing content from users (comments, forum posts), use safe mode: + +```php +$texy = new Texy\Texy; +Texy\Configurator::safeMode($texy); + +$userInput = $_POST['comment']; +$html = $texy->process($userInput); +``` + +SafeMode: +- Allows only **safe HTML tags** (`<strong>`, `<em>`, `<a>`, ...) +- Disables **classes and IDs** +- Disables **inline styles** +- Disables **images** +- Adds `rel="nofollow"` to absolute links +- Filters **URL schemes** (only `http:`, `https:`, `ftp:`, `mailto:`) + +More about security in the chapter [Configuration - Security |configuration#Security]. + + +Complete Example +================ + +```php +require __DIR__ . '/vendor/autoload.php'; + +$texy = new Texy\Texy; + +// Configuration +$texy->imageModule->root = '/images/'; +$texy->linkModule->root = '/'; +$texy->allowed['html/tag'] = false; + +// Text to process +$text = ' + + +Article Heading +=============== + +This is an **introductory paragraph** with a link to "homepage":https://example.com. + +- First item +- Second item +- Third item + +[* photo.jpg .(Photograph) *] +'; + +// Processing +$html = $texy->process($text); + +// Output +echo $html; + +// Additional information +echo "Page title: " . $texy->headingModule->title; +print_r($texy->summary['links']); +print_r($texy->summary['images']); +``` + +After processing, you have access to: +- `$texy->headingModule->title` - first heading (suitable for `<title>`) +- `$texy->summary['links']` - array of all used links +- `$texy->summary['images']` - array of all used images + + +Next Steps +========== + +Now you know how to use Texy. Continue with: + +- **[Configuration | configuration]** - detailed settings for all modules +- **[Syntax | syntax]** - learn the Texy markup +- **[Architecture | architecture]** - understand how Texy works internally diff --git a/texy/en/syntax-full.texy b/texy/en/syntax-full.texy deleted file mode 100644 index 2df749bac6..0000000000 --- a/texy/en/syntax-full.texy +++ /dev/null @@ -1,889 +0,0 @@ -Detailed Description of Syntax -****************************** - - -- [#Philosophy] -- [#Paragraphs of Text] -- [#Headings] -- [#Horizontal lines] -- [#Code] -- [#Turning off the Texy] -- [#Quotes] -- [#Links] -- [#Images] -- [#Phrases] -- [#Direct HTML] -- [#Lists] -- [#Modifiers] -- [#Typography] -- [#Long Words Hyphenation] -- [#Tables] - - -Philosophy -========== - -The Texy tool was created to allow inexperienced users to easily edit the content of web pages. Therefore, the syntax is maximally intuitive. The intention is that the text in pure (unformatted) form is clear and its format can be guessed. - -Today, Texy also serves well-experienced HTML experts. Allows you to freely combine Texy notation with HTML tags. Thus, experienced users do not have to learn a new meta-language and make full use of their knowledge. Texy only simplifies their work. - -The primary logic of the syntax is ** not to use any syntax **. Just write plain text. Inserting advanced information, such as CSS classes or links, does not disrupt the flow of text. And it is written in a way that even non-technical users can easily understand. - - -Paragraphs of Text -================== - -A paragraph is considered to be one or more consecutive lines of text. The paragraphs are separated by a blank line. - -/--code texy -First paragraph lorem ipsum dolor sit amet. - -Second paragraph, který tvoří jeden řádek. -And second line of paragraph. Texy will join the lines. -\-- - -/--texysource -First paragraph lorem ipsum dolor sit amet. - -Second paragraph, který tvoří jeden řádek. -And second line of paragraph. Texy will join the lines. -\-- - -*In the edit box (textarea) is not a division into two lines of paragraph evident. That's why Texy considers them one paragraph.* - -To wrap a line in a paragraph, insert one space to the left: - -/--code texy -April is the cruellest month, breeding - Lilacs out of the dead land, mixing - Memory and desire, stirring - Dull roots with spring rain. -\-- - -/--texysource -April is the cruellest month, breeding - Lilacs out of the dead land, mixing - Memory and desire, stirring - Dull roots with spring rain. -\-- - - -Headings -======== - -Headings can be written in two ways: **underlined** or **surrounded**. - -Each headline has its own degree. In the case of **underlined**, the importance of the title is decided by the underline character. From the highest to the lowest, these are: `#` `*` `=` `-` - -/--code texy -Head Title -********** - - -Subtitle -======== -\-- - -/--texysource -Head Title -********** - - -Subtitle -======== -\-- - -For **surrounded** titles, the level determines the number of preceding characters. It can be `#` nebo `=` - -The following applies: the more characters, the more important the title (minimum two characters, maximum seven). - -/--code texy -=== Head title === - -## Subtitle -\-- - -As you can see in the case of the subtitle, the characters on the right can be omitted. - -*Subtitle levels are always relative only! So Texy finds the highest caption used and relatively differentiates the other captions.* - - -Horizontal Lines -================ - -Texy knows these notations: - -/--code texy ------------- - -******** -\-- - -/--texysource -------------- - -******** -\-- - - -Code -==== - -Used to insert source code. Syntax highlighting can also be activated using an add-on module. - -/--code texy - /---code php - function reImage($matches) { - $content = $matches[1]; - $align = $matches[5]; - $href = $matches[6]; - } - \--- -\-- - -/--texysource - /---code php - function reImage($matches) { - $content = $matches[1]; - $align = $matches[5]; - $href = $matches[6]; - } - \--- -\-- - -*Note the words `php` to indicate the language.* - - -Turning Off the Texy -==================== - -The keyword `html` or` text` affects whether the content will be understood as HTML (including tags) or plain text. - -/--code texy - /---html - <em>example</em>: **this is not strong** - \--- - - - /---text - <em>example</em>: **this is not strong** - \--- -\-- - -To turn off Texy inline, it is possible to use a double apostrophe `''` and wrap a part of the text that is not to be Texy processed. - -/--code texy - Example: ''**this is not strong**'' -\-- - - -Division into Blocks (Div) -========================== - -Use this ability to create more complex documents. - -/--code texy - /---div .[header] - - content of div - - \--- -\-- - -/--texysource - /---div .[header] - - content of div - - \--- -\-- - - -It is also possible to nest blocks: - -/--code texy - /---div .[header] - - ## This is a header. - - /---div - inner div - \--- - - Texy is sexy! - - \--- -\-- - -/--texysource - /---div .[header] - - ## This is a header. - - /---div - inner div - \--- - - Texy is sexy! - - \--- -\-- - - -Quotes -====== - -Quotes are indented, similarly to emails, by a character `>` - -/--code texy -> This is a blockquote with two paragraphs. -> -> 640 K should be enough for everyone -\-- - -/--texysource -> This is a blockquote with two paragraphs. -> -> 640 K should be enough for everyone -\-- - - -Links -===== - -Links are written by enclosing the referencing text in quotation marks, followed by a colon and a URL. Texy tries to intelligently guess the end of the URL. You can also help it by enclosing the URL in square brackets. The `http://` section is optional. - -It is also possible to insert emails as a link, Texy transforms them into a form that should confuse spambots. - -/--code texy -Look at homepage:[https://texy.info]. - -Do you know "La Trine":https://www.latrine.cz? - -"Write me":me@example.com -\-- - -/--texysource -Look at homepage:[https://texy.info]. - -Do you know "La Trine":https://www.latrine.cz? - -"Write me":me@example.com -\-- - - -References ----------- - -In order not to "pollute" the text flow by inserting URLs, it is possible to list all addresses in one place and then just link to them. This is called a reference. In addition to the address, it is possible to add the text of the link and the [modifier | #modifier]. - -/--code texy - [homepage]: https://texy.info/ Texy .(homepage) - [nette]: http://nette.org - -This is [homepage] - -Look at "this site":[nette] -\-- - - -Images -====== - -They are written between square brackets with an asterisk: - -/--code texy -[* image.gif *] -\-- - -/--texysource line -[* image.gif *] -\-- - -In text paragraphs, you often need to choose whether the image should be left-aligned or right-aligned. To do this, use the `<` and `>` character used before the right parenthesis: - -/--code texy -[* image.gif <] Left-aligned image - -[* image.gif >] Right-aligned image -\-- - -/--texysource -[* image.gif <] Left-aligned image - -[* image.gif >] Right-aligned image -\-- - -*Note: In the example above, Texy used a straight CSS style for alignment. It is possible to configure the system to assign a selected class to images instead.* - -*Note: it is possible to set a default directory for all (relative) image URLs. In the examples given, it was `images/`, so in Texy code the directory is not listed, while in the generated HTML it is.* - -*Note: if no alternative text is specified by default (as shown below), Texy will use the default. Here's a simple `image`* - - -Dimensions ----------- - -For local images, Texy detects the dimensions automatically. If you want to specify them manually, enter them as follows: - -/--code texy -[* image.gif 10x20 *] -\-- - -/--texysource line -[* image.gif 10x20 *] -\-- - - -Modifiers ---------- - -You can learn more about them in [another chapter | #modifier], but it doesn't hurt to show how they're written on images. Let's try a modifier to specify alternate text and class: - -/--code texy -[* image.gif .(alt text)[photo] *] -\-- - -/--texysource line -[* image.gif .(alt text)[photo] *] -\-- - - -Reference ---------- - -For the same reasons as for links, images can also be written using references. You need to define URLs (or multiple URLs separated by `|`) and possibly modifiers. - -/--code texy -What a beautiful girl [* picture*] ! - -[* picture*]: image.gif .(my girl) -\-- - -/--texysource -What a beautiful girl [* picture*] ! - -[* picture*]: image.gif .(my girl) -\-- - - -Figure with Caption -------------------- - -Enter three asterisks after the image, followed by a caption: - - -/--code texy -[* image.gif *] *** This is the *caption* below the image -\-- - -/--texysource -[* image.gif *] *** This is the *caption* below the image -\-- - - -Phrases -======= - -Probably the most used syntax in Texy. In almost all cases, a double character is used. - -/--code texy -//italics// - -*too italics* - -**bold** - -***heavy bold*** - -superscript^2 vs. subscript_2 -\-- - -/--texysource -//italics// - -*too italics* - -**bold** - -***heavy bold*** - -superscript^2 vs. subscript_2 -\-- - -/--div .[output] -//italics// - -*too italics* - -**bold** - -***heavy bold*** - -superscript^2 vs. subscript_2 -\-- - -A special case of a phrase is the so-called code. It differs from the other phrases in that its content will no longer be formatted and will be displayed literally: - -/--code texy -This is `<br />` and entity `&ndash` -\-- - -/--texysource -This is `<br />` and entity `&ndash` -\-- - -*Note: whether the `<code>` element is used or another (or none) can be changed simply by configuring Texy.* - - -With Modifier -------------- - -It is possible to insert it into each phrase, always just before the closing character: - -/--code texy -**strong and green .{color:green}** like Hulk -\-- - -/--texysource -**strong and green .{color:green}** like Hulk -\-- - -/--div .[output] -**strong and green .{color:green}** like Hulk -\-- - - -Direct HTML -=========== - -Texy is not a substitute for HTML. It also doesn't look for alternative ways to write HTML. The goal is to simplify content writing. If you find it easier to write a structure directly in HTML, you can do so. HTML tags are fully supported. - -/--code texy -This <strong class=info>is strong</strong> text. -<br> This is not. -\-- - -/--texysource -This <strong class=info>is strong</strong> text. -<br> This is not. -\-- - -*Note: note that Texy adjusts the notation of attributes and tags to be valid (even for XHTML output). It also pays attention to the **well-formed notation**!* - -*Note: Deciding which tags and which attributes can be used in the text is fully user configurable. This demonstrates one example from the distribution.* - - -Lists -===== - -Bulleted lists are written using `*`, `+` or `-`. It must be written at the very beginning of the line and followed by a space. - -/--code texy -- Red -- Green -- Blue -\-- - -/--texysource -- Red -- Green -- Blue -\-- - -/--div .[output] -- Red -- Green -- Blue -\-- - - -Numbered Lists --------------- - -Texy knows these five ways of writing (the first two are equivalent): - -/--code texy -1) Learn -2) Learn -3) Learn - -a) Long -b) Wide -c) Shortsighted - -A) DOS -B) Windows -C) Linux - -I) Yesterday -II) Today -III) Tomorrow -\-- - -/--texysource -1) Learn -2) Learn -3) Learn - -a) Long -b) Wide -c) Shortsighted - -A) DOS -B) Windows -C) Linux - -I) Yesterday -II) Today -III) Tomorrow -\-- - -/--div .[output] -1) Learn -2) Learn -3) Learn - -a) Long -b) Wide -c) Shortsighted - -A) DOS -B) Windows -C) Linux - -I) Yesterday -II) Today -III) Tomorrow -\-- - - -Nested Lists ------------- - -/--code texy -a) Bird - I) Bird - - Red - - Green - - Blue - II) McHale - III) Parish -b) McHale -c) Parish - 1) Bird - 2) McHale - 3) Parish -\-- - -/--texysource -a) Bird - I) Bird - - Red - - Green - - Blue - II) McHale - III) Parish -b) McHale -c) Parish - 1) Bird - 2) McHale - 3) Parish -\-- - - -Definition List ---------------- - -/--code texy -Wild Bill concert: - - date: 9 December 2004 - - place: Vodová Hall, Brno - - price: 260 CZK -\-- - -/--texysource -Wild Bill concert: - - date: 9 December 2004 - - place: Vodová Hall, Brno - - price: 260 CZK -\-- - -/--div .[output] -Wild Bill concert: - - date: 9 December 2004 - - place: Vodová Hall, Brno - - price: 260 CZK -\-- - - -With Modifiers --------------- - -The modifier that affects the entire list is listed in the line before it. Others (classic) at the end of the line: - -/--code texy -.{color:red} -triangl: .{color:blue} - - triangle .{color:green} - - untuned percussion musical instrument - - tringulation tower -\-- - -/--div .[output] -.{color:red} -triangl: .{color:blue} - - triangle .{color:green} - - untuned percussion musical instrument - - tringulation tower -\-- - - -Modifiers .[#modifier] -====================== - -Texy's most powerful weapon. The following types of modifiers can be used: - -- (caption) adds a title to the object (or alternative text to images) -- `[class1 class2 #id]` specifying the class and / or ID of the element -- {class: blue} direct style notation -- {target: _blank} or direct entry of HTML attributes -- horizontal alignment: - - left < - - right > - - centered <> - - to block = - -- vertical alignment: (only for tables) - - up ^ - - center - - - down _ - -Modifiers are written continuously (without spaces) and **must be preceded by a dot**. So for example `.(description)[left]` sets the title attribute to `description` and the class to `left`. - -**Modifiers are always written rightmost**. - -Example of applying a modifier to a paragraph of text: - -/--code texy -Centered with a modifier .<> - -Colored by a modifier .{color:blue; lang: cs} -\-- - -/--texysource -Centered with a modifier .<> - -Colored by a modifier .{color:blue; lang: cs} -\-- - - -Typography .[#typography] -========================= - -This includes all modifications and replacements of the text that modify its appearance in accordance with typographic rules and the like: - -/--code texy -- "English" 'typographic' quotation marks -- dash vs. hyphen: 10-15 vs. česko-slovenský -- dash: one -- two -- typographic cross for dimensions 10 x 20 -- arrows <- and -> and <-> ; -- three dots... -- preservation of HTML entities & -- replacing (TM) or (R) with the relevant entities (C) -\-- - -/--div .[output] -- "English" 'typographic' quotation marks -- dash vs. hyphen: 10-15 vs. česko-slovenský -- dash: one -- two -- typographic cross for dimensions 10 x 20 -- arrows <- and -> and <-> ; -- three dots... -- preservation of HTML entities & -- replacing (TM) or (R) with the relevant entities (C) -\-- - -Spaces handling: - -/--code texy -- inserting unbreakable spaces for one-letter prepositions (a car) -- unbreakable spaces for telephone numbers +420 776 552 046 -\-- - -/--code html -inserting unbreakable spaces for one-letter prepositions (a car) - -unbreakable spaces for telephone numbers +420 776 552 046 -\-- - -*Note: Replacements are usually governed by other rules that determine when -symbol replace and when not. For example, the arrow `->` cannot be at the end of a line, etc. So don't be surprised if in some cases Texy doesn't make the substitution.* - - -Abbreviations, Acronyms ------------------------ - -Double parenthetical notation is used: - -/--code texy -one word: NATO((North Atlantic Treaty Organisation)) - -multiword: "et al."((and more)) -\-- - -/--texysource -one word: NATO((North Atlantic Treaty Organisation)) - -multiword: "et al."((and more)) -\-- - - -Clickable Web Addresses ------------------------ - -Automatic conversion of web addresses and emails into a clickable form - -/--code texy -more information at www.texy.info and also ... -\-- - -/--div .[output] -more information at www.texy.info and also ... -\-- - - -Long Words Hyphenation -====================== - -Very interesting and important function of Texy. Long words can disrupt the appearance of the page, so it's a good idea to tell your browser where to wrap them. Texy searches for these places taking into account national customs, so he divides the word according to syllables: - -/--code texy -nejneobhospodařovávatelnějšími -\-- - -/--code html -nejneobhospoda­řovávatelnější­mi</p -\-- - -*Note: the word length limit is optional* - - -Tables .[#table] -================ - -Example of a simple table, columns are separated by a character `|` - -/--code texy -| first col | second col | third col -| Adam | Eva | Franta -\-- - -And the result is: - -| first col | second col | third col -| Adam | Eva | Franta - - -The table header can be defined with this notation: - -/--code texy -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Jesus | Christ | 33 -| Cecilie | Svobodova | 74 -\-- - -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Jesus | Christ | 33 -| Cecilie | Svobodova | 74 - -If the header does not form a row (rows), we can define it at the cell level. Just insert an asterisk immediately after the `|` character - - -/--code texy -|* First Name | Jesus | Cecilie -|* Last Name | Christ | Svobodova -|* Age | 33 | 74 -\-- - - -|* First Name | Jesus | Cecilie -|* Last Name | Christ | Svobodova -|* Age | 33 | 74 - - -Merging Columns ---------------- - -Notice the double `||`: - -/--code texy -|----------------------------- -| Name || Age -|---------------------------- -| Jesus | Christ | 33 -\-- - -|----------------------------- -| Name || Age -|---------------------------- -| Jesus | Christ | 33 - - -Merging Rows ------------- - -Notice the `^` character symbolizing the upward direction: - - -/--code texy -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Bill || 50 -| ^| 52 -| Jim | Beam | 70 -\-- - -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Bill || 50 -| ^| 52 -| Jim | Beam | 70 - - -Modifiers ---------- - -The following rules apply: -- a modifier affecting the whole table is inserted immediately before the table -- the modifier affecting the line is inserted at the end of the line -- the modifier affecting the column is inserted at the beginning of the cell (left in the cell) -- and finally the modifier affecting the cell is inserted at the end of the cell (right in the cell) - -Take a look at an example. - -/--code texy -.(people) -| .{color: green} first col | second col .>| third col | .{font-style:italic} -| Adam | Eva .{color: blue}| Franta | -\-- - -There is: -- `.(people)` table modifier -- `.{color: green}` column modifier -- `.{font-style:italic}` line modifier -- `.{color: blue}` a také `.>` cell modifier - -So the resulting table looks like this: - - -.(people) -| .{color: green} first col | second col .>| third col | .{font-style:italic} -| Adam | Eva .{color: blue}| Franta | diff --git a/texy/en/syntax.texy b/texy/en/syntax.texy index 2e07405d68..315090464d 100644 --- a/texy/en/syntax.texy +++ b/texy/en/syntax.texy @@ -1,463 +1,891 @@ Syntax ****** ---> "Detailed syntax description":syntax-full +.[perex] +Texy was created to allow inexperienced users to easily edit website content. Therefore, its syntax is intuitive and clear. + + +Cheat Sheet +=========== + +| [Text Formatting |#Text formatting] | Syntax +|----------------------------------------------- +| [Bold text |#Text formatting] | .[text-code] ''**bold text**'' +| [Italics |#Text formatting] | ''*italics*'' or ''//italics//'' +| [Inline code |#Text formatting] | ''`code`'' +| [#Links] | ''"text":URL'' or ''[text](URL)'' +| [#Images] | ''[* image.jpg *]'' +| [#Disabling Formatting] | ''special characters'' +|----------------------------------------------- +| Elements +|----------------------------------------------- +| [#Underlined Headings] | H1 <br> === +| [#Surrounded Headings] | ''### H1'' <br> ## H2 +| [#Bulleted Lists] | ''- first'' <br> ''- second'' +| [#Numbered Lists] | ''1) first'' <br> ''2) second'' +| [#Definition Lists] | term: <br>   ''- first'' +| [#Blockquotes] | ''> blockquote'' +| [#Horizontal Rules] | ''---'' +| [#Tables] | ''\| cell \| cell \|'' +| [Code Blocks |#Preformatted text] | ''/--'' <br> ... <br> ''\--'' +|----------------------------------------------- +| Modifiers .[#toc-modifiers] +|-------------------------------------------------------- +| title | ''.(title)'' +| CSS class | ''.[btn btn-primary]'' +| ID | ''.[#id]'' +| CSS style or HTML attribute | ''.{color: blue}'' or ''.{target: _blank}'' +| horizontal alignment | ''.< .> .<> .='' +| vertical alignment | ''.^ .- ._'' + + +Paragraphs of text +================== +Texy considers a paragraph to be one or more consecutive lines of text. As soon as you leave **one blank line** between them, Texy automatically understands that it should start a new paragraph. -The Texy tool was created to allow inexperienced users to easily edit the content of web pages. Therefore, the syntax is maximally intuitive. The intention is that the text in pure (unformatted) form is clear and its format can be guessed. +This means that Texy will join lines that belong together. You don't have to worry about a sentence breaking in the middle when you shrink the editor window. -Today, Texy also serves well-experienced HTML experts. Allows you to freely combine Texy notation with HTML tags. Thus, experienced users do not have to learn a new meta-language and make full use of their knowledge. Texy only simplifies their work. +```texy +This is the first paragraph. It can easily have multiple lines, +and Texy will join them into one continuous block of text. -The primary logic of the syntax is **not to use any syntax**. Just write plain text. Inserting advanced information, such as CSS classes or links, does not disrupt the flow of text. And it is written in a way that even non-technical users can easily understand. +Only here, after a blank line, does a completely new, second paragraph begin. +``` +However, line merging can be disabled in the configuration, after which each line is considered a separate paragraph: -Paragraphs .[#paragraph] -======================== +/--php +$texy->mergeLines = false; +\-- -A paragraph is considered to be one or more consecutive lines of text. The paragraphs are separated by a blank line. -/--code texy -First paragraph lorem ipsum dolor sit amet. +Line Breaks +----------- -Second paragraph, který tvoří jeden řádek. -And second line of paragraph. Texy will join the lines. -\-- +But what if you just need to break a line without creating a whole new paragraph? This is typically useful for poems, song lyrics, or when writing an address. **Start the new line with a single space**. -To wrap a line in a paragraph, insert one space to the left: +```texy +Karel Novák, + U Tiché pošty 5 + 150 00 Praha 5 +``` -/--code texy -April is the cruellest month, breeding - Lilacs out of the dead land, mixing - Memory and desire, stirring - Dull roots with spring rain. -\-- +Styling Paragraphs +------------------ -Headings .[#heading] -==================== +Sometimes you need to distinguish an entire paragraph - for example, to make it a lead paragraph of an article, center it, or assign it a specific style for a border. This is where [#modifiers] come in, which you can place either on a separate line **before** the paragraph or at the end of its last line. -Headings can be written in two ways: **underlined** or **surrounded**. +```texy +.[perex] +This is the lead paragraph of the article, which, thanks to the modifier, +will get the CSS class "perex" and can thus look different from the rest of the text. -Each headline has its own degree. In the case of **underlined**, the importance of the title is decided by the underline character. From the highest to the lowest, these are: `#` `*` `=` `-` +This paragraph, in turn, has a unique ID assigned. .[#section-intro] -/--code texy -Head title -********** +And this paragraph will be centered. .<> +``` -Subtitle -======== +Text formatting +=============== + +| syntax | output | Syntax ID +|----------------------------------------------------------------------------- +| .[text-code] ''**bold text**'' | **bold text** | `phrase/strong` +| ''*italics* or //italics//'' | *italics* | `phrase/em-alt`, `phrase/em` +| ''***bold italics***'' | ***bold italics*** | `phrase/strong+em` +| ''`inline code`'' | `inline code` | `phrase/code` +| ''x^2 … O_2'' | x^2 … O_2 | `phrase/sup-alt`, `phrase/sub-alt` +| ''x^^2^^ … O__2__'' | x^2 … O_2 | `phrase/sup`🔸, `phrase/sub`🔸 +| ''++inserted text++'' | <ins>inserted text</ins> | `phrase/ins`🔸 +| ''--deleted text--'' | <del>deleted text</del> | `phrase/del`🔸 +| ''>>quoted text<<'' | >>quoted text<< | `phrase/quote` +| ''"blue text .{color: blue}"'' | "blue text .{color: blue}" | `phrase/span` +| ''~blue text .{color: blue}~'' | ~blue text .{color: blue}~ | `phrase/span-alt` +| ''"et al."((and others))'' | "et al."((and others)) | `phrase/acronym` +| ''NBA((National Basketball Association))'' | NBA((National Basketball Association)) | `phrase/acronym-alt` + +Syntaxes marked with 🔸 are not enabled by default and you must turn them on. For example: + +/--php +$texy->allowed['phrase/ins'] = true; \-- -For **surrounded** titles, the level determines the number of preceding characters. It can be `#` nebo `=` +For simple numerical indices, you can use the shorthand syntax `x^2` and `O_2`, but for more complex cases, the double-character variant is more robust, or you can use the HTML tags `<sup>` and `<sub>`. -The following applies: the more characters, the more important the title (minimum two characters, maximum seven). +There **must not be spaces** inside the syntax characters: -/--code texy -=== Head title === +```texy +Wrong: ** this will not be bold ** +Correct: **this will be bold** +``` -## Subtitle -\-- -As you can see in the case of the subtitle, the characters on the right can be omitted. +Styling Text +------------ +This is one of Texy's most powerful features. You can "attach" [#modifiers] to any formatted text to add a CSS class, ID, or direct style. The modifier is always inserted **just before the closing tag**: -Horizontal Lines .[#horizline] -============================== +```texy +This text is **strong and green .{color:green}** like the Hulk. -Texy knows these notations: +Warning: --This feature is deprecated .[deprecated]-- +``` +If you want to apply a modifier to text without making it bold or italic, use quotation marks `"` or tildes `~` as the enclosing characters. Texy will then create a universal HTML `<span>` tag with your styles: -/--code texy ------------- +```texy +Regular text, but "this piece is red .{color: red}", and the rest is not. +``` -******** -\-- +Formatting and Links in One +--------------------------- -Turning Off the Texy .[#disable-Texy] -===================================== +You can turn formatted text into a link - simply add a colon and the URL: -The keyword `html` or` text` affects whether the content will be understood as HTML (including tags) or plain text. +```texy +Visit our **new gallery**:https://example.com/gallery +``` -/--code texy - /---html - <em>example</em>: **this is not strong** - \--- +This works for bold text, italics, and inline code. - /---text - <em>example</em>: **this is not strong** - \--- -\-- +Writing Special Characters +-------------------------- -To turn off Texy inline, it is possible to use a double apostrophe `''` and wrap a part of the text that is not to be Texy processed. +What if you want to write `**text**` literally, including the asterisks, without it becoming bold text? You have three options: -/--code texy - Example: ''**this is not strong**'' -\-- +- a backslash is the quickest way to escape a single special character `\**text\**` +- double apostrophes [disable Texy|#Disabling Formatting] for the entire phrase `''**text**''` +- you can use standard HTML entities `**text**` -Quotes .[#blockquote] -===================== +Links +===== -Quotes are indented, similarly to emails, by a character `>` +Links are the soul of the internet. In Texy, their creation is designed to be as natural and clear as possible directly within the text. -/--code texy -> This is a blockquote with two paragraphs. -> -> 640 K should be enough for everyone -\-- +The basic syntax for a link is simple and highly readable. Enclose the linked text in `"` (or other characters for [#text formatting]) and immediately append a colon and the target URL: +```texy +Visit the official website of the "Nette Framework":https://nette.org. -Links .[#link] -============== +If you have a question, "email us":info@example.com. +``` -Links are written by enclosing the referencing text in quotation marks, followed by a colon and a URL. Texy tries to intelligently guess the end of the URL. You can also help it by enclosing the URL in square brackets. The `http://` section is optional. +The advantage is that Texy is intelligent and automatically recognizes where the URL ends. So you don't have to worry about it accidentally including a period or comma at the end of a sentence in the link. However, if the URL contains non-standard characters, you can enclose it in square brackets to precisely define the start and end of the address: -It is also possible to insert emails as a link, Texy transforms them into a form that should confuse spambots. +```texy +"Read our article":[https://example.com/news?id=1&category=articles] +``` -/--code texy -Look at homepage:[https://texy.info]. +Syntax ID `phrase/span`, `phrase/span-alt` | [PhraseModule |configuration#phrasemodule] and [LinkModule |configuration#linkmodule] -Do you know "La Trine":https://www.latrine.cz? -"Write me":me@example.com -\-- +Alternative Link Syntax +----------------------- +Are you used to the formats used by Markdown or Wikipedia? Texy understands them too. You can choose the style that suits you best. -Images .[#image] -================ +```texy +[Link text](https://address.com) // Style known from Markdown +[Link text | https://address.com] // Style known from MediaWiki +text:[target URL or reference] // Single-word link +``` -They are written between square brackets with an asterisk: +Syntax ID `phrase/markdown`, `phrase/wikilink`, `phrase/quicklink` | [PhraseModule |configuration#phrasemodule] -/--code texy -[* image.gif .(alternative text) *] -\-- +Organizing Links with References +-------------------------------- + +When writing longer texts, it can be inconvenient to insert long URLs directly into paragraphs - it can harm readability and clarity. For these cases, Texy has **reference links**. + +In the text, you use only a short, easily memorable reference name. And at the end of the document, you define all these references clearly. + +```texy +We recommend studying the "official documentation":[doc] and going through the "syntax examples":[syntax]. +The entire project is built on [Nette]. + +​[doc]: https://texy.nette.org/en/ "Texy Documentation!" +​[syntax]: https://texy.nette.org/en/syntax +​[Nette]: https://nette.org +``` -In text paragraphs, you often need to choose whether the image should be left-aligned or right-aligned. To do this, use the `<` and `>` character used before the right parenthesis: +Syntax ID `link/reference`, `link/definition` | [LinkModule |configuration#linkmodule] -/--code texy -[* image.gif <] Left-aligned image. Lorem ipsum ... -[* image.gif >] Right-aligned image. Curabitur quam ... +Automatic Links +--------------- + +Whenever you write a URL (starting with `http://`, `https://`, `www.`) or an email address in the text, Texy will automatically recognize it and convert it into a clickable link. You don't have to do anything at all. + +```texy +You can find our website at www.example.com. +For support, write to support@example.com. +``` + +Syntax ID `link/url`, `link/email` | [LinkModule |configuration#linkmodule] + + +Styling Links +------------- + +With [#modifiers], you can easily add other properties to links: + +```texy +"External link .[external](Opens in a new window){target:_blank}":https://google.com +``` + +The special class `nofollow` adds the `rel="nofollow"` attribute to the link, signaling to search engines not to follow this link. This is useful, for example, for links in comments. + +```texy +"A link I don't trust .[nofollow]":https://example.com +``` + + +Automatic Email Masking +----------------------- + +Texy automatically obfuscates (masks) email addresses from spambots: + +```latte +<a href="mailto:info@example.com">info@<!-- -->example.com</a> +``` + +You can disable this behavior: + +/--php +$texy->obfuscateEmail = false; \-- -Figure with Caption +Direct HTML +=========== + +Texy is designed so that you don't have to write HTML at all. But what if you encounter a situation where inserting a direct HTML tag is simpler, or you need to create something that Texy's syntax doesn't cover? No problem. Texy gives you complete freedom to combine both worlds. + +You can seamlessly switch between Texy syntax and pure HTML whenever it suits you. + +```texy +This is **bold text** in Texy and this is <strong>bold text</strong> using HTML. + +<div class="info-box"> + <h3>You can also insert entire complex blocks</h3> +</div> +``` + +You might think that inserting direct HTML can be risky. What if you make a mistake or someone inserts malicious code? Texy thinks about this and acts as an intelligent filter and helper: + +- **Corrects errors:** Texy ensures that the resulting code is always valid and won't break your page. +- **Monitors security:** By default, Texy has a list of allowed tags and their attributes. If an unknown tag or a potentially dangerous attribute (e.g., `onclick`) appears in the code, Texy will safely remove it. This protects your website from XSS attacks. +- **Ensures consistent output:** No matter what HTML code you insert, Texy will make sure the result is always well-formed. + +You can customize this protective shield. Using the `$texy->allowedTags` configuration, you can precisely define which HTML tags and attributes are allowed on your website and which are not. + +This gives you full control over what HTML, for example, editors can use, ensuring the consistency and security of the entire site. For more information, see the "configuration":configuration#allowedtags section. + +Syntax ID `html/tag`, `html/comment` | [HtmlModule |configuration#htmlmodule] + + +Headings +======== + +Texy offers you two elegant and intuitive ways to create headings: **underlined** and **surrounded**. + + +Underlined Headings ------------------- -Enter three asterisks after the image, followed by a caption: +This style is reminiscent of a typewriter. Simply place an underline (at least 3 characters) below the heading. The importance of the title is determined by the underlining character. From highest to lowest, these are: `#` `*` `=` `-` +```texy +This is the most important heading of the entire document +​################################################ -/--code texy -[* image.gif *] *** This is the *caption* below the image -\-- +And this is a second-level heading +​****************************** +``` +Syntax ID `heading/underlined` | [HeadingModule |configuration#headingmodule] -Phrases .[#phrase] -================== -Probably the most used syntax in Texy. In almost all cases, a double character is used. +Surrounded Headings +------------------- -/--code texy -//italics// +This method is very quick to write. You "wrap" the heading text between `#` or `=` characters. Here, the level of the heading is determined by the **number** of characters used (2 to 7). The more characters, the more important the heading. -**bold** +```texy +==== Most important heading (H1) -x^2 + y^3 -\-- +=== Less important (H2) +== Even less important (H3) +``` -/--div .[output] -//italics// +You can use borders on both sides (for better visual clarity) or just at the beginning. Texy can handle both variants. -**bold** +Syntax ID `heading/surrounded` | [HeadingModule |configuration#headingmodule] -x^2 + y^3 -\-- +Styling Headings +---------------- -Texts can also be temporarily turned off - the content will not be formatted and will be displayed literally: +You can add [#modifiers] to any heading. This allows you to assign it a specific CSS class for styling or a unique ID that you can then link to. -/--code texy -Remove ''<br />'' and entity ''&ndash'' -\-- +```texy +Heading with red color .[red-heading] +​========================================== +### Heading with a unique ID for linking .[#contact] +``` -Direct HTML .[#html] -==================== -Texy is not a substitute for HTML. It also doesn't look for alternative ways to write HTML. The goal is to simplify content writing. If you find it easier to write a structure directly in HTML, you can do so. HTML tags are fully supported. +Automatic Anchors for Easy Navigation +------------------------------------- -/--code texy -This <strong class=info>is strong</strong> text. -<br> This is not. -\-- +Don't want to come up with an ID for every heading manually? Texy can do it for you! In the configuration, you can enable automatic ID generation for all headings. This is incredibly useful for directly linking to specific sections. +```php +// Enable automatic ID generation +$texy->headingModule->generateID = true; -Lists .[#list] -============== +// Optionally set a prefix for generated IDs (e.g., "toc-") +$texy->headingModule->idPrefix = 'toc-'; +``` -Bulleted lists are written using `*`, `+` or `-`. It must be written at the very beginning of the line and followed by a space. +With this setting, the heading `## My Chapter` will automatically get an ID like `id="toc-my-chapter"` without you having to write anything extra. -/--code texy -- Red -- Green -- Blue -\-- + +Lists +===== + + +Bulleted Lists +-------------- + +For a quick list of items where the order doesn't matter, a bulleted list is perfect. Just start each line with a hyphen `-`, an asterisk `*`, or a plus sign `+`, followed by a space. All three characters work the same, so you can choose the one you prefer. + +```texy +What needs to be bought: + +- Milk +- Bread +* Eggs ++ Butter +``` + +Syntax ID `list` | [ListModule |configuration#listmodule] Numbered Lists -------------- -Texy knows these five ways of writing (the first two are equivalent): +Texy supports various numbering styles: -/--code texy -1) Learn -2) Learn -3) Learn +| `1.` | Arabic numerals (with a period) +| `1)` | Arabic numerals (with a parenthesis) +| `a)` | Lowercase letters of the alphabet +| `A)` | Uppercase letters of the alphabet +| `I)` | Roman numerals -a) Long -b) Wide -c) Shortsighted +The beauty of this is that you don't have to worry about correct numbering at all. Even if you number all the lines with a one, Texy will automatically renumber them for you. This is a huge advantage when you later need to add, delete, or move an item. -A) DOS -B) Windows -C) Linux -I) Yesterday -II) Today -III) Tomorrow -\-- +Nested and Combined Lists +------------------------- +The power of lists truly shines when you combine and nest them. This allows you to create clear, multi-level structures. You create nesting simply by indenting the line by at least **two spaces** (or one tab). -Nested Lists ------------- +```texy +1) First chapter + a) Subchapter 1.1 + - First point + - Second point + b) Subchapter 1.2 +2) Second chapter + - Main idea + - Another note +``` -/--code texy -a) Bird - I) Bird - - Red - - Green - - Blue - II) McHale - III) Parish -b) McHale -c) Parish - 1) Bird - 2) McHale - 3) Parish -\-- +Definition Lists +---------------- -Definition List ---------------- +For cases where you need to create a glossary of terms or clearly explain several terms, a definition list is ideal. -Wild Bill concert: - - date: 9 December 2004 - - place: Vodová Hall, Brno - - price: 260 CZK +On the first line, write the term you want to define and end it with a colon. On the following lines, write its definition, indenting each line and starting it with a hyphen `-`. -/--code texy -Wild Bill concert: - - date: 9 December 2004 - - place: Vodová Hall, Brno - - price: 260 CZK -\-- +```texy +HTML: + - Markup language for creating web pages. + - Abbreviation for HyperText Markup Language. +CSS: + - Language for describing the presentation (styling) of pages. + - Abbreviation for Cascading Style Sheets. +``` -Modifiers .[#modifier] -====================== +Syntax ID `list/definition` | [ListModule |configuration#listmodule] -Texy's most powerful weapon. The following types of modifiers can be used: -- (caption) adds a title to the object (or alternative text to images) -- `[class1 class2 #id]` specifying the class and / or ID of the element -- {class: blue} direct style notation -- {target: _blank} or direct entry of HTML attributes -- horizontal alignment: - - left < - - right > - - centered <> - - to block = +Styling Lists +------------- -- vertical alignment: (only for tables) - - up ^ - - center - - - down _ +Just like with other elements in Texy, you can easily add [#modifiers] to lists to change their appearance. -Modifiers are written continuously (without spaces) and **must be preceded by a dot**. So for example `.(description)[left]` sets the title attribute to `description` and the class to `left`. +**Entire list:** Write the modifier on the line **before** the start of the list. -**Modifiers are always written rightmost**. +```texy +.[colored-list] +- First item +- Second item +``` -Example of applying a modifier to a paragraph of text: +**Individual item:** Add the modifier to the **end** of the line of the given item or definition term. -/--code texy -Centered with a modifier .<> +```texy +- Regular item +- This item is important! .{font-weight: bold} +- Another regular item +``` -Colored by a modifier .{color:blue; lang: cs} -\-- -/--code html -<p style="text-align:center">Centered with a modifier</p> +Images +====== -<p style="color:blue" lang="cs">Colored by a modifier</p> -\-- +The basic syntax is very simple. Just enclose the path to the image (whether a local file or a URL) in square brackets with an asterisk: +```texy +[* image.jpg *] +[* https://domain.com/logo.png *] +``` -Typography .[#typography] -========================= +Often you will want the text to wrap around the image. For this, there are simple alignment tags that are inserted before the closing bracket: -This includes all modifications and replacements of the text that modify its appearance in accordance with typographic rules and the like: +```texy +[* image.jpg <] This text will flow smoothly around the image from the right side. -/--code texy -- "English" 'typographic' quotation marks -- dash vs. hyphen: 10-15 vs. česko-slovenský -- dash: one -- two -- typographic cross for dimensions 10 x 20 -- arrows <- and -> and <-> ; -- three dots... -- preservation of HTML entities & -- replacing (TM) or (R) with the relevant entities (C) -\-- +[* image.jpg >] In this case, the text will instead flow around the image from the left side. + +[* large-image.jpg *] +This text will continue below the image, which does not float. +``` + +A properly inserted image should also have "alternative text," which is displayed if the image fails to load. Using a [modifier|#modifiers], you can add this text and other elements for styling. + +```texy +[* landscape-photo.jpg .(A beautiful mountain landscape at sunset)[main-photo] *] +``` + + +Image Dimensions +---------------- + +Texy can automatically detect the dimensions of local images (if the `$texy->imageModule->fileRoot` path is set) and add them to the HTML, which speeds up page loading. However, if you want to set the dimensions manually, you have several options: + +| `[* img.jpg 150x100 *]` | Exact width 150px and height 100px +| `[* img.jpg 150 *]` | Width will be 150px, height will be automatically calculated while maintaining the aspect ratio +| `[* img.jpg ?x100 *]` | Height will be 100px, width will be automatically calculated + + +Clickable Images +---------------- + +Do you want a large image to be displayed when a small thumbnail is clicked? Or for an image to link to another page? Just add a colon and the target URL after the image syntax. + +```texy +[* thumbnail.jpg *]:large.jpg +[* nette-logo.png *]:https://nette.org +``` + +For galleries, there is also a handy shortcut `::`. This automatically creates a link to the same file located at `$texy->imageModule->linkedRoot`. + + +Visible Caption Below the Image +------------------------------- + +If you want to add a visible caption under an image (e.g., the author's name or a description of the scene), write three asterisks `***` and the caption text after it. Texy creates a figure structure from this - by default `<div class="figure">`, or a semantic `<figure>` and `<figcaption>` if you set `figureModule->tagName = 'figure'`. + +```texy +[* photo.jpg *] *** This is a caption. It can also contain **other formatting**. +``` + + +Organizing Images with References +--------------------------------- + +If you use one image multiple times in the text or want to have all image definitions neatly in one place, you can use references. In the text, you use only a placeholder name, and at the end of the file, you define what this name means. + +```texy +In our logo [* company-logo *] you can see the symbol of our vision. -/--div .[output] -- "English" 'typographic' quotation marks -- dash vs. hyphen: 10-15 vs. česko-slovenský -- dash: one -- two -- typographic cross for dimensions 10 x 20 -- arrows <- and -> and <-> ; -- three dots... -- preservation of HTML entities & -- replacing (TM) or (R) with the relevant entities (C) +​[* company-logo *]: /images/logo.svg 200x50 .(Our company logo) +``` + +This approach significantly clarifies the main text and simplifies image management. + +Syntax ID `image/definition` | [ImageModule |configuration#imagemodule] + + +Preformatted text +================= + +In Texy, you can easily insert blocks of code or any preformatted text where you want to ensure it is displayed exactly as you write it - including all spaces and line breaks. This is ideal for examples of source code, logs, or ASCII art. + +To insert such a block, use the `/--` and `\--` delimiters: + +```texy +/-- +function hello() { + echo 'Hello World'; +} \-- +``` -Spaces handling: +To make your code even more readable, you can tell Texy which programming language it is written in and create a handler that, for example, syntax highlights it, see the "example":custom-handlers#syntax-highlighting. Just add the keyword `code` and the language name after the initial `/--` tag: -/--code texy -- inserting unbreakable spaces for one-letter prepositions (a car) -- unbreakable spaces for telephone numbers +420 776 552 046 +```texy +/--code javascript +console.log('JavaScript'); \-- /--code html -inserting unbreakable spaces for one-letter prepositions (a car) - -unbreakable spaces for telephone numbers +420 776 552 046 +<div>This is HTML code</div> \-- +``` -*Note: Replacements are usually governed by other rules that determine when -symbol replace and when not. For example, the arrow `->` cannot be at the end of a line, etc. So don't be surprised if in some cases Texy doesn't make the substitution.* +Content Blocks (divs) +===================== -Abbreviations, Acronyms .[#acronym] ------------------------------------ +Texy allows you to create generic `<div>` blocks, thanks to which you can easily group content into logical units and then style them. -Double parenthetical notation is used: +You create a block using the `/--div` and `\--` tags. In addition, you can easily add [#modifiers]: -/--code texy -one word: NATO((North Atlantic Treaty Organisation)) +```texy +/--div .[important] +## Important Notice -multiword: "et al."((and more)) +This text will be enclosed in a `<div class="important">` block. +This allows you to style it with CSS to make it stand out. \-- +``` +The power of `<div>` blocks also lies in the ability to nest them. This allows you to create even more complex structures directly in Texy without having to write HTML manually. -Clickable Web Addresses ------------------------ +```texy +/--div .[outer] + This is the outer block. -Automatic conversion of web addresses and emails into a clickable form + /--div .[inner] + And this is a nested, inner block. + \-- -/--code texy -more information at www.texy.info and also ... + Here we are back in the outer block. \-- +``` +Thanks to this simple syntax, you can keep your content clear and semantically well-structured. -/--div .[output] -more information at www.texy.info and also ... -\-- +Disabling Formatting +==================== + +Sometimes it can be useful to "turn off" Texy for a moment and insert a piece of text where Texy should not process its tags. -Long Words Hyphenation .[#longwords] -==================================== +If you need to insert a more complex HTML structure without Texy parsing the tags, use the `/--html` block: -Very interesting and important function of Texy. Long words can disrupt the appearance of the page, so it's a good idea to tell your browser where to wrap them. Texy searches for these places taking into account national customs, so he divides the word according to syllables: +```texy +/--html +<em>This text will be processed as HTML, so it will be in italics.</em> -/--code texy -nejneobhospodařovávatelnějšími +**But Texy ignores these asterisks, so they won't be bold.** \-- +``` -/--code html -nejneobhospoda­řovávatelnější­mi</p +If you want to display text exactly as it is written, ignoring all tags (both Texy and HTML), use the `/--text` block. Everything inside this block will be displayed as plain text. + +```texy +/--text +<em>This text will be displayed with the tags, but it will not be in italics.</em> + +**This won't be bold either.** \-- +``` -*Note: the word length limit is optional* +But what if you don't want to disable Texy for an entire block of text, but just for a short phrase in the middle of a sentence? For these cases, there is an elegant and quick solution: wrap the text in **double apostrophes** `''`: +```texy +If you want to show how to write bold text, you would write: The syntax is ''**bold text**''. +``` -Tables .[#table] -================ +The result will not be bold text; instead, the string `**bold text**` will be printed literally. -Example of a simple table, columns are separated by a character `|` -/--code texy -| first col | second col | third col -| Adam | Eva | Franta -\-- +Tables +====== -And the result is: +To create a table, start each row with a `|` character and also separate the individual cells with this character. Texy will take care of the alignment and correct HTML on its own. -| first col | second col | third col -| Adam | Eva | Franta +```texy +| Jan | Novák | Praha +| Eva | Svobodová | Brno +``` +The result will be a clear and correctly formatted table. -The table header can be defined with this notation: +Syntax ID `table` | [TableModule |configuration#tablemodule] -/--code texy -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Jesus | Christ | 33 -| Cecilie | Svobodova | 74 -\-- -|----------------------------- -| First Name | Last Name | Age -|---------------------------- -| Jesus | Christ | 33 -| Cecilie | Svobodova | 74 +Table Header +------------ +Every proper table should have a header that describes what is in each column. You create the header by separating it from the rest of the table with a line containing hyphens `-`. -Merging Columns ---------------- +```texy +| Name | Age | City +|----------|-----|------- +| Jan | 25 | Prague +| Eva | 30 | Brno +``` -Notice the double `||`: +Alternatively, you can define headers for individual rows (for example, if you have labels in the first column). You achieve this by adding an asterisk `*` immediately after the initial `|`. -/--code texy -| Name || Age -|---------------------------- -| Jesus | Christ | 33 -\-- +```texy +|* Name | Jan | Eva +|* Age | 25 | 30 +|* City | Praha | Brno +``` + + +Merging Cells +------------- + +Sometimes it is necessary to merge several cells together, either in columns or in rows. -| Name || Age +**Merging columns:** For horizontal cell merging, simply omit the separator and use a double vertical bar `||` instead. The cell to the right will be merged with the cell to its left. + +```texy +| First Name || Age |---------------------------- -| Jesus | Christ | 33 +| Jan | Novák | 25 +``` +**Merging rows:** For vertical cell merging, use the caret symbol `^` in the cell you want to attach to the one above it. This tells Texy: "Merge this cell with the one above it." -Merging Rows ------------- +```texy +| Month | Sales | +|---------|---------- +| January | 150 pcs | +| February | ^| +| March | 210 pcs | +``` -Notice the `^` character symbolizing the upward direction: +In this example, the cell with sales for January and February will be merged. +This way, several cells can be merged across rows and columns: -/--code texy +```texy | First Name | Last Name | Age |---------------------------- | Bill || 50 | ^| 52 | Jim | Beam | 70 -\-- +``` -| First Name | Last Name | Age -|---------------------------- -| Bill || 50 -| ^| 52 -| Jim | Beam | 70 + +Styling Tables +-------------- + +As with other elements in Texy, you can also add [#modifiers] to tables and their parts to change their appearance (e.g., CSS classes, styles, or IDs). + +**Entire table:** Place the modifier for the entire table on a separate line just before it. + +```texy +.[data-table table-striped] +| Header 1 | Header 2 +|------------|------------ +| data | data +``` + +**Individual rows:** If you want to style a specific row, add the modifier to its end. + +```texy +| Name | Status +|-------|-------------- +| Petr | Approved +| Jana | Rejected | .{background: #ffdddd} +``` + +**Individual columns:** To style an entire column, insert the modifier at the beginning of the first cell of that column. + +```texy +| Name | .> Price | In Stock +|----------------|-----------|--------- +| Product A | 1 200 Kč | Yes +| Product B | 850 Kč | No +``` + +**Specific cell:** Write the modifier for a single cell directly in it, usually at the end of its content. + +```texy +| Task | Status +|----------------------|------------------------------------- +| Prepare documents | Done +| Check data | In progress .{color: orange; font-weight: bold} +``` + + +Blockquotes +=========== + +If you need to emphasize someone else's idea in your text, cite a source, or just visually separate a block of text, simply start the line with the `>` character. + +```texy +> This is a blockquote. It serves to highlight an important idea or an excerpt from another source. +``` + +A blockquote doesn't have to be just one paragraph. If you want to continue with another paragraph within the same blockquote, simply insert a blank line that also starts with the `>` character. + +```texy +> This is the first paragraph of the blockquote. Lorem ipsum dolor sit amet. +> +> And this is the second paragraph, which still belongs to the same blockquote. +> This way you can structure even longer texts. +``` + +Texy even supports nested blockquotes, which is useful if you are quoting someone who is quoting someone else. For each additional level of nesting, add another `>` character. + +```texy +> This is the outer, main blockquote. +> +> > And this is a nested, second-level blockquote. +> +> Here the text returns to the main blockquote. +``` + +Inside blockquotes, you can of course use other formatting, such as **bold text** or *italics*. + + +Horizontal Rules +================ + +Sometimes it is necessary to visually divide the text. A horizontal rule is great for this. On a separate line, write three or more hyphens `---` or asterisks `***`. + +```texy +The first part of the text on a certain topic. + +*** + +The second part of the text, which begins after the visual separation. +``` + +To create a horizontal rule, it **must be preceded by a blank line**. If you were to write it immediately after the text, Texy would think you wanted to create an underlined heading. + +Syntax ID `horizline` | [HorizLineModule |configuration#horizlinemodule] + + +Typography +========== + +The power of Texy lies not only in formatting, but also in automatic typographic corrections. Texy takes care of the details that make text professional and easy to read, all according to Czech typographic rules. This allows you to focus solely on the content. + +**Quotes:** You don't have to worry about how to type correct typographic quotes on the keyboard. Texy will do it for you. + +It automatically converts classic ''"typewriter quotes"'' into the correct English “quotes” and nested ‘quotes’. The type of quotes depends on the locale setting: + +```php +$texy->typographyModule->locale = 'cs'; // Czech +$texy->typographyModule->locale = 'en'; // English +``` + +**Dashes and hyphens:** It intelligently recognizes when to use a short hyphen (in hyphenated words) and when to use a longer en dash - for example, in ranges (10-15) or between words. + +```texy +10-15 → 10–15 (en dash for ranges) +czech-slovak → czech-slovak (hyphen remains) +word -- word → word – word (en dash between words) +word --- word → word — word (em dash) +``` + +**Non-breaking spaces**: One of the biggest advantages is the automatic insertion of non-breaking spaces where needed. This prevents single-letter words (like `a` or `I`) from being left alone at the end of a line, which is a common typographic error. + +```texy +// You write: +I visited a castle in Prague. + +// Texy ensures that "a" never remains at the end of a line: +I visited a castle in Prague. +``` + +It also takes care of correct spacing in phone numbers or dates to prevent them from breaking. + +```texy ++420 776 552 046 → +420 776 552 046 (all spaces non-breaking) +``` + +**Automatic symbols:** Texy also makes it easier for you to write frequently used symbols. + +| You write | Texy generates | Description +|----- +| `...` | … | Ellipsis +| `(c)` | © | Copyright +| `(r)` | ® | Registered trademark +| `(tm)` | ™ | Trademark +| `10 x 5` | 10 × 5 | Multiplication sign +| `+-` | ± | Plus-minus +| `<-` `->` `<->` | ← → ↔ | Arrows (surround with spaces) + +Thanks to these automatic adjustments, your text will always look professional without you having to know complex keyboard shortcuts or HTML entities. + + +Hyphenation of Long Words +------------------------- + +You know it - a long word appears in the text, such as "antidisestablishmentarianism," and on a narrow mobile phone screen, it breaks the entire page layout. Fortunately, Texy offers an elegant solution: it can insert invisible "soft hyphens" (`­`) into the word. These hyphens tell the browser where (between syllables) it can safely break the word if it doesn't fit at the end of the line. If the word fits on the line, the hyphens remain hidden and nothing happens. + +```latte +antidisestablish­mentarianism +``` + +Thanks to this, your text will always adapt beautifully to any screen width without unwanted horizontal scrolling. + +Because this feature is not suitable for all types of websites, it is disabled by default. You can activate it in the configuration: + +```php +$texy->allowed['longwords'] = true; + +// Set the minimum word length from which to hyphenate (e.g., 20 characters) +$texy->longWordsModule->wordLimit = 20; +``` + +Syntax ID `longwords` | [LongWordsModule |configuration#longwordsmodule] + + +Emoticons +========= + +Texy can automatically convert classic text smileys into graphical emoticons. Simply type the smiley as you are used to, and Texy will take care of the rest. + +| You write | Texy generates +|----- +| `:-)` | 🙂 +| `:-(` | ☹ +| `;-)` | 😉 +| `:-D` | 😁 +| `:-P` | 😛 + +Depending on the configuration, Texy can convert these shortcuts either to modern Unicode emoji (as in the table above) or to small images (`<img>`). + +To prevent unwanted conversions, for example in technical texts, this feature is disabled by default. If you want to use it, you just need to enable it: + +```php +$texy->allowed['emoticon'] = true; +``` + +More information about available emoticons and configuration options can be found in the [EmoticonModule configuration |configuration#emoticonmodule]. + +Syntax ID `emoticon` | [EmoticonModule |configuration#emoticonmodule] diff --git a/texy/en/try-settings.texy b/texy/en/try-settings.texy index 0b8706a6a8..1a7a34e3c3 100644 --- a/texy/en/try-settings.texy +++ b/texy/en/try-settings.texy @@ -21,9 +21,9 @@ function blockHandler( Texy\HandlerInvocation $invocation, string $blocktype, string $content, - string $lang, + ?string $lang, Texy\Modifier $modifier -): Texy\HtmlElement +): Texy\HtmlElement|string|null { if ($blocktype !== 'block/code') { // nothing to do From d04e45a3030431d55e088d51c20da93a196ce0ae Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Fri, 31 Oct 2025 03:02:48 +0100 Subject: [PATCH 004/112] removed null as an array offset --- application/bg/routing.texy | 2 +- application/cs/routing.texy | 4 ++-- application/de/routing.texy | 2 +- application/el/routing.texy | 2 +- application/en/routing.texy | 4 ++-- application/es/routing.texy | 2 +- application/fr/routing.texy | 2 +- application/hu/routing.texy | 2 +- application/it/routing.texy | 2 +- application/ja/routing.texy | 2 +- application/pl/routing.texy | 2 +- application/pt/routing.texy | 2 +- application/ro/routing.texy | 2 +- application/ru/routing.texy | 2 +- application/sl/routing.texy | 2 +- application/tr/routing.texy | 2 +- application/uk/routing.texy | 2 +- utils/bg/arrays.texy | 4 ++-- utils/cs/arrays.texy | 4 ++-- utils/de/arrays.texy | 4 ++-- utils/el/arrays.texy | 4 ++-- utils/en/arrays.texy | 4 ++-- utils/es/arrays.texy | 4 ++-- utils/fr/arrays.texy | 4 ++-- utils/hu/arrays.texy | 4 ++-- utils/it/arrays.texy | 4 ++-- utils/ja/arrays.texy | 4 ++-- utils/pl/arrays.texy | 4 ++-- utils/pt/arrays.texy | 4 ++-- utils/ro/arrays.texy | 4 ++-- utils/ru/arrays.texy | 4 ++-- utils/sl/arrays.texy | 4 ++-- utils/tr/arrays.texy | 4 ++-- utils/uk/arrays.texy | 4 ++-- 34 files changed, 53 insertions(+), 53 deletions(-) diff --git a/application/bg/routing.texy b/application/bg/routing.texy index f6d0f09e15..d3a66e32fa 100644 --- a/application/bg/routing.texy +++ b/application/bg/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/cs/routing.texy b/application/cs/routing.texy index 826bed4363..3a2e753731 100644 --- a/application/cs/routing.texy +++ b/application/cs/routing.texy @@ -306,7 +306,7 @@ Parametry `presenter`, `action` a `module` už mají předdefinované filtry, kt Obecné filtry ------------- -Vedle filtrů určených pro konkrétní parametry můžeme definovat též obecné filtry, které obdrží asociativní pole všech parametrů, které mohou jakkoliv modifikovat a poté je vrátí. Obecné filtry definujeme pod klíčem `null`. +Vedle filtrů určených pro konkrétní parametry můžeme definovat též obecné filtry, které obdrží asociativní pole všech parametrů, které mohou jakkoliv modifikovat a poté je vrátí. Obecné filtry definujeme pod prázdným klíčem. ```php use Nette\Routing\Route; @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/de/routing.texy b/application/de/routing.texy index 251247119d..ff2f7378e0 100644 --- a/application/de/routing.texy +++ b/application/de/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/el/routing.texy b/application/el/routing.texy index 4886294f8b..31800ef023 100644 --- a/application/el/routing.texy +++ b/application/el/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/en/routing.texy b/application/en/routing.texy index daa1c181d8..4b83f317f2 100644 --- a/application/en/routing.texy +++ b/application/en/routing.texy @@ -306,7 +306,7 @@ The parameters `presenter`, `action`, and `module` already have predefined filte General Filters --------------- -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`. +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 empty key. ```php use Nette\Routing\Route; @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/es/routing.texy b/application/es/routing.texy index 829edb3c7d..d906f83f48 100644 --- a/application/es/routing.texy +++ b/application/es/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/fr/routing.texy b/application/fr/routing.texy index 41a575cac4..66bfebc31b 100644 --- a/application/fr/routing.texy +++ b/application/fr/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/hu/routing.texy b/application/hu/routing.texy index 0887f12764..4fb937eba5 100644 --- a/application/hu/routing.texy +++ b/application/hu/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/it/routing.texy b/application/it/routing.texy index c308d57f62..36b8f5968c 100644 --- a/application/it/routing.texy +++ b/application/it/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/ja/routing.texy b/application/ja/routing.texy index d3c524f7ab..4dd36be16c 100644 --- a/application/ja/routing.texy +++ b/application/ja/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/pl/routing.texy b/application/pl/routing.texy index 49f68aa093..34577ae078 100644 --- a/application/pl/routing.texy +++ b/application/pl/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/pt/routing.texy b/application/pt/routing.texy index 659307ca93..6becfb0d3b 100644 --- a/application/pt/routing.texy +++ b/application/pt/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/ro/routing.texy b/application/ro/routing.texy index e0b8840500..0d580d0565 100644 --- a/application/ro/routing.texy +++ b/application/ro/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/ru/routing.texy b/application/ru/routing.texy index dbe0c6a228..4e2896a131 100644 --- a/application/ru/routing.texy +++ b/application/ru/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/sl/routing.texy b/application/sl/routing.texy index dbdd549664..06c2ccee69 100644 --- a/application/sl/routing.texy +++ b/application/sl/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/tr/routing.texy b/application/tr/routing.texy index 5b0b81e30a..8fccd6c0b1 100644 --- a/application/tr/routing.texy +++ b/application/tr/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/application/uk/routing.texy b/application/uk/routing.texy index a9db693215..ab3b132ccd 100644 --- a/application/uk/routing.texy +++ b/application/uk/routing.texy @@ -314,7 +314,7 @@ use Nette\Routing\Route; $router->addRoute('<presenter>/<action>', [ 'presenter' => 'Home', 'action' => 'default', - null => [ + '' => [ Route::FilterIn => function (array $params): array { /* ... */ }, Route::FilterOut => function (array $params): array { /* ... */ }, ], diff --git a/utils/bg/arrays.texy b/utils/bg/arrays.texy index d73bfc5b6e..c2f5884283 100644 --- a/utils/bg/arrays.texy +++ b/utils/bg/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Връща и премахва стойността на елемент от масива. Ако не съществува, хвърля изключение или връща стойността `$default`, ако е посочена. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/cs/arrays.texy b/utils/cs/arrays.texy index 9351bba355..baa1c82d10 100644 --- a/utils/cs/arrays.texy +++ b/utils/cs/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Vrátí a odstraní hodnotu prvku z pole. Pokud neexistuje, vyhodí výjimku, nebo vrátí hodnotu `$default`, pokud je uvedena. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/de/arrays.texy b/utils/de/arrays.texy index 9167248df2..472f4b1c8e 100644 --- a/utils/de/arrays.texy +++ b/utils/de/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Gibt den Wert eines Elements aus dem Array zurück und entfernt ihn. Wenn er nicht existiert, wird eine Ausnahme geworfen, oder der Wert `$default` zurückgegeben, falls angegeben. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/el/arrays.texy b/utils/el/arrays.texy index 835c2658eb..8fdc7e0860 100644 --- a/utils/el/arrays.texy +++ b/utils/el/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Επιστρέφει και αφαιρεί την τιμή του στοιχείου από τον πίνακα. Αν δεν υπάρχει, ρίχνει μια εξαίρεση, ή επιστρέφει την τιμή `$default`, αν έχει καθοριστεί. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/en/arrays.texy b/utils/en/arrays.texy index a14be68d17..c6abf68903 100644 --- a/utils/en/arrays.texy +++ b/utils/en/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, mixed $default=null): mixed .[method] Returns and removes the value of an item with key `$key` from an array. If the item does not exist, it throws an exception, or returns `$default` if provided. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/es/arrays.texy b/utils/es/arrays.texy index 062f6a39d4..39497de06e 100644 --- a/utils/es/arrays.texy +++ b/utils/es/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Devuelve y elimina el valor de un elemento del array. Si no existe, lanza una excepción, o devuelve el valor `$default` si se especifica. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/fr/arrays.texy b/utils/fr/arrays.texy index a084df7709..140b2e2cc8 100644 --- a/utils/fr/arrays.texy +++ b/utils/fr/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Retourne et supprime la valeur de l'élément du tableau. S'il n'existe pas, lève une exception, ou retourne la valeur `$default` si elle est spécifiée. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/hu/arrays.texy b/utils/hu/arrays.texy index 2a76d0ea3b..a25bf3e74f 100644 --- a/utils/hu/arrays.texy +++ b/utils/hu/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Visszaadja és eltávolítja az elem értékét a tömbből. Ha nem létezik, kivételt dob, vagy visszaadja a `$default` értéket, ha meg van adva. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/it/arrays.texy b/utils/it/arrays.texy index 1d60931cee..c2b6c5ae5c 100644 --- a/utils/it/arrays.texy +++ b/utils/it/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Restituisce e rimuove il valore di un elemento dall'array. Se non esiste, lancia un'eccezione o restituisce il valore `$default`, se specificato. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/ja/arrays.texy b/utils/ja/arrays.texy index adb4a9385c..090e2f6646 100644 --- a/utils/ja/arrays.texy +++ b/utils/ja/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] 配列から要素の値を返して削除します。存在しない場合は例外をスローするか、`$default` が指定されている場合はその値を返します。 ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/pl/arrays.texy b/utils/pl/arrays.texy index 03b7309a65..7273b72ee4 100644 --- a/utils/pl/arrays.texy +++ b/utils/pl/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Zwraca i usuwa wartość elementu z tablicy. Jeśli nie istnieje, rzuca wyjątek lub zwraca wartość `$default`, jeśli jest podana. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/pt/arrays.texy b/utils/pt/arrays.texy index a5081dbad6..bef58ccebb 100644 --- a/utils/pt/arrays.texy +++ b/utils/pt/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Retorna e remove o valor de um elemento do array. Se não existir, lança uma exceção ou retorna o valor `$default`, se fornecido. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/ro/arrays.texy b/utils/ro/arrays.texy index 2f998eb83a..cae59f62dd 100644 --- a/utils/ro/arrays.texy +++ b/utils/ro/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Returnează și elimină valoarea unui element din array. Dacă nu există, aruncă o excepție sau returnează valoarea `$default`, dacă este specificată. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/ru/arrays.texy b/utils/ru/arrays.texy index 4ba819ae5b..dec3f21649 100644 --- a/utils/ru/arrays.texy +++ b/utils/ru/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, mixed $default=null): mixed .[method] Возвращает и удаляет значение элемента из массива. Если он не существует, выбрасывает исключение, или возвращает значение `$default`, если оно указано. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/sl/arrays.texy b/utils/sl/arrays.texy index c9c044f98c..9c82a7842c 100644 --- a/utils/sl/arrays.texy +++ b/utils/sl/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Vrne in odstrani vrednost elementa iz polja. Če ne obstaja, sproži izjemo ali vrne vrednost `$default`, če je podana. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/tr/arrays.texy b/utils/tr/arrays.texy index aa7941b653..e47a08a863 100644 --- a/utils/tr/arrays.texy +++ b/utils/tr/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Bir öğenin değerini diziden döndürür ve kaldırır. Eğer mevcut değilse, istisna fırlatır veya belirtilmişse `$default` değerini döndürür. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' diff --git a/utils/uk/arrays.texy b/utils/uk/arrays.texy index 6d3a4bf31f..2a0e7fc547 100644 --- a/utils/uk/arrays.texy +++ b/utils/uk/arrays.texy @@ -360,8 +360,8 @@ pick(array &$array, string|int $key, ?mixed $default=null): mixed .[method] Повертає та видаляє значення елемента з масиву. Якщо він не існує, викликає виняток або повертає значення `$default`, якщо воно вказане. ```php -$array = [1 => 'foo', null => 'bar']; -$a = Arrays::pick($array, null); +$array = [1 => 'foo', 'x' => 'bar']; +$a = Arrays::pick($array, 'x'); // $a = 'bar' $b = Arrays::pick($array, 'not-exists', 'foobar'); // $b = 'foobar' From 9b06819493d572a05ea7c9c7d06039d51f629512 Mon Sep 17 00:00:00 2001 From: Jan Tojnar <jtojnar@gmail.com> Date: Sun, 28 Sep 2025 01:44:13 +0200 Subject: [PATCH 005/112] typo [Closes #1081] --- latte/bg/type-system.texy | 4 ++-- latte/cs/type-system.texy | 4 ++-- latte/de/type-system.texy | 4 ++-- latte/el/type-system.texy | 4 ++-- latte/en/type-system.texy | 4 ++-- latte/es/type-system.texy | 4 ++-- latte/fr/type-system.texy | 4 ++-- latte/hu/type-system.texy | 4 ++-- latte/it/type-system.texy | 4 ++-- latte/ja/type-system.texy | 4 ++-- latte/pl/type-system.texy | 4 ++-- latte/pt/type-system.texy | 4 ++-- latte/ro/type-system.texy | 4 ++-- latte/ru/type-system.texy | 4 ++-- latte/sl/type-system.texy | 4 ++-- latte/tr/type-system.texy | 4 ++-- latte/uk/type-system.texy | 4 ++-- 17 files changed, 34 insertions(+), 34 deletions(-) diff --git a/latte/bg/type-system.texy b/latte/bg/type-system.texy index 8f052cf074..aa1be010a5 100644 --- a/latte/bg/type-system.texy +++ b/latte/bg/type-system.texy @@ -21,7 +21,7 @@ class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -След това в началото на шаблона поставете тага `{templateType}` с пълното име на класа (включително namespace). Това дефинира, че в шаблона има променливи `$langs` и `$products`, включително съответните типове. Типовете на локалните променливи можете да посочите с помощта на таговете [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Дефиниции]. +След това в началото на шаблона поставете тага `{templateType}` с пълното име на класа (включително namespace). Това дефинира, че в шаблона има променливи `$lang` и `$products`, включително съответните типове. Типовете на локалните променливи можете да посочите с помощта на таговете [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Дефиниции]. От този момент IDE може да ви подсказва правилно. diff --git a/latte/cs/type-system.texy b/latte/cs/type-system.texy index 0454b2743d..7dcf6e8dce 100644 --- a/latte/cs/type-system.texy +++ b/latte/cs/type-system.texy @@ -21,7 +21,7 @@ Jak začít používat typy? Vytvořte si třídu šablony, např. `CatalogTempl class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -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]. +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é `$lang` 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. diff --git a/latte/de/type-system.texy b/latte/de/type-system.texy index 88beabecd4..f8994c6ef0 100644 --- a/latte/de/type-system.texy +++ b/latte/de/type-system.texy @@ -21,7 +21,7 @@ Wie beginnt man mit der Verwendung von Typen? Erstellen Sie eine Template-Klasse class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -Fügen Sie dann am Anfang des Templates das Tag `{templateType}` mit dem vollständigen Klassennamen (einschließlich Namespace) ein. Dies definiert, dass im Template die Variablen `$langs` und `$products` einschließlich der entsprechenden Typen vorhanden sind. Die Typen lokaler Variablen können Sie mit den Tags [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definition] angeben. +Fügen Sie dann am Anfang des Templates das Tag `{templateType}` mit dem vollständigen Klassennamen (einschließlich Namespace) ein. Dies definiert, dass im Template die Variablen `$lang` und `$products` einschließlich der entsprechenden Typen vorhanden sind. Die Typen lokaler Variablen können Sie mit den Tags [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definition] angeben. Von diesem Moment an kann Ihnen die IDE korrekt Vorschläge machen. diff --git a/latte/el/type-system.texy b/latte/el/type-system.texy index 91b4f29239..26dff347f3 100644 --- a/latte/el/type-system.texy +++ b/latte/el/type-system.texy @@ -21,7 +21,7 @@ class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -Και στη συνέχεια, στην αρχή του προτύπου, εισαγάγετε το tag `{templateType}` με το πλήρες όνομα της κλάσης (συμπεριλαμβανομένου του namespace). Αυτό ορίζει ότι στο πρότυπο υπάρχουν οι μεταβλητές `$langs` και `$products` συμπεριλαμβανομένων των αντίστοιχων τύπων τους. Μπορείτε να δηλώσετε τους τύπους των τοπικών μεταβλητών χρησιμοποιώντας τα tags [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Ορισμοί define]. +Και στη συνέχεια, στην αρχή του προτύπου, εισαγάγετε το tag `{templateType}` με το πλήρες όνομα της κλάσης (συμπεριλαμβανομένου του namespace). Αυτό ορίζει ότι στο πρότυπο υπάρχουν οι μεταβλητές `$lang` και `$products` συμπεριλαμβανομένων των αντίστοιχων τύπων τους. Μπορείτε να δηλώσετε τους τύπους των τοπικών μεταβλητών χρησιμοποιώντας τα tags [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Ορισμοί define]. Από εκείνη τη στιγμή, το IDE σας μπορεί να παρέχει σωστή αυτόματη συμπλήρωση. diff --git a/latte/en/type-system.texy b/latte/en/type-system.texy index 8b7dfd642e..eac2758138 100644 --- a/latte/en/type-system.texy +++ b/latte/en/type-system.texy @@ -21,7 +21,7 @@ How to start using types? Create a template class, e.g., `CatalogTemplateParamet class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -Then insert the `{templateType}` tag with the full class name (including the namespace) at the beginning of the template. This defines that the variables `$langs` and `$products` exist in the template, including their respective types. You can also specify the types of local variables using the [`{var}` |tags#var-default], `{varType}`, and [`{define}` |template-inheritance#Definitions] tags. +Then insert the `{templateType}` tag with the full class name (including the namespace) at the beginning of the template. This defines that the variables `$lang` and `$products` exist in the template, including their respective types. You can also specify the types of local variables using the [`{var}` |tags#var-default], `{varType}`, and [`{define}` |template-inheritance#Definitions] tags. From this point on, your IDE can correctly provide autocompletion. diff --git a/latte/es/type-system.texy b/latte/es/type-system.texy index b3b4097727..d1cacbcb12 100644 --- a/latte/es/type-system.texy +++ b/latte/es/type-system.texy @@ -21,7 +21,7 @@ Los tipos declarados son informativos y Latte no los verifica en este momento. class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -Y luego, al principio de la plantilla, inserte la etiqueta `{templateType}` con el nombre completo de la clase (incluido el namespace). Esto define que en la plantilla existen las variables `$langs` y `$products` con sus tipos correspondientes. Puede indicar los tipos de las variables locales usando las etiquetas [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definiciones define]. +Y luego, al principio de la plantilla, inserte la etiqueta `{templateType}` con el nombre completo de la clase (incluido el namespace). Esto define que en la plantilla existen las variables `$lang` y `$products` con sus tipos correspondientes. Puede indicar los tipos de las variables locales usando las etiquetas [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definiciones define]. A partir de ese momento, su IDE puede sugerir correctamente. diff --git a/latte/fr/type-system.texy b/latte/fr/type-system.texy index 8419906504..0cac1c1ac0 100644 --- a/latte/fr/type-system.texy +++ b/latte/fr/type-system.texy @@ -21,7 +21,7 @@ Comment commencer à utiliser les types ? Créez une classe de template, par exe class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -Ensuite, au début du template, insérez la balise `{templateType}` avec le nom complet de la classe (y compris le namespace). Cela définit que les variables `$langs` et `$products` existent dans le template, y compris leurs types respectifs. Vous pouvez spécifier les types des variables locales à l'aide des balises [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Définitions]. +Ensuite, au début du template, insérez la balise `{templateType}` avec le nom complet de la classe (y compris le namespace). Cela définit que les variables `$lang` et `$products` existent dans le template, y compris leurs types respectifs. Vous pouvez spécifier les types des variables locales à l'aide des balises [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Définitions]. À partir de ce moment, l'IDE peut correctement vous faire des suggestions. diff --git a/latte/hu/type-system.texy b/latte/hu/type-system.texy index b2dcceee31..b2e09d3024 100644 --- a/latte/hu/type-system.texy +++ b/latte/hu/type-system.texy @@ -21,7 +21,7 @@ Hogyan kezdjük el használni a típusokat? Hozzon létre egy sablonosztályt, p class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -Ezután a sablon elejére illessze be a `{templateType}` taget az osztály teljes nevével (beleértve a névteret is). Ez definiálja, hogy a sablonban a `$langs` és `$products` változók a megfelelő típusokkal együtt léteznek. A lokális változók típusait a [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definíciók define] tagekkel adhatja meg. +Ezután a sablon elejére illessze be a `{templateType}` taget az osztály teljes nevével (beleértve a névteret is). Ez definiálja, hogy a sablonban a `$lang` és `$products` változók a megfelelő típusokkal együtt léteznek. A lokális változók típusait a [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definíciók define] tagekkel adhatja meg. Ettől kezdve az IDE helyesen tud súgni. diff --git a/latte/it/type-system.texy b/latte/it/type-system.texy index ae98829c21..a5fb971b3c 100644 --- a/latte/it/type-system.texy +++ b/latte/it/type-system.texy @@ -21,7 +21,7 @@ Come iniziare a usare i tipi? Create una classe di template, ad es. `CatalogTemp class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -Quindi, all'inizio del template, inserite il tag `{templateType}` con il nome completo della classe (incluso il namespace). Questo definisce che nel template ci sono le variabili `$langs` e `$products` con i rispettivi tipi. Potete specificare i tipi delle variabili locali usando i tag [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definizioni define]. +Quindi, all'inizio del template, inserite il tag `{templateType}` con il nome completo della classe (incluso il namespace). Questo definisce che nel template ci sono le variabili `$lang` e `$products` con i rispettivi tipi. Potete specificare i tipi delle variabili locali usando i tag [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definizioni define]. Da quel momento, l'IDE può suggerire correttamente. diff --git a/latte/ja/type-system.texy b/latte/ja/type-system.texy index 74a8f15022..0f93ddc900 100644 --- a/latte/ja/type-system.texy +++ b/latte/ja/type-system.texy @@ -21,7 +21,7 @@ class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -次に、テンプレートの先頭に、クラスの完全な名前(名前空間を含む)を持つ `{templateType}` タグを挿入します。これにより、テンプレート内に変数 `$langs` と `$products` が、対応する型とともに定義されます。ローカル変数の型は、[`{var}` |tags#var default]、`{varType}`、[`{define}` |template-inheritance#Definitions] タグを使用して指定できます。 +次に、テンプレートの先頭に、クラスの完全な名前(名前空間を含む)を持つ `{templateType}` タグを挿入します。これにより、テンプレート内に変数 `$lang` と `$products` が、対応する型とともに定義されます。ローカル変数の型は、[`{var}` |tags#var default]、`{varType}`、[`{define}` |template-inheritance#Definitions] タグを使用して指定できます。 その時点から、IDEは正しく補完できるようになります。 diff --git a/latte/pl/type-system.texy b/latte/pl/type-system.texy index 3715e6a9d5..9f19e935d9 100644 --- a/latte/pl/type-system.texy +++ b/latte/pl/type-system.texy @@ -21,7 +21,7 @@ Jak zacząć używać typów? Utwórz klasę szablonu, np. `CatalogTemplateParam class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -A następnie na początku szablonu wstaw tag `{templateType}` z pełną nazwą klasy (włącznie z namespace). To definiuje, że w szablonie są zmienne `$langs` i `$products` wraz z odpowiednimi typami. Typy zmiennych lokalnych możesz podać za pomocą tagów [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definicje define]. +A następnie na początku szablonu wstaw tag `{templateType}` z pełną nazwą klasy (włącznie z namespace). To definiuje, że w szablonie są zmienne `$lang` i `$products` wraz z odpowiednimi typami. Typy zmiennych lokalnych możesz podać za pomocą tagów [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definicje define]. Od tego momentu IDE może poprawnie podpowiadać. diff --git a/latte/pt/type-system.texy b/latte/pt/type-system.texy index 110047b4b4..9baf8bb9f8 100644 --- a/latte/pt/type-system.texy +++ b/latte/pt/type-system.texy @@ -21,7 +21,7 @@ Como começar a usar tipos? Crie uma classe de template, por exemplo, `CatalogTe class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -E, em seguida, no início do template, insira a tag `{templateType}` com o nome completo da classe (incluindo o namespace). Isso define que no template existem as variáveis `$langs` e `$products`, incluindo os tipos correspondentes. Você pode especificar os tipos de variáveis locais usando as tags [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definições]. +E, em seguida, no início do template, insira a tag `{templateType}` com o nome completo da classe (incluindo o namespace). Isso define que no template existem as variáveis `$lang` e `$products`, incluindo os tipos correspondentes. Você pode especificar os tipos de variáveis locais usando as tags [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definições]. A partir desse momento, o IDE pode sugerir corretamente. diff --git a/latte/ro/type-system.texy b/latte/ro/type-system.texy index fc64e5ef84..508abcd9ee 100644 --- a/latte/ro/type-system.texy +++ b/latte/ro/type-system.texy @@ -21,7 +21,7 @@ Cum să începeți să utilizați tipurile? Creați o clasă de șablon, de exem class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -Și apoi, la începutul șablonului, introduceți tag-ul `{templateType}` cu numele complet al clasei (inclusiv namespace). Acest lucru definește că în șablon există variabilele `$langs` și `$products` inclusiv tipurile corespunzătoare. Tipurile variabilelor locale pot fi specificate folosind tag-urile [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definiții define]. +Și apoi, la începutul șablonului, introduceți tag-ul `{templateType}` cu numele complet al clasei (inclusiv namespace). Acest lucru definește că în șablon există variabilele `$lang` și `$products` inclusiv tipurile corespunzătoare. Tipurile variabilelor locale pot fi specificate folosind tag-urile [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definiții define]. Din acel moment, IDE-ul vă poate oferi sugestii corecte. diff --git a/latte/ru/type-system.texy b/latte/ru/type-system.texy index 875667cde2..ab6a5a5d6f 100644 --- a/latte/ru/type-system.texy +++ b/latte/ru/type-system.texy @@ -21,7 +21,7 @@ class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -А затем в начало шаблона вставьте тег `{templateType}` с полным именем класса (включая пространство имен). Это определяет, что в шаблоне есть переменные `$langs` и `$products` с соответствующими типами. Типы локальных переменных можно указать с помощью тегов [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Определения]. +А затем в начало шаблона вставьте тег `{templateType}` с полным именем класса (включая пространство имен). Это определяет, что в шаблоне есть переменные `$lang` и `$products` с соответствующими типами. Типы локальных переменных можно указать с помощью тегов [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Определения]. С этого момента IDE сможет правильно подсказывать. diff --git a/latte/sl/type-system.texy b/latte/sl/type-system.texy index 09f661778b..e814b63fd0 100644 --- a/latte/sl/type-system.texy +++ b/latte/sl/type-system.texy @@ -21,7 +21,7 @@ Kako začeti uporabljati tipe? Ustvarite si razred predloge, npr. `CatalogTempla class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -Nato na začetek predloge vstavite značko `{templateType}` s polnim imenom razreda (vključno z imenskim prostorom). To definira, da so v predlogi spremenljivke `$langs` in `$products` vključno z ustreznimi tipi. Tipe lokalnih spremenljivk lahko navedete s pomočjo značk [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definicije]. +Nato na začetek predloge vstavite značko `{templateType}` s polnim imenom razreda (vključno z imenskim prostorom). To definira, da so v predlogi spremenljivke `$lang` in `$products` vključno z ustreznimi tipi. Tipe lokalnih spremenljivk lahko navedete s pomočjo značk [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Definicije]. Od takrat vam lahko IDE pravilno predlaga. diff --git a/latte/tr/type-system.texy b/latte/tr/type-system.texy index 005ce71f91..eaf4424ee5 100644 --- a/latte/tr/type-system.texy +++ b/latte/tr/type-system.texy @@ -21,7 +21,7 @@ Tipleri kullanmaya nasıl başlanır? İletilen parametreleri, tiplerini ve muht class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -Ve ardından şablonun başına `{templateType}` etiketini sınıfın tam adıyla (ad alanı dahil) ekleyin. Bu, şablonda `$langs` ve `$products` değişkenlerinin ilgili tipleriyle birlikte bulunduğunu tanımlar. Yerel değişkenlerin tiplerini [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Tanımlar] etiketlerini kullanarak belirtebilirsiniz. +Ve ardından şablonun başına `{templateType}` etiketini sınıfın tam adıyla (ad alanı dahil) ekleyin. Bu, şablonda `$lang` ve `$products` değişkenlerinin ilgili tipleriyle birlikte bulunduğunu tanımlar. Yerel değişkenlerin tiplerini [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Tanımlar] etiketlerini kullanarak belirtebilirsiniz. O andan itibaren IDE size doğru önerilerde bulunabilir. diff --git a/latte/uk/type-system.texy b/latte/uk/type-system.texy index 9d6c08535b..14ea265b99 100644 --- a/latte/uk/type-system.texy +++ b/latte/uk/type-system.texy @@ -21,7 +21,7 @@ class CatalogTemplateParameters { public function __construct( - public string $langs, + public string $lang, /** @var ProductEntity[] */ public array $products, public Address $address, @@ -35,7 +35,7 @@ $latte->render('template.latte', new CatalogTemplateParameters( )); ``` -А далі на початку шаблону вставте тег `{templateType}` з повною назвою класу (включаючи простір імен). Це визначає, що в шаблоні є змінні `$langs` та `$products` з відповідними типами. Типи локальних змінних можна вказати за допомогою тегів [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Визначення]. +А далі на початку шаблону вставте тег `{templateType}` з повною назвою класу (включаючи простір імен). Це визначає, що в шаблоні є змінні `$lang` та `$products` з відповідними типами. Типи локальних змінних можна вказати за допомогою тегів [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Визначення]. З цього моменту ваше IDE може правильно підказувати. From df6a0f617dc3e626031d1b0400b233717d0f2ab4 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Wed, 26 Nov 2025 14:24:57 +0100 Subject: [PATCH 006/112] latte 3.1.0 --- application/cs/templates.texy | 2 +- application/en/templates.texy | 2 +- latte/cs/@left-menu.texy | 1 + latte/cs/cookbook/@home.texy | 1 + .../cs/cookbook/migration-from-latte-30.texy | 109 +++++++++++++ latte/cs/custom-filters.texy | 24 --- latte/cs/custom-tags.texy | 2 +- latte/cs/develop.texy | 32 +++- latte/cs/extending-latte.texy | 7 - latte/cs/filters.texy | 41 +++++ latte/cs/html-attributes.texy | 151 ++++++++++++++++++ latte/cs/syntax.texy | 39 ++++- latte/cs/tags.texy | 19 ++- latte/en/@left-menu.texy | 1 + latte/en/cookbook/@home.texy | 1 + .../en/cookbook/migration-from-latte-30.texy | 110 +++++++++++++ latte/en/custom-filters.texy | 24 --- latte/en/custom-tags.texy | 2 +- latte/en/develop.texy | 32 +++- latte/en/extending-latte.texy | 7 - latte/en/filters.texy | 41 +++++ latte/en/html-attributes.texy | 151 ++++++++++++++++++ latte/en/syntax.texy | 39 ++++- latte/en/tags.texy | 21 ++- latte/files/html-attributes.webp | Bin 0 -> 11126 bytes 25 files changed, 772 insertions(+), 87 deletions(-) create mode 100644 latte/cs/cookbook/migration-from-latte-30.texy create mode 100644 latte/cs/html-attributes.texy create mode 100644 latte/en/cookbook/migration-from-latte-30.texy create mode 100644 latte/en/html-attributes.texy create mode 100644 latte/files/html-attributes.webp diff --git a/application/cs/templates.texy b/application/cs/templates.texy index 4ded503e34..feb0346ea4 100644 --- a/application/cs/templates.texy +++ b/application/cs/templates.texy @@ -215,7 +215,7 @@ public function beforeRender(): void // nebo konfigurujeme přímo objekt Latte\Engine $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); + $latte->setFeature(Latte\Feature::MigrationWarnings); } ``` diff --git a/application/en/templates.texy b/application/en/templates.texy index 2950a8f35c..90f641d221 100644 --- a/application/en/templates.texy +++ b/application/en/templates.texy @@ -215,7 +215,7 @@ public function beforeRender(): void // or configure the Latte\Engine object directly $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); + $latte->setFeature(Latte\Feature::MigrationWarnings); } ``` diff --git a/latte/cs/@left-menu.texy b/latte/cs/@left-menu.texy index 31a85bcaf7..4556b26913 100644 --- a/latte/cs/@left-menu.texy +++ b/latte/cs/@left-menu.texy @@ -5,6 +5,7 @@ - [Dědičnost šablon |Template Inheritance] - [Typový systém |type-system] - [Sandbox] + - [HTML attributy |html-attributes] - Pro designéry 🎨 - [Syntaxe |syntax] diff --git a/latte/cs/cookbook/@home.texy b/latte/cs/cookbook/@home.texy index b4d54b0b7e..b6af15e673 100644 --- a/latte/cs/cookbook/@home.texy +++ b/latte/cs/cookbook/@home.texy @@ -8,6 +8,7 @@ Příklady kódů a receptů pro provádění běžných úkolů pomocí Latte. - [Předávání proměnných napříč šablonami |passing-variables] - [Všechno, co jste kdy chtěli vědět o seskupování |grouping] - [Jak psát SQL queries v Latte? |how-to-write-sql-queries-in-latte] +- [Migrace z Latte 3.0 |migration-from-latte-30] - [Migrace z Latte 2 |migration-from-latte2] - [Migrace z PHP |migration-from-php] - [Migrace z Twigu |migration-from-twig] diff --git a/latte/cs/cookbook/migration-from-latte-30.texy b/latte/cs/cookbook/migration-from-latte-30.texy new file mode 100644 index 0000000000..03211a226d --- /dev/null +++ b/latte/cs/cookbook/migration-from-latte-30.texy @@ -0,0 +1,109 @@ +Migrace z Latte 3.0 +******************* + +.[perex] +Latte 3.1 přináší několik vylepšení a změn, díky kterým je psaní šablon bezpečnější a pohodlnější. Většina změn je zpětně kompatibilní, ale některé vyžadují pozornost při přechodu. Tento průvodce shrnuje BC breaky a jak je řešit. + +Latte 3.1 vyžaduje **PHP 8.2** nebo novější. + + +Chytré atributy a migrace +========================= + +Nejvýznamnější změnou v Latte 3.1 je nové chování [chytrých atributů |/html-attributes]. To ovlivňuje, jak se vykreslují hodnoty `null` a logické hodnoty v `data-` atributech. + +1. **Hodnoty `null`:** Dříve se `title={$null}` vykresloval jako `title=""`. Nyní se atribut zcela vynechá. +2. **`data-` atributy:** Dříve se `data-foo={=true}` / `data-foo={=false}` vykreslovaly jako `data-foo="1"` / `data-foo=""`. Nyní se vykreslují jako `data-foo="true"` / `data-foo="false"`. + +Abychom vám pomohli identifikovat místa, kde se výstup ve vaší aplikaci změnil, Latte poskytuje migrační nástroj. + + +Migrační varování +----------------- + +Můžete zapnout [migrační varování |/develop#Migrační varování], která vás během vykreslování upozorní, pokud se výstup liší od Latte 3.0. + +```php +$latte = new Latte\Engine; +$latte->setFeature(Latte\Feature::MigrationWarnings); +``` + +Pokud jsou povolena, sledujte logy aplikace nebo Tracy bar pro `E_USER_WARNING`. Každé varování bude ukazovat na konkrétní řádek a sloupec v šabloně. + +**Jak varování vyřešit:** + +Pokud je nové chování správné (např. chcete, aby prázdný atribut zmizel), potvrďte jej použitím filtru `|accept` pro potlačení varování: + +```latte +<div class="{$var|accept}"></div> +``` + +Pokud chcete atribut zachovat jako prázdný (např. `title=""`) místo jeho vynechání, použijte null coalescing operátor: + +```latte +<div title={$var ?? ''}></div> +``` + +Nebo, pokud striktně vyžadujete staré chování (např. `"1"` pro `true`), explicitně přetypujte hodnotu na string: + +```latte +<div data-foo={(string) $bool}></div> +``` + +**Poté, co vyřešíte všechna varování:** + +Jakmile vyřešíte všechna varování, vypněte migrační varování a **odstraňte všechny** filtry `|accept` ze svých šablon, protože již nejsou potřeba. + + +Strict Types +============ + +Latte 3.1 zapíná `declare(strict_types=1)` ve výchozím nastavení pro všechny kompilované šablony. To zlepšuje typovou bezpečnost, ale může způsobit typové chyby v PHP výrazech uvnitř šablon, pokud jste spoléhali na volné typování. + +Pokud typy nemůžete opravit okamžitě, můžete toto chování vypnout: + +```php +$latte->setFeature(Latte\Feature::StrictTypes, false); +``` + + +Globální konstanty +================== + +Parser šablon byl vylepšen, aby lépe rozlišoval mezi jednoduchými řetězci a konstantami. V důsledku toho musí být globální konstanty nyní prefixovány zpětným lomítkem `\`. + +```latte +{* Starý způsob (vyhodí varování, v budoucnu bude interpretováno jako string 'PHP_VERSION') *} +{if PHP_VERSION > ...} + +{* Nový způsob (správně interpretováno jako konstanta) *} +{if \PHP_VERSION > ...} +``` + +Tato změna předchází nejednoznačnostem a umožňuje volnější používání neuvodzovkovaných řetězců. + + +Odstraněné funkce +================= + +**Rezervované proměnné:** Proměnné začínající na `$__` (dvou podtržítko) a proměnná `$this` jsou nyní vyhrazeny pro vnitřní použití Latte. Nemůžete je používat v šablonách. + +**Undefined-safe operátor:** Operátor `??->`, což byla specifická funkce Latte vytvořená před PHP 8, byl odstraněn. Jde o historický relikt. Používejte prosím standardní PHP nullsafe operátor `?->`. + +**Filter Loader** +Metoda `Engine::addFilterLoader()` byla označena jako zastaralá a odstraněna. Šlo o nekonzistentní koncept, který se jinde v Latte nevyskytoval. + +**Date Format** +Statická vlastnost `Latte\Runtime\Filters::$dateFormat` byla odstraněna, aby se předešlo globálnímu stavu. + + +Nové funkce +=========== + +Během migrace si můžete začít užívat nové funkce: + +- **Chytré HTML atributy:** Předávání polí do `class` a `style`, automatické vynechání `null` atributů. +- **Nullsafe filtry:** Použijte `{$var?|filter}` pro přeskočení filtrování null hodnot. +- **`n:elseif`:** Nyní můžete používat `n:elseif` společně s `n:if` a `n:else`. +- **Zjednodušená syntaxe:** Pište `<div n:if={$cond}>` bez uvozovek. +- **Toggle filtr:** Použijte `|toggle` pro ruční ovládání boolean atributů. diff --git a/latte/cs/custom-filters.texy b/latte/cs/custom-filters.texy index 562d8fd802..7f7cab0c0c 100644 --- a/latte/cs/custom-filters.texy +++ b/latte/cs/custom-filters.texy @@ -117,30 +117,6 @@ $latte->addExtension(new App\Latte\MyLatteExtension); Tento přístup udrží logiku vašeho filtru zapouzdřenou a registraci jednoduchou. -Použití načítače filtrů ------------------------ - -Latte umožňuje registrovat načítač filtrů pomocí `addFilterLoader()`. Jde o jediné volatelné callable, které Latte požádá o jakýkoliv neznámý název filtru během kompilace. Načítač vrací PHP callable filtru nebo `null`. - -```php -$latte = new Latte\Engine; - -// Načítač může dynamicky vytvářet/získávat callable filtry -$latte->addFilterLoader(function (string $name): ?callable { - if ($name === 'myLazyFilter') { - // Představte si zde náročnou inicializaci... - $service = get_some_expensive_service(); - return fn($value) => $service->process($value); - } - return null; -}); -``` - -Tato metoda byla primárně určena pro líné načítání filtrů s velmi **náročnou inicializací**. Avšak moderní praktiky vkládání závislostí (dependency injection) obvykle zvládají líné služby efektivněji. - -Načítače filtrů přidávají složitost a obecně se nedoporučují ve prospěch přímé registrace pomocí `addFilter()` nebo v rámci rozšíření pomocí `getFilters()`. Používejte načítače pouze pokud máte závažný, specifický důvod související s výkonnostními problémy při inicializaci filtrů, které nelze řešit jinak. - - Filtry používající třídu s atributy ----------------------------------- diff --git a/latte/cs/custom-tags.texy b/latte/cs/custom-tags.texy index bb55513f56..7d41e7799d 100644 --- a/latte/cs/custom-tags.texy +++ b/latte/cs/custom-tags.texy @@ -1004,7 +1004,7 @@ Zástupné symboly `PrintContext::format()` - `$argsNode = new ArrayNode([...]);` - `$context->format('myFunc(%args);', $argsNode)` -> `myFunc(1, name: 'Joe');` - **`%line`**: Argument musí být objekt `Position` (obvykle `$this->position`). Vkládá PHP komentář `/* line X */` indikující číslo řádku zdroje. - - `$context->format('echo "Hi" %line;', $this->position)` -> `echo "Hi" /* line 42 */;` + - `$context->format('echo "Hi" %line;', $this->position)` -> `echo "Hi" /* line 42:1 */;` - **`%escape(...)`**: Generuje PHP kód, který *za běhu* escapuje vnitřní výraz pomocí aktuálních kontextově uvědomělých pravidel escapování. - `$context->format('echo %escape(%node);', $variableNode)` - **`%modify(...)`**: Argument musí být `ModifierNode`. Generuje PHP kód, který aplikuje filtry specifikované v `ModifierNode` na vnitřní obsah, včetně kontextově uvědomělého escapování, pokud není zakázáno pomocí `|noescape`. diff --git a/latte/cs/develop.texy b/latte/cs/develop.texy index 273e85ad47..28f0d64892 100644 --- a/latte/cs/develop.texy +++ b/latte/cs/develop.texy @@ -15,7 +15,8 @@ Podporované verze PHP (platí pro poslední setinkové verze Latte): | verze | kompatibilní s PHP |-----------------|------------------- -| Latte 3.0 | PHP 8.0 – 8.2 +| Latte 3.1 | PHP 8.2 – 8.5 +| Latte 3.0 | PHP 8.0 – 8.5 Jak vykreslit šablonu @@ -193,6 +194,27 @@ $latte = new Latte\Engine; $latte->setStrictTypes(); ``` +.[note] +Od verze Latte 3.1 jsou strict types povoleny ve výchozím nastavení. Můžete je deaktivovat pomocí `$latte->setStrictTypes(false)`. + + +Migrační varování .{data-version:3.1} +===================================== + +Latte 3.1 mění chování některých [HTML atributů|html-attributes]. Například hodnoty `null` nyní odstraní atribut namísto vypsání prázdného řetězce. Abyste snadno našli místa, kde tato změna ovlivňuje vaše šablony, můžete zapnout varování o migraci: + +```php +$latte->setFeature(Latte\Feature::MigrationWarnings); +``` + +Pokud je toto zapnuto, Latte kontroluje vykreslované atributy a vyvolá uživatelské varování (`E_USER_WARNING`), pokud se výstup liší od toho, co by Latte 3.0 vygenerovalo. Když narazíte na varování, použijte jedno z těchto řešení: + +1. Pokud je nový výstup pro váš případ použití správný (např. preferujete, aby atribut zmizel při `null`), potlačte varování přidáním filtru `|accept` +2. Pokud chcete, aby byl atribut vykreslen jako prázdný (např. `title=""`) namísto odstranění, když je proměnná `null`, poskytněte prázdný řetězec jako zálohu: `title={$val ?? ''}` +3. Pokud striktně vyžadujete staré chování (např. vypsání `"1"` pro `true` namísto `"true"`), explicitně přetypujte hodnotu na řetězec: `data-foo={(string) $val}` + +Jakmile jsou všechna varování vyřešena, vypněte varování o migraci a **odstraňte všechny** filtry `|accept` ze svých šablon, protože již nejsou potřeba. + Překládání v šablonách .{toc: TranslatorExtension} ================================================== @@ -266,7 +288,9 @@ Jelikož Latte kompiluje šablony do přehledného PHP kódu, můžete je pohodl Linter: validace syntaxe šablon .{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: +Ke kontrole všech šablon slouží nástroj **Linter**. Jeho úkolem je projít zadané soubory a ověřit, že neobsahují syntaktické chyby ani odkazy na neexistující značky, filtry, funkce, třídy apod. + +Linter se spouští z příkazové řádky: ```shell vendor/bin/latte-lint <cesta> @@ -274,7 +298,7 @@ vendor/bin/latte-lint <cesta> Parametrem `--strict` aktivujete [#striktní režim]. -Pokud používáte vlastní značky, vytvořte si také vlastní verzi Linteru, např. `custom-latte-lint`: +Pokud používáte vlastní značky, filtry nebo další rozšíření Latte, je potřeba vytvořit si vlastní variantu Linteru, například `custom-latte-lint`. V té zaregistrujete všechna potřebná rozšíření ještě před samotnou validací šablon: ```php #!/usr/bin/env php @@ -302,6 +326,8 @@ $latte = new Latte\Engine; $linter = new Latte\Tools\Linter(engine: $latte); ``` +Takto přizpůsobený linter pak můžete používat stejným způsobem jako standardní nástroj, ale s plnou znalostí vašich vlastních rozšíření. + Načítání šablon z řetězce ========================= diff --git a/latte/cs/extending-latte.texy b/latte/cs/extending-latte.texy index e155953867..a378ff8cf0 100644 --- a/latte/cs/extending-latte.texy +++ b/latte/cs/extending-latte.texy @@ -44,13 +44,6 @@ $latte->addFilter('truncate', $myTruncate); // Použití v šabloně: {$text|truncate} nebo {$text|truncate:100} ``` -Můžete také zaregistrovat **Filter Loader**, funkci, která dynamicky poskytuje volatelné objekty filtrů podle požadovaného názvu: - -```php -$latte->addFilterLoader(fn(string $name) => /* vrátí volatelný objekt nebo null */); -``` - - Pro registraci funkce použitelné ve výrazech šablony použijte `addFunction()`. ```php diff --git a/latte/cs/filters.texy b/latte/cs/filters.texy index f7159ffd25..bcce55ae3e 100644 --- a/latte/cs/filters.texy +++ b/latte/cs/filters.texy @@ -55,6 +55,11 @@ V šablonách můžeme používat funkce, které pomáhají upravit nebo přefor | `floor` | [zaokrouhlí číslo dolů na danou přesnost |#floor] | `round` | [zaokrouhlí číslo na danou přesnost |#round] +.[table-latte-filters] +|## HTML atributy +| `accept` | [potvrzuje nové chování chytrých atributů |#accept] +| `toggle` | [přepíná přítomnost HTML atributu |#toggle] + .[table-latte-filters] |## Escapování | `escapeUrl` | [escapuje parametr v URL |#escapeUrl] @@ -117,10 +122,31 @@ V šabloně se potom volá takto: ``` +Nullsafe filtry .{data-version:3.1} +----------------------------------- + +Jakýkoliv filtr lze učinit nullsafe použitím `?|` místo `|`. Pokud je hodnota `null`, filtr se nevykoná a vrátí se `null`. Filtry následující v řetězci jsou také přeskočeny. + +To je užitečné v kombinaci s HTML atributy, které jsou vynechány, pokud je hodnota `null`. + +```latte +<div title={$title?|upper}> +{* Pokud je $title null: <div> *} +{* Pokud je $title 'hello': <div title="HELLO"> *} +``` + + Filtry ====== +accept .[filter]{data-version:3.1} +---------------------------------- +Filtr se používá při [migraci z Latte 3.0|cookbook/migration-from-latte-30] k potvrzení, že jste zkontrolovali změnu chování atributu a akceptujete ji. Nemění hodnotu. + +Jde o dočasný nástroj. Jakmile je migrace dokončena a varování při migraci jsou vypnuta, měli byste tento filtr ze svých šablon odstranit. + + batch(int $length, mixed $item): array .[filter] ------------------------------------------------ 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. @@ -827,6 +853,21 @@ Extrahuje část řetězce. Tento filtr byl nahrazen filtrem [#slice]. ``` +toggle .[filter]{data-version:3.1} +---------------------------------- +Filtr `toggle` ovládá přítomnost atributu na základě boolean hodnoty. Pokud je hodnota truthy, atribut je přítomen; pokud je falsy, atribut je zcela vynechán: + +```latte +<div uk-grid={$isGrid|toggle}> +{* Pokud je $isGrid truthy: <div uk-grid> *} +{* Pokud je $isGrid falsy: <div> *} +``` + +Tento filtr je užitečný pro vlastní atributy nebo atributy JavaScriptových knihoven, které vyžadují kontrolu přítomnosti/nepřítomnosti podobně jako HTML boolean atributy. + +Filtr lze použít pouze uvnitř HTML atributů. + + translate(...$args) .[filter] ----------------------------- 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]. diff --git a/latte/cs/html-attributes.texy b/latte/cs/html-attributes.texy new file mode 100644 index 0000000000..e9632b2586 --- /dev/null +++ b/latte/cs/html-attributes.texy @@ -0,0 +1,151 @@ +Chytré HTML atributy +******************** + +.[perex] +Latte 3.1 přichází se sadou vylepšení, která se zaměřuje na jednu z nejčastějších činností v šablonách – vypisování HTML atributů. Přináší více pohodlí, flexibility a bezpečnosti. + + +Boolean atributy +================ + +HTML používá speciální atributy jako `checked`, `disabled`, `selected` nebo `hidden`, u kterých nezáleží na konkrétní hodnotě – pouze na jejich přítomnosti. Fungují jako jednoduché příznaky. + +Latte je zpracovává automaticky. Atributu můžete předat jakýkoliv výraz. Pokud je pravdivý (truthy), atribut se vykreslí. Pokud je nepravdivý (falsey - např. `false`, `null`, `0` nebo prázdný řetězec), atribut se zcela vynechá. + +To znamená, že se můžete rozloučit se složitými podmínkami nebo `n:attr` a jednoduše použít: + +```latte +<input type="text" disabled={$isDisabled} readonly={$isReadOnly}> +``` + +Pokud `$isDisabled` je `false` a `$isReadOnly` je `true`, vykreslí se: + +```latte +<input type="text" readonly> +``` + +Pokud potřebujete přepínací chování pro standardní atributy, které nemají toto automatické zpracování (tedy např. atributy `data-` nebo `aria-`), použijte filtr [toggle |filters#toggle]. + + +Hodnoty null +============ + +Toto je jedna z nejpříjemnějších změn. Dříve, pokud byla proměnná `null`, vypsala se jako prázdný řetězec `""`. To často vedlo k prázdným atributům v HTML jako `class=""` nebo `title=""`. + +V Latte 3.1 platí nové univerzální pravidlo: **Hodnota `null` znamená, že atribut neexistuje.** + +```latte +<div title="{$title}"></div> +``` + +Pokud `$title` je `null`, výstupem je `<div></div>`. Pokud obsahuje řetězec, např. "Ahoj", výstupem je `<div title="Ahoj"></div>`. Díky tomu nemusíte obalovat atributy do podmínek. + +Pokud používáte filtry, mějte na paměti, že obvykle převádějí `null` na řetězec (např. prázdný řetězec). Abyste tomu zabránili, použijte [nullsafe filtr |filters#Nullsafe filtry] `?|`: + +```latte +<div title="{$title?|upper}"></div> +``` + + +Třídy (Classes) +=============== + +Atributu `class` můžete předat pole. To je ideální pro podmíněné třídy: pokud je pole asociativní, klíče se použijí jako názvy tříd a hodnoty jako podmínky. Třída se vykreslí pouze v případě, že je podmínka splněna. + +```latte +<button class={[ + btn, + btn-primary, + active => $isActive, +]}>Stiskni mě</button> +``` + +Pokud je `$isActive` true, vykreslí se: + +```latte +<button class="btn btn-primary active">Stiskni mě</button> +``` + +Toto chování není omezeno pouze na `class`. Funguje pro jakýkoliv HTML atribut, který očekává seznam hodnot oddělených mezerou, jako jsou `itemprop`, `rel`, `sandbox` atd. + +```latte +<a rel={[nofollow, noopener, external => $isExternal]}>odkaz</a> +``` + + +Styly (Styles) +============== + +Atribut `style` také podporuje pole. Je to obzvláště užitečné pro podmíněné styly. Pokud položka pole obsahuje klíč (CSS vlastnost) a hodnotu, vlastnost se vykreslí pouze v případě, že hodnota není `null`. + +```latte +<div style={[ + background => lightblue, + display => $isVisible ? block : null, + font-size => '16px', +]}></div> +``` + +Pokud je `$isVisible` false, vykreslí se: + +```latte +<div style="background: lightblue; font-size: 16px"></div> +``` + + +Data atributy +============= + +Často potřebujeme do HTML předat konfiguraci pro JavaScript. Dříve se to dělalo přes `json_encode`. Nyní můžete atributu `data-` jednoduše předat pole nebo objekt stdClass a Latte jej serializuje do JSONu: + +```latte +<div data-config={[ theme: dark, version: 2 ]}></div> +``` + +Vypíše: + +```latte +<div data-config='{"theme":"dark","version":2}'></div> +``` + +Také `true` a `false` se vykreslují jako řetězce `"true"` a `"false"` (tj. validní JSON). + + +Aria atributy +============= + +Specifikace WAI-ARIA vyžaduje textové hodnoty `"true"` a `"false"` pro logické hodnoty. Latte to pro atributy `aria-` řeší automaticky: + +```latte +<button aria-expanded={=true} aria-checked={=false}></button> +``` + +Vypíše: + +```latte +<button aria-expanded="true" aria-checked="false"></button> +``` + + +Typová kontrola +=============== + +Už jste někdy viděli `<input value="Array">` ve svém vygenerovaném HTML? Je to klasická chyba, která často projde bez povšimnutí. Latte zavádí přísnou typovou kontrolu pro HTML atributy, aby byly vaše šablony vůči takovým přehlédnutím odolnější. + +Latte ví, které atributy jsou které a jaké hodnoty očekávají: + +- **Standardní atributy** (jako `href`, `id`, `value`, `placeholder`...) očekávají hodnotu, kterou lze vykreslit jako text. To zahrnuje řetězce, čísla nebo stringable objekty. Také je akceptováno `null` (atribut vynechá). Pokud však omylem předáte pole, boolean nebo obecný objekt, Latte vyvolá varování a neplatnou hodnotu inteligentně ignoruje. +- **Boolean atributy** (jako `checked`, `disabled`...) akceptují jakýkoliv typ, protože jejich přítomnost je určena logikou pravdivý/nepravdivý. +- **Chytré atributy** (jako `class`, `style`, `data-`...) specificky zpracovávají pole jako validní vstupy. + +Tato kontrola zajišťuje, že vaše aplikace nebude produkovat neočekávané HTML. + + +Migrace z Latte 3.0 +=================== + +Protože se změnilo chování `null` (dříve vypisovalo `""`, nyní atribut vynechá) a atributů `data-` (boolean hodnoty vypisovaly `"1"`/`""`, nyní `"true"`/`"false"`), možná budete muset aktualizovat své šablony. + +Pro hladký přechod poskytuje Latte migrační režim, který upozorňuje na rozdíly. Přečtěte si podrobného průvodce [Migrace z Latte 3.0 na 3.1 |cookbook/migration-from-latte-30]. + +[* html-attributes.webp *] diff --git a/latte/cs/syntax.texy b/latte/cs/syntax.texy index 0913d20fc7..1e21ab6dcf 100644 --- a/latte/cs/syntax.texy +++ b/latte/cs/syntax.texy @@ -111,6 +111,34 @@ Což vypíše v závislosti na proměnné `$url`: 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]. +Kromě syntaxe s uvozovkami `<div n:if="$foo">` můžete použít alternativní syntaxi se složenými závorkami `<div n:if={$foo}>`. Hlavní výhodou je, že uvnitř `{...}` můžete volně používat jednoduché i dvojité uvozovky: + +```latte +<div n:if={str_contains($val, "foo")}> ... </div> +``` + + +Chytré HTML atributy .{data-version:3.1} +======================================== + +Latte dělá práci se standardními HTML atributy neuvěřitelně snadnou. Za vás řeší boolean atributy jako `checked`, odstraňuje atributy obsahující `null` a umožňuje vám skládat hodnoty `class` a `style` pomocí polí. Dokonce automaticky serializuje data pro `data-` atributy do JSON. + +```latte +{* null odstraní atribut *} +<div title={$title}> + +{* boolean ovládá přítomnost boolean atributů *} +<input type="checkbox" checked={$isChecked}> + +{* pole fungují v class *} +<div class={['btn', 'btn-primary', active => $isActive]}> + +{* pole jsou JSON-enkódována v data- atributech *} +<div data-config={[theme: dark, version: 2]}> +``` + +Více informací v samostatné kapitole [Chytré HTML atributy|html-attributes]. + Filtry ====== @@ -148,10 +176,17 @@ Na blok: ``` Nebo přímo na hodnotu (v kombinaci s tagem [`{=expr}` |tags#Vypisování]): + ```latte <h1>{=' Hello world '|trim}<h1> ``` +Pokud může být hodnota `null` a chcete v takovém případě zabránit použití filtru, použijte [nullsafe filter |filters#Nullsafe Filters] `?|`: + +```latte +<h1>{$heading?|upper}</h1> +``` + Dynamické HTML značky .{data-version:3.0.9} =========================================== @@ -204,7 +239,7 @@ Jednoduché řetězce jsou ty, které jsou tvořeny čistě z písmen, číslic, Konstanty --------- -Jelikož lze u jednoduchých řetězců vynechávat uvozovky, doporučujeme pro odlišení zapisovat globální konstanty s lomítkem na začátku: +K rozlišení globálních konstant od jednoduchých řetězců použijte oddělovač globálního jmenného prostoru: ```latte {if \PROJECT_ID === 1} ... {/if} @@ -265,8 +300,6 @@ 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. -Undefined-safe operator `??->`, což je obdoba nullsafe operatoru `?->`, který ale nevyvolá chybu, pokud proměnná neexistuje, vznikl z historických důvodů a dnes doporučujeme používat standardní PHP operátor `?->`. - Omezení PHP v Latte =================== diff --git a/latte/cs/tags.texy b/latte/cs/tags.texy index d302666d5a..598b2b73dd 100644 --- a/latte/cs/tags.texy +++ b/latte/cs/tags.texy @@ -16,7 +16,7 @@ Přehled a popis všech tagů (neboli značek či maker) šablonovacího systém | `{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] -| `n:else` | [alternativní obsah pro podmínky |#n:else] +| `n:else`, `n:elseif` | [alternativní obsah pro podmínky |#n:else] .[table-latte-tags language-latte] |## Cykly @@ -252,14 +252,16 @@ Víte, že k n:atributům můžete připojit prefix `tag-`? Pak se bude podmínk Boží. -`n:else` .{data-version:3.0.11} -------------------------------- +`n:else` `n:elseif` .{data-version:3.0.11} +------------------------------------------ -Pokud podmínku `{if} ... {/if}` zapíšete v podobě [n:attributu |syntax#n:atributy], máte možnost uvést i alternativní větev pomocí `n:else`: +Pokud podmínku `{if} ... {/if}` zapíšete v podobě [n:attributu |syntax#n:atributy], máte možnost uvést i alternativní větev pomocí `n:else` a `n:elseif` (od Latte 3.1): ```latte <strong n:if="$count > 0">Skladem {$count} kusů</strong> +<em n:elseif="$count < 0">Neplatný počet</em> + <em n:else>není dostupné</em> ``` @@ -947,6 +949,9 @@ Pomocníci HTML kodéra n:class ------- +.[note] +Od verze Latte 3.1 získal standardní HTML atribut `class` [stejnou funkcionalitu |html-attributes#třídy-classes]. Není tedy již nutné používat 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`: @@ -997,6 +1002,12 @@ V závislosti na vrácených hodnotách vypíše např.: <input type="checkbox" value="Hello" checked> ``` +Funkce inteligentních atributů v Latte 3.1, jako je vynechání hodnot `null` nebo předávání polí do `class` nebo `style`, fungují také v rámci `n:attr`: + +```latte +<div n:attr="class: [a, b], title: $title"></div> +``` + n:tag ----- diff --git a/latte/en/@left-menu.texy b/latte/en/@left-menu.texy index eebab75253..88fc173a96 100644 --- a/latte/en/@left-menu.texy +++ b/latte/en/@left-menu.texy @@ -5,6 +5,7 @@ - [Template Inheritance] - [Type System] - [Sandbox] + - [HTML attributes] - For Designers 🎨 - [Syntax] diff --git a/latte/en/cookbook/@home.texy b/latte/en/cookbook/@home.texy index 7c2738769a..58b23d4136 100644 --- a/latte/en/cookbook/@home.texy +++ b/latte/en/cookbook/@home.texy @@ -8,6 +8,7 @@ Example codes and recipes for accomplishing common tasks with Latte. - [Passing variables across templates |passing-variables] - [Everything you always wanted to know about grouping |grouping] - [How to write SQL queries in Latte? |how-to-write-sql-queries-in-latte] +- [Migration from Latte 3.0 |migration-from-latte-30] - [Migration from Latte 2 |migration-from-latte2] - [Migration from PHP |migration-from-php] - [Migration from Twig |migration-from-twig] diff --git a/latte/en/cookbook/migration-from-latte-30.texy b/latte/en/cookbook/migration-from-latte-30.texy new file mode 100644 index 0000000000..0c73bfce8f --- /dev/null +++ b/latte/en/cookbook/migration-from-latte-30.texy @@ -0,0 +1,110 @@ +Migration from Latte 3.0 +************************ + +.[perex] +Latte 3.1 brings several improvements and changes that make templates safer and more convenient to write. Most changes are backward compatible, but some require attention during migration. This guide summarizes the breaking changes and how to handle them. + +Latte 3.1 requires **PHP 8.2** or newer. + + +Smart Attributes and Migration +============================== + +The most significant change in Latte 3.1 is the new behavior of [Smart Attributes |/html-attributes]. This affects how `null` values and boolean values in `data-` attributes are rendered. + +1. **`null` values:** Previously, `title={$null}` rendered as `title=""`. Now, the attribute is completely dropped. +2. **`data-` attributes:** Previously, `data-foo={=true}` / `data-foo={=false}` rendered as `data-foo="1"` / `data-foo=""`. Now, it renders as `data-foo="true"` / `data-foo="false"`. + +To help you identify places where the output has changed in your application, Latte provides a migration tool. + + +Migration Warnings +------------------ + +You can enable [migration warnings |/develop#Migration Warnings], which will warn you during rendering if the output differs from Latte 3.0. + +```php +$latte = new Latte\Engine; +$latte->setFeature(Latte\Feature::MigrationWarnings); +``` + +When enabled, check your application logs or Tracy bar for `E_USER_WARNING`s. Each warning will point to the specific line, and column in template. + +**How to resolve warnings:** + +If the new behavior is correct (e.g. you want the empty attribute to disappear), confirm it using the `|accept` filter to suppress the warning: + +```latte +<div class="{$var|accept}"></div> +``` + +If you want to keep the attribute as empty (e.g. `title=""`) instead of dropping it, use the null coalescing operator: + +```latte +<div title={$var ?? ''}></div> +``` + +Or, if you strictly require the old behavior (e.g. `"1"` for `true`), explicitly cast the value to string: + +```latte +<div data-foo={(string) $bool}></div> +``` + +**After you resolve all warnings:** + +Once all warnings are resolved, disable migration warnings and **remove all** `|accept` filters from your templates, as they are no longer needed. + + +Strict Types +============ + +Latte 3.1 enables `declare(strict_types=1)` by default for all compiled templates. This improves type safety but might cause type errors in PHP expressions inside your templates if you were relying on loose typing. + +If you cannot fix the types immediately, you can disable this behavior: + +```php +$latte->setFeature(Latte\Feature::StrictTypes, false); +``` + + +Global Constants +================ + +The template parser has been improved to better distinguish between simple strings and constants. As a result, global constants must now be prefixed with a backslash `\`. + +```latte +{* Old way (throws a warning; in the future will be interpreted as the string 'PHP_VERSION') *} +{if PHP_VERSION > ...} + +{* New way (correctly interpreted as constant) *} +{if \PHP_VERSION > ...} +``` + +This change prevents ambiguity and allows you to use unquoted strings more freely. + + +Removed Features +================ + +**Reserved Variables:** Variables starting with `$__` (double underscore) and the variable `$this` are now strictly reserved for Latte's internal use. You cannot use them in your templates. + +**Undefined-safe Operator:** The `??->` operator, which was a Latte-specific feature created before PHP 8, has been removed. It is a historical relic. Please use the standard PHP nullsafe operator `?->`. + +**Filter Loader** +The `Engine::addFilterLoader()` method has been deprecated and removed. It was an inconsistent concept not found elsewhere in Latte. + +**Date Format** +The static property `Latte\Runtime\Filters::$dateFormat` was removed to avoid global state. + + +New Features +============ + +While migrating, you can start enjoying the new features: + +- **Smart HTML +Attributes:** Pass arrays to `class` and `style`, auto-drop `null` attributes. +- **Nullsafe filters:** Use `{$var?|filter}` to skip filtering null values. +- **`n:elseif`:** You can now use `n:elseif` alongside `n:if` and `n:else`. +- **Simplified syntax:** Write `<div n:if={$cond}>` without quotes. +- **Toggle filter:** Use `|toggle` for manual control over boolean attributes. diff --git a/latte/en/custom-filters.texy b/latte/en/custom-filters.texy index ee38269043..9fc3ea58b6 100644 --- a/latte/en/custom-filters.texy +++ b/latte/en/custom-filters.texy @@ -117,30 +117,6 @@ $latte->addExtension(new App\Latte\MyLatteExtension); This approach keeps your filter logic encapsulated and makes registration straightforward. -Using a Filter Loader ---------------------- - -Latte allows registering a filter loader via `addFilterLoader()`. This is a single callable that Latte asks for any unknown filter name during compilation. The loader returns the filter's PHP callable or `null`. - -```php -$latte = new Latte\Engine; - -// Loader might dynamically create/fetch filter callables -$latte->addFilterLoader(function (string $name): ?callable { - if ($name === 'myLazyFilter') { - // Imagine expensive initialization here... - $service = get_some_expensive_service(); - return fn($value) => $service->process($value); - } - return null; -}); -``` - -This method was primarily intended for lazy loading filters with very **expensive initialization**. However, modern dependency injection practices usually handle lazy services more effectively. - -Filter loaders add complexity and are generally discouraged in favor of direct registration via `addFilter()` or within an Extension using `getFilters()`. Use loaders only if you have a strong, specific reason related to performance bottlenecks in filter initialization that cannot be addressed otherwise. - - Filters Using a Class with Attributes .{toc: Filters Using the Class} --------------------------------------------------------------------- diff --git a/latte/en/custom-tags.texy b/latte/en/custom-tags.texy index ed99d6f014..b427e74a27 100644 --- a/latte/en/custom-tags.texy +++ b/latte/en/custom-tags.texy @@ -1004,7 +1004,7 @@ We've frequently used `PrintContext::format()` to generate PHP code in the `prin - `$argsNode = new ArrayNode([...]);` - `$context->format('myFunc(%args);', $argsNode)` -> `myFunc(1, name: 'Joe');` - **`%line`**: Argument must be a `Position` object (usually `$this->position`). It inserts a PHP comment `/* line X */` indicating the source line number. - - `$context->format('echo "Hi" %line;', $this->position)` -> `echo "Hi" /* line 42 */;` + - `$context->format('echo "Hi" %line;', $this->position)` -> `echo "Hi" /* line 42:1 */;` - **`%escape(...)`**: It generates PHP code that, *at runtime*, will escape the inner expression using the current context-aware escaping rules. - `$context->format('echo %escape(%node);', $variableNode)` - **`%modify(...)`**: Argument must be a `ModifierNode`. It generates PHP code that applies the filters specified in the `ModifierNode` to the inner content, including context-aware escaping if not disabled by `|noescape`. diff --git a/latte/en/develop.texy b/latte/en/develop.texy index a57ec48b11..40262a706f 100644 --- a/latte/en/develop.texy +++ b/latte/en/develop.texy @@ -15,7 +15,8 @@ Supported PHP versions (applies to the latest patch Latte versions): | version | compatible with PHP |-----------------|------------------- -| Latte 3.0 | PHP 8.0 – 8.2 +| Latte 3.1 | PHP 8.2 – 8.5 +| Latte 3.0 | PHP 8.0 – 8.5 How to Render a Template @@ -193,6 +194,27 @@ $latte = new Latte\Engine; $latte->setStrictTypes(); ``` +.[note] +Since Latte 3.1, strict types are enabled by default. You can disable them with `$latte->setStrictTypes(false)`. + + +Migration Warnings .{data-version:3.1} +====================================== + +Latte 3.1 changes the behavior of some [HTML attributes|html-attributes]. For example, `null` values now drop the attribute instead of printing an empty string. To easily find places where this change affects your templates, you can enable migration warnings: + +```php +$latte->setFeature(Latte\Feature::MigrationWarnings); +``` + +When enabled, Latte checks rendered attributes and triggers a user warning (`E_USER_WARNING`) if the output differs from what Latte 3.0 would have produced. When you encounter a warning, apply one of the solutions: + +1. If the new output is correct for your use case (e.g., you prefer the attribute to disappear when `null`), suppress the warning by adding the `|accept` filter +2. If you want the attribute to be rendered as empty (e.g. `title=""`) instead of being dropped when the variable is `null`, provide an empty string as a fallback: `title={$val ?? ''}` +3. If you strictly require the old behavior (e.g., printing `"1"` for `true` instead of `"true"`), explicitly cast the value to a string: `data-foo={(string) $val}` + +Once all warnings are resolved, disable migration warnings and **remove all** `|accept` filters from your templates, as they are no longer needed. + Translation in Templates .{toc: TranslatorExtension} ==================================================== @@ -266,7 +288,9 @@ Since Latte compiles templates into readable PHP code, you can conveniently step Linter: Validating the Template Syntax .{toc: Linter} ===================================================== -The Linter tool will help you go through all templates and check for syntax errors. It is launched from the console: +The **Linter** tool is used to validate all templates. Its purpose is to scan the specified files and ensure that they contain no syntax errors and no references to non-existent tags, filters, functions, classes, or similar constructs. + +The Linter is executed from the command line: ```shell vendor/bin/latte-lint <path> @@ -274,7 +298,7 @@ vendor/bin/latte-lint <path> Use the `--strict` parameter to activate [#strict mode]. -If you use custom tags, also create your customized Linter, e.g. `custom-latte-lint`: +If you use custom tags, filters, or other Latte extensions, you need to create your own variant of the Linter, for example `custom-latte-lint`. In this script, you register all required extensions before the actual template validation takes place: ```php #!/usr/bin/env php @@ -302,6 +326,8 @@ $latte = new Latte\Engine; $linter = new Latte\Tools\Linter(engine: $latte); ``` +The resulting customized linter can then be used in the same way as the standard tool, but with full knowledge of all your custom extensions. + Loading Templates from a String =============================== diff --git a/latte/en/extending-latte.texy b/latte/en/extending-latte.texy index 9f63a3a280..22499f514a 100644 --- a/latte/en/extending-latte.texy +++ b/latte/en/extending-latte.texy @@ -44,13 +44,6 @@ $latte->addFilter('truncate', $myTruncate); // Template usage: {$text|truncate} or {$text|truncate:100} ``` -You can also register a **Filter Loader**, a function that dynamically provides filter callables based on the requested name: - -```php -$latte->addFilterLoader(fn(string $name) => /* return callable or null */); -``` - - Use `addFunction()` to register a function usable within template expressions. ```php diff --git a/latte/en/filters.texy b/latte/en/filters.texy index ef92176edb..c87f8ffceb 100644 --- a/latte/en/filters.texy +++ b/latte/en/filters.texy @@ -55,6 +55,11 @@ In templates, we can use functions that help modify or reformat data into its fi | `floor` | [rounds a number down to a given precision |#floor] | `round` | [rounds a number to a given precision |#round] +.[table-latte-filters] +|## HTML Attributes +| `accept` | [accepts the new behavior of smart attributes |#accept] +| `toggle` | [toggles the presence of an HTML attribute |#toggle] + .[table-latte-filters] |## Escaping | `escapeUrl` | [escapes a parameter in a URL |#escapeUrl] @@ -117,10 +122,31 @@ It is then called in the template like this: ``` +Nullsafe Filters .{data-version:3.1} +------------------------------------ + +Any filter can be made nullsafe by using `?|` instead of `|`. If the value is `null`, the filter is not executed and `null` is returned. Subsequent filters in the chain are also skipped. + +This is useful in combination with HTML attributes, which are omitted if the value is `null`. + +```latte +<div title={$title?|upper}> +{* If $title is null: <div> *} +{* If $title is 'hello': <div title="HELLO"> *} +``` + + Filters ======= +accept .[filter]{data-version:3.1} +---------------------------------- +The filter is used during [migration from Latte 3.0|cookbook/migration-from-latte-30] to acknowledge that you've reviewed the attribute behavior change and accept it. It does not modify the value. + +This is a temporary tool. Once the migration is complete and migration warnings are disabled, you should remove this filter from your templates. + + batch(int $length, mixed $item): array .[filter] ------------------------------------------------ A filter that simplifies listing linear data in a table format. It returns an array of arrays with the specified number of items. If you provide a second parameter, it will be used to fill in missing items in the last row. @@ -827,6 +853,21 @@ Extracts a portion of a string. This filter has been replaced by the [#slice] fi ``` +toggle .[filter]{data-version:3.1} +---------------------------------- +The `toggle` filter controls the presence of an attribute based on a boolean value. If the value is truthy, the attribute is present; if falsy, the attribute is omitted entirely: + +```latte +<div uk-grid={$isGrid|toggle}> +{* If $isGrid is truthy: <div uk-grid> *} +{* If $isGrid is falsy: <div> *} +``` + +This filter is useful for custom attributes or JavaScript library attributes that require presence/absence control similar to HTML boolean attributes. + +The filter can only be used within HTML attributes. + + translate(...$args) .[filter] ----------------------------- Translates expressions into other languages. To make the filter available, you need to [set up the translator |develop#TranslatorExtension]. You can also use the [tags for translation |tags#Translation]. diff --git a/latte/en/html-attributes.texy b/latte/en/html-attributes.texy new file mode 100644 index 0000000000..1107579e99 --- /dev/null +++ b/latte/en/html-attributes.texy @@ -0,0 +1,151 @@ +Smart HTML Attributes +********************* + +.[perex] +Latte 3.1 comes with a set of improvements that focuses on one of the most common activities in templates – printing HTML attributes. It brings more convenience, flexibility and security. + + +Boolean Attributes +================== + +HTML uses special attributes like `checked`, `disabled`, `selected`, or `hidden`, where the specific value is irrelevant—only their presence matters. They act as simple flags. + +Latte handles them automatically. You can pass any expression to the attribute. If it is truthy, the attribute is rendered. If it is falsey (e.g. `false`, `null`, `0`, or an empty string), the attribute is completely omitted. + +This means you can say goodbye to cumbersome macro conditions or `n:attr` and simply use: + +```latte +<input type="text" disabled={$isDisabled} readonly={$isReadOnly}> +``` + +If `$isDisabled` is `false` and `$isReadOnly` is `true`, it renders: + +```latte +<input type="text" readonly> +``` + +If you need this toggling behavior for standard attributes that don't have this automatic handling (like `data-` or `aria-` attributes), use the [toggle |filters#toggle] filter. + + +Null Values +=========== + +This is one of the most pleasant changes. Previously, if a variable was `null`, it printed as an empty string `""`. This often led to empty attributes in HTML like `class=""` or `title=""`. + +In Latte 3.1, a new universal rule applies: **A value of `null` means the attribute does not exist.** + +```latte +<div title="{$title}"></div> +``` + +If `$title` is `null`, the output is `<div></div>`. If it contains a string, e.g. "Hello", the output is `<div title="Hello"></div>`. Thanks to this, you don't have to wrap attributes in conditions. + +If you use filters, keep in mind that they usually convert `null` to a string (e.g. empty string). To prevent this, use the [nullsafe filter |filters#Nullsafe Filters] `?|`: + +```latte +<div title="{$title?|upper}"></div> +``` + + +Classes +======= + +You can pass an array to the `class` attribute. This is perfect for conditional classes: if the array is associative, the keys are used as class names and the values as conditions. The class is rendered only if the condition is true. + +```latte +<button class={[ + btn, + btn-primary, + active => $isActive, +]}>Press me</button> +``` + +If `$isActive` is true, it renders: + +```latte +<button class="btn btn-primary active">Press me</button> +``` + +This behavior is not limited to `class`. It works for any HTML attribute that expects a space-separated list of values, such as `itemprop`, `rel`, `sandbox`, etc. + +```latte +<a rel={[nofollow, noopener, external => $isExternal]}>link</a> +``` + + +Styles +====== + +The `style` attribute also supports arrays. It is especially useful for conditional styles. If an array item contains a key (CSS property) and a value, the property is rendered only if the value is not `null`. + +```latte +<div style={[ + background => lightblue, + display => $isVisible ? block : null, + font-size => '16px', +]}></div> +``` + +If `$isVisible` is false, it renders: + +```latte +<div style="background: lightblue; font-size: 16px"></div> +``` + + +Data Attributes +=============== + +Often we need to pass configuration for JavaScript into HTML. Previously this was done via `json_encode`. Now you can simply pass an array or stdClass object to a `data-` attribute and Latte will serialize it to JSON: + +```latte +<div data-config={[ theme: dark, version: 2 ]}></div> +``` + +Outputs: + +```latte +<div data-config='{"theme":"dark","version":2}'></div> +``` + +Also, `true` and `false` are rendered as strings `"true"` and `"false"` (i.e. valid JSON). + + +Aria Attributes +=============== + +The WAI-ARIA specification requires text values `"true"` and `"false"` for boolean values. Latte handles this automatically for `aria-` attributes: + +```latte +<button aria-expanded={=true} aria-checked={=false}></button> +``` + +Outputs: + +```latte +<button aria-expanded="true" aria-checked="false"></button> +``` + + +Type Checking +============= + +Have you ever seen `<input value="Array">` in your generated HTML? It's a classic bug that often goes unnoticed. Latte introduces strict type checking for HTML attributes to make your templates more resilient against such oversight. + +Latte knows which attributes are which and what values they expect: + +- **Standard attributes** (like `href`, `id`, `value`, `placeholder`...) expect a value that can be rendered as text. This includes strings, numbers, or stringable objects. `null` is also accepted (it drops the attribute). However, if you accidentally pass an array, boolean or a generic object, Latte triggers a warning and intelligently ignores the invalid value. +- **Boolean attributes** (like `checked`, `disabled`...) accept any type, as their presence is determined by truthy/falsey logic. +- **Smart attributes** (like `class`, `style`, `data-`...) specifically handle arrays as valid inputs. + +This check ensures that your application doesn't produce unexpected HTML. + + +Migration from Latte 3.0 +======================== + +Since the behavior of `null` (it used to print `""`, now it drops the attribute) and `data-` attributes (booleans used to print `"1"`/`""`, now `"true"`/`"false"`) has changed, you might need to update your templates. + +For a smooth transition, Latte provides a migration mode that highlights differences. Read the detailed guide [Migration from Latte 3.0 to 3.1|cookbook/migration-from-latte-30]. + +[* html-attributes.webp *] diff --git a/latte/en/syntax.texy b/latte/en/syntax.texy index a65b056772..ef926e75be 100644 --- a/latte/en/syntax.texy +++ b/latte/en/syntax.texy @@ -111,6 +111,34 @@ Which outputs, depending on the variable `$url`: 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] or the very handy [n:href |application:creating-links#In the Presenter Template]. +In addition to the syntax using quotes `<div n:if="$foo">`, you can use alternative syntax with curly braces `<div n:if={$foo}>`. The main advantage is that you can freely use both single and double quotes inside `{...}`: + +```latte +<div n:if={str_contains($val, "foo")}> ... </div> +``` + + +Smart HTML Attributes .{data-version:3.1} +========================================= + +Latte makes working with standard HTML attributes incredibly easy. It handles boolean attributes like `checked` for you, removes attributes containing `null`, and allows you to compose `class` and `style` values using arrays. It even automatically serializes data for `data-` attributes into JSON. + +```latte +{* null removes the attribute *} +<div title={$title}> + +{* boolean controls presence of boolean attributes *} +<input type="checkbox" checked={$isChecked}> + +{* arrays work in class *} +<div class={['btn', 'btn-primary', active => $isActive]}> + +{* arrays are JSON-encoded in data- attributes *} +<div data-config={[theme: dark, version: 2]}> +``` + +Read more in the separate chapter [Smart HTML Attributes|html-attributes]. + Filters ======= @@ -148,10 +176,17 @@ On a block: ``` Or directly on a value (in combination with the [`{=expr}` |tags#Printing] tag): + ```latte <h1>{=' Hello world '|trim}<h1> ``` +If the value can be `null` and you want to avoid applying the filter in that case, use the [nullsafe filter |filters#Nullsafe Filters] `?|`: + +```latte +<h1>{$heading?|upper}</h1> +``` + Dynamic HTML Tags .{data-version:3.0.9} ======================================= @@ -204,7 +239,7 @@ Simple strings are those composed purely of letters, digits, underscores, hyphen Constants --------- -Since quotes can be omitted for simple strings, we recommend writing global constants with a leading slash to distinguish them: +Use the global namespace separator to distinguish global constants from simple strings: ```latte {if \PROJECT_ID === 1} ... {/if} @@ -265,8 +300,6 @@ A Window into History Over its history, Latte introduced several syntactic sugar features that 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 equivalent to today's `...$arr` operator from PHP. -The undefined-safe operator `??->`, which is similar to the nullsafe operator `?->` but does not raise an error if the variable does not exist, was created for historical reasons, and today we recommend using the standard PHP operator `?->`. - PHP Limitations in Latte ======================== diff --git a/latte/en/tags.texy b/latte/en/tags.texy index e9cb9309ae..cfbc94ae6f 100644 --- a/latte/en/tags.texy +++ b/latte/en/tags.texy @@ -16,7 +16,7 @@ An overview and description of all the tags available by default in the Latte te | `{ifset}` … `{elseifset}` … `{/ifset}` | [ifset condition |#ifset elseifset] | `{ifchanged}` … `{/ifchanged}` | [tests if a value has changed |#ifchanged] | `{switch}` `{case}` `{default}` `{/switch}` | [switch condition |#switch case default] -| `n:else` | [alternative content for conditions |#n:else] +| `n:else`, `n:elseif` | [alternative content for conditions |#n:else] .[table-latte-tags language-latte] |## Loops @@ -252,18 +252,20 @@ Did you know you can add the `tag-` prefix to n:attributes? Then the condition w Awesome. -`n:else` .{data-version:3.0.11} -------------------------------- +`n:else` `n:elseif` .{toc: n:else}{data-version:3.0.11} +------------------------------------------------------- -If you write the `{if} ... {/if}` condition in the form of an [n:attribute |syntax#n:attributes], you have the option to specify an alternative branch using `n:else`: +If you write the `{if} ... {/if}` condition in the form of an [n:attribute |syntax#n:attributes], you have the option to specify alternative branches using `n:else` and `n:elseif` (since Latte 3.1): ```latte <strong n:if="$count > 0">In stock {$count} items</strong> +<em n:elseif="$count < 0">Invalid count</em> + <em n:else>not available</em> ``` -The `n:else` attribute can also be used in conjunction with [`n:ifset` |#ifset elseifset], [`n:foreach` |#foreach], [`n:try` |#try], [#`n:ifcontent`], and [`n:ifchanged` |#ifchanged]. +The `n:else` attribute can also be used in conjunction with [`n:ifset` |#ifset elseifset], [`n:foreach` |#foreach], [`n:try` |#try], [`n:ifcontent`|#nifcontent], and [`n:ifchanged` |#ifchanged]. `{/if $cond}` @@ -947,6 +949,9 @@ HTML Coder Helpers `n:class` --------- +.[note] +Since Latte 3.1, the standard HTML class attribute has gained the [same functionality |html-attributes#classes]. So you don't need to use n:class anymore + Thanks to `n:class`, it's very easy to generate the HTML `class` attribute exactly as needed. Example: I need the active element to have the class `active`: @@ -997,6 +1002,12 @@ Depending on the returned values, it prints, for example: <input type="checkbox" value="Hello" checked> ``` +Smart attribute features in Latte 3.1, such as dropping `null` values or passing arrays into `class` or `style`, also work within `n:attr`: + +```latte +<div n:attr="class: [a, b], title: $title"></div> +``` + `n:tag` ------- diff --git a/latte/files/html-attributes.webp b/latte/files/html-attributes.webp new file mode 100644 index 0000000000000000000000000000000000000000..56e729541b6ca3c84e678b115842be201c429e10 GIT binary patch literal 11126 zcmZ9y1ymeOvoO5);=Z^o7GSXi5AF^jxVyVMi@UpfaDoO6!Ciy9OM*i}fFS=q?{mL< z?|;vk(=}CHHq|vf)zhseCoS#B0sv@9iK}X=@+q3Y?%8dJz5>W)9m(WUYTeuSUY({Z zCogm^a5>=~r%r#h`Hav)lf&pP(XUPc>@&B5XPcWm#-auqLrC`f!(9d%y-DWHyYx_$ zP{UNdF1B~@U-*Ad8fr+v!P)CkhdDSU*~!WE*Do~+ow(MurnsMOPyW!=H4=E$IsSsl z82t`QRU;WOsWrp@gSX<B{Yk6qN})FBqokVSD&th%Bg2mifQAPA6v-T8Dq?4s3F-M6 z3jfmQh?OtNbT+rxd!`+eo_}6y{5H=?@XXgIcV6C9)QD2AZaWn`UWUcf$=nD}oy<7| z<z+zDJx{+}TmRjh^Fbq7QW%6o-31qA=w_p4g=sVBQkHrr4<>RDo0I)$r#uNW+>fXH z^qko%n?lEJV^hA_B_Xsrh3$8U(qPVAM7omlnbF!p0|75d6_rmEJ{#WZ#D0xEWrD$Y zodd^K@I9tw<|<-*M^<bSqH3NYdJn@Jx(f;{ujfcaIoEZ|v|I5nDjd$x`vK6<n&zo2 z$ORYY`VDEy2aej{iFEtS&5Ie|L{Vso0rIL@{JUXloFeN?u{-VkIL=4*;CQ-52Wcoe zYDTB89MO;G8-t7dmL!dJ$4%YbBp_nA>aTTKu8;wMcpo)ZR)VZv(>mGqGEuo~3Zh&} zpR1ezTZ*}<*JaT?OY;3^=wLG{ylhjpc@qDyw%O>5g7La^3f=koc3zEDQk7E0kubt$ z(n@<Wo!ra&mWyXUtrLNRN^vCpSEsw}AX)o$*_0#RZNE#xJvw^XdWSa0nuYWbHI?di ziVZAPc+2c!1a+tsd>J{Ze_?hhz;VE`9sZuU5{M}F?RiiXh{5<pBN*3`BN;@|k_^Qx z*%L4(q~7&f-vuE3qQYC}NugV`yHh8rR5XZxc%KZ@e)kO*mxcxxmzK4L_Q$H69t)Kv zF_bhYnO2rM1eFd)Y=#7={V6Ef#ohmvqS0YrcCF_<$-Q8#{`y_d_m<e#`^5jb5>6>S zeIPIUBCzY2w22ZKS-;gOVp90ro|u?;K7D4OdZ0Qucn+6aGD4AObsz*!IM|)%=5rlq z{polPc>Wzn*1I(5N)^-R{I@#g^Creu_4q%WN-&1TLtP0v;MFuc;^wLV{JI#AjIw#I z1L8=zZs(N4W#`HM>co{gqbztJc_!NsFB+duojx8~o&*(lTiy8Z^mMCdo#GbFBul4< zi`tFR^)<3WIG$olA&~VXWd&uYm8xWgO3iP%)RZF@Bb=)Tqpb9-Nt39I3L$5FKAr>; zsQfC5Kf0a!`OmlR(sMi*%T%8aNr!g!7B7Vy&V2vq1z&NnZhhJSk;H7d&wEJ9%~u1R z`k2wq@V0bwO6@!3S@JQd1m-AYs)~CJ5XQxKb!C~VmUBf5TDFc*tL;Obd)Pp-0lova zaYb^VpHLjfG}EU21acNmeXZ|{-6M{Y`M7FF8eR8;7F$+La-n_`NVoN!(xZM&;-j;R zN}4tvPfktUztt&llgcKJy?^s`(U4Y@a7()C6Q!t)+<j(K%_|fWXD{QZKAVVK#Qjbl zJcYad&`mW}4K+%unU-~AjQ|F7TacGtL%9hSpXt!2e#$1-{CZ;>NtBT!*ti0Z>_$gp zcXxE4o4Nk0|Ei_P+EVygn|apu#5&Mhm}JEzz5m_N!DJo?;qbuO)mw9%Ncl|!o~g(B z@o2y&qFkHZTxxoWZJVRtXKAU(!b+b`En0TJFQJb*YR6-B@1rsMh_mPDoA|o8>c4^e z^g<fO?lyWTC$^Cq&<EX-6^vQiRqpsA<LyY%vkN5nEE`A4T~H@?)&2sei3RFqt1`__ zh9^6APDRv~({YCp=l=F){%*@_{Nq?e<n`eJ`<2yIsq23y=>26THe9esi>K1g%*fT@ zTI&VPKdmd?u>#5c<tsi8mws$u1`gM)px{2CSxdrqP9b?Q0^%uxaBMhUt69WA495E7 zb@8NC%Nmn*s-P#$X=Cf_k&0koyw>E@cqJ*pOuY`Wy#z@+kD)L_RTG$F0A2)Mf}G%C z?E~KX?bb-ptN)OGFN~8IQHJXEZ=X}TIuKY{=K5=Wqm_EgYgp0aeTDY<?fn(aaMx~J z>hS6a%h88q{5gs%yWLXNmlj7_Gazv{mSVo&)Afwg0(eK9jfLgA?FdVT)pZQqJ|;MV z4A!Hwh%`(YRI<{n&S!)fC4DhO^^F}24B3bk^6>Yq3%NYezPVS=0M-8~!>{*pkil5c z5TcqMRMkrfy<1ZvQRWqv1g61b!hd*~%P^-YugpR^fj=HI@FzA`F|)!@)qNC|vwcQM zuckSr6D40ft<c;oBI@lFEn=Wu))DI)IQ1sheYzeY^3!EZ_;dYkFV}Be4wHUX>+)ug zoc;ST$ZtM;z+aL}{^e=!)f<10-_&_GM^r}IUW+i`YVeGanIM8hDy$d7;$y0hByk!I zrDa0P=MQ|Qh!NK%(nw7eE(l2nL*fMpAFE8&q?zKxe}R}06)_~xiU>!=Bcu6`W|NM8 zvLlGSW4}Gy+VIB>wkkDFKeB5?3x-!plVZliuGOj{)g?hGWkOYlzx_a4Y1k=|8{zt~ zbeqahWk&$)yqofVAsP3qIuE-b>sgWpIqQIHMS_d-w$d7ncuHJP?2fBmjFY-d4vERq zmV1bp<7;>?qy@l(MwfsbPTWv!#EX7Q&c1X5U$#cn`=I(kxh({zFVM}vc>bza%!g_s zjW&#^-aAX+t+>JBtr&h|Zo}=Kqg<VFI-;$K`d5V9?g|&o@sdwEit-c^eKJ3DWWvZw z+5(u0kQ(%;uRL}g;@XXf+DbKDi2bRD#gh38e2M%};z?JyYUi`a^t-mx0!Ew$JqN`N zET}SZ3G!n_A8%cR>PH?bqX7nuB`$C=lOl<Q*g*p{;%?KLc_>C8inChYki;PGdp$Z+ z_IKuoeCDpK9O=_SFAm1-?70csz<-n>QEB0|7^1aNawiO&y6OX{8p>RvpR^VAMf7pm zwpfGu{*Iiw^!_cKC4<X<5(kwvS^a)HXL7~e@SvlCf}-i=*vBH~H2&gb2y`C|F_5BK z#$B-cZ7wm5Xe1(RBDcR+QLP@l?)ePprs71&yj4BH1S^vugzFUCE}`~SprBM`;QD7r zGR(*fQml64E+!aFtl$!vSzQ&QaZQAZS#Wa{>hc-Yg+=@s_}fX48;xx_!&Tp(Zhe^R z6cpG#dWy31NJU<+P|j<OlKH@V^7q|<6YfX>p2*W22e-_l{kxIXy=H*8{OX9vVR+o$ z#oyOw|H99I{ysQfRG+;Mq}fO=#$3_(RXoz9<9rNluMl-e<-C4i4*&9=olieA<aU5x zM3Yx<ff&qhIMR7sZ2jGZkA0}Q-=*l+=qbFn2-;T@V}&i=o3#5PO?*3sTJqeuKPYJn z4;FfJcN|{!Gw7X97fKEy&(uECl|9c-8$nygo1Ra@?C&Y~De>IX15O(4LGB3q1#k@& z_fy9#gvvPGR0N|&k%f`V7iy&U(9;jgx0Jo53n1<kStq!i?!3hPD}kM)Q=b_WtQP*Z zcj2!@X})GzDD|r9-rh7S*?wR7*x|RT4sm>BCzth{zScY{i|L&5YqeS?46<leUv-#c za~oYp<~CL{KVV%lHy`dZ8eKYVy}5r8wy?es2xOeaoku=le#9|fg|xe=Z@Bf!o;CQ4 z`lqYKbsS3Aqv&U<^LfH@fxJ9p92>-KnatS&`QazzO|2U^Krh6U7b)+AV^&$oKmbDA z=x+Tp#yxKM@W{~Pv;UXbwVcA{g=KiBj<ZF;Z%C^v&^{{2(T&i|-O+7Epj?J2D*j{k zfZ}a$vxDG%kd0{Q!b2Y`s>qI$+?H^W@8fnm5jXq?!`xDep#lpEEB^;g0}<}fG0Ru7 zNxb6GGx7yNc`<vAoFA083%QAh6J9~K0XfgdY*xXIXa0Tx*kq5|E)T+&-1>50|E?Tm z(VgFCO!ya{_P@#a)FNkT-Nr5k)WA&*#O$2BE6Jf(oW(aK0yLA;Dmz#%W=uu{e-#B! zAs#(r7}YhpYxSfFm1_=9>rT9^<63bHnz^DUQ^xcWy`A)FV=|g3P+Oip(IVC(rYYUt zt0+(WCR|K+N0C!xq@jzX`l#BoV9MXQT-~7?hPTJAVnm167Zbw~P(Ez6n2^GnF0yF7 z+qQF($ssl)l_!q}4=CzqA=JY_NwQS37lQ*NUCG+Y;d7`j<ea$9$%n3rN|sYKf{z3b zhP3#Tsi}>9%%O`k!ATwAix41?IFW2LC5pPGGS`}Wj2USa_s(8Hd4+>_{q6F3r>7c0 zc52mwrUU>wN#$oJ34nqljZgKW%;JdwqDjWN@motY69eZfKY;K6oQ~t-`E97o^v_{s z+RWOYC(ZtNl&<FO;X|HfbsC}phE<e)jwn@-oM?^x7^J?BDpxY|ie}xWfJ-rn@ZxMX z60bCl^gKP)NKTUYAYt%lnvjB6J_|q<PvIIlyreoFvz+$iPcAocuE?U%IvxyB-!f|* zr4&FPntF<QE)VsmxI!HYQ=l$4!!r=Xc}T<Bb0a33Kc}0BXflJBLA*ZXY7g#CX+MFW zB83fOTIhfo;c1W*;uhwzBx6Wo4`std6~3;gyyKbt`eib_t3I2+LK1Ib10(@x`(Tf< za4cC{VSDMz5<1b2i(2G*E2xja-R89<>V0sbr$CLkzR(1GCK%LcRF}aKmsYuHcE-A& zJO9Mi7@=ohF3?)4nhh0j9&+XS^E;;039Da*o@3!+A+X1`>cB_${6iI2ixRye{W0il zdg>`pK79$vN?ed(B1H`?s<JFaH#5DGP0ZK)ZL6Acc|d009N1E@%;HR2jWCw}+HLSx zX&7REK|0uDcR7wZIohBmO+!F^(|0!x)9o{>+ity`XBzHX-z~O&yLn*ZCQYdmb<Pn$ zhOymA(w6j(zfVcjWbG%La_2~bE}$`Y8z{4?MP2DUix>a2h%h!dh=)jSY79KRocEFJ zoN+Y}-TqB6^L2f#-!JGBxp;XlrZ5w=kXlaxD~jzRbIo+gY?d|df!Gd5Sc<BLAuG*y z{@Td;6ru5IB-I996y8b9wn>k!dMV$?0WPVdoT%xv;T_&34RMOYG%l<M9c>z^s729$ z3pH8qRc1$fCPbJE=-*5Fe&XdITz@QL_6t|3v*dQDUGSM94+k(qNK^Gw@%~h+$lUUW z8EfINipS3Bnnn-f-;eR*^TSPe{>BPiGIlI0jd%Pd3$*FK#vEu<6iQVix>ce7+Pv@` zWX0^kL@P*R#gcP0VJfHjjOkn&pRz)BZNrCD-J}4qqF0X!RqoArQV3)i^DzWB@#Dd@ zZi+Pk39l}{Kv;S75Lv1}HI6bFb-ziUWTfqM?vE(e`;;eF75BqFB%$kJm#cLMe!&}a z5M|{t!@uV3t7e(|8TZB-+Tu18>+(Vxt;qiBTt_o6aG1k<FU^D^TFY4=DmF<JP?IUz zsNW}JJvNs&nTs=<HFI>LsFA8hWyTUJgVQHO^*1ge4fEwYBM9~;#?O$jPHyW|3Lxq> z{xoS>xb$*m0;<&J`ssiW$)Hi=QhDfmZ>b}xIAqkpL@79TwS;i-5B3Su>fB677tRSh zVtsk{YiBrH)q3&UwMx{(R!AnZF&0ji@<`Y5yI4884kXIK)oBdEHIZ7kIvCre@vHBG zWZiY-P(&h$zY$p}#?*r1-oBZLe#~e&ANxUF;MxwTgC?o+3-0;vroI6(c=7>FrJ^@8 zqK2(Kjt1wRv(s{jpW)Si;E-nU<nC9Q;3}b0*dhdNh%M6W#Bhrgv~dqvu)5C#SEV7F znMXkRIj?4q$#6Tg>Xaqu!{jq3veux0tnwA@DO-$zBE#W@;qID_@|Ehk{qxy*vF;R! zeWDUF8}iNxHG;l<A8rQRZ1TZDBlkw*EqB4s!#?eoXgD>~pGIz_xuk2}F(%Y!r`dWw zYAS}|Bs*v-Repb`Fc>9?tB+9BAtl6)vwS;^`nzgJi%FBBF_b0a8NzL}aMiUi9bC0w zDGVO3{ma^~&hbaWZ?GbXtEP}h)K`)SN20&PrW7+~fywYUY8m5R+^55LDv_byOp!|> z{iFJoZ+<T2NLn}8MbBkPRf!pR<E_K>G_3xHv_glY{)rEFsv2e;sKe@O*S32*eOe9h zZ6fXcN{kM1IS<WdB_sC<Pt{Kx=OM3-+XGizw#-H0TUP5KUjFu(iTwRYyp}yZ@{R1@ zL<%SQ^rldm7u66=|H7`){GRws0D@Ziu}Xm8Q0={@U!mnI_9q|H)n{Yjr%a@|qGA8m zG0@>RE3~~^PtPDbs1wx_&bgoSi*D46)+OE(+Z8T&UvjsKZ)N#ng}c|kB1&&vyN$)l zzn!0!OXiO&t-=SnGoi39dMh-MV35f(QxqBTjj0!=msQx&iJ@DI6ZQi;rl3yX<ovXF zzb4kRulR$tpr<aE+gWoIB$utv`l;b2Bm(8W7Cd2jbnV7!1{NO)!`hiE(>hSn`NBL? zP(NXE_~pttuyCodhlkFQo?9d{-1bt#UWi;Yo44`z(+cQ&fdZ=yTttj7W2bKIcVnK} zIlU9IK{={NlD>F3X7KXK&B2=P5#@FcHb#0S!VzwY!pc}NmE0Su_z`8CW)0GMUR#b> zT4vNN!R~ST8RxI9Z@wvbUkRSo@n+-aq=V84Fo|5-_>7z+-%Q(k?()e_{F*Et)A~_Z zq?<V~uC&`nE_<qOL<ZtOR{eQhcP<7^Fcc@Wnb4-f?Df|vm7Eb60HhtqWO&da;V=}( zr1`dBavD4hKT<~&dMvM+?o%s2&Q1d>IQgaM$PDuID=lD3t|2LhBjCDyye0l$rKpFc z*DI(cbe`P=EQNjN`t9~5McKUTLWMy?eJynKE&3h%Ncv>#KCDV_{S6YBMXZjWZCVXH z9)+sa^)e4oeH!(aHq=CA*wYufY8O4YCo<|kX>?ZInG_az;4lVQTZPSM#er|)*%aB9 z#1ozToV~Lj-{d7C-!x`Rh{TBnx1SJ(E*<Lz?>QHCf5{l);!?+d6-O^SMxo>KPUrZ! zd+MtCAlEHYceFo$DOTy(jKqwTc09h^Y{68c+W}z@QE9>2TZt@?=8GSD1KvK#8S}6W z9%%~z(8}-NLgud0cwA4G@yGuF<Em0mKbw2%u({!yw0NFOBI=&$PaykWboiW}SHAWw zTPQsI=w_-@xT^E0Ej>>c;LP(t6K%CH%dBy|={{Yb^T!s?%Cw;vWTGM^-Q{8^jq)$G zV_2@T(A#lYohv&1FEaors-)~bMx}1P(~zMf^L!J+<fc)Z>Z*M!qCk<e%)>)BW8GbC zq?~$eF57=*f7&XaBDx%%YC!8(<x}eEhuIm}SdmB_kQu!1(4WX~u)g=CEA~&k75!sm zpUKJf$pXc7l@XdsgsnHOvJLZ>ED8RNUXfo0cgHpdMs~Y}w!-2BEFyW>g|D9f#$^7a zGc~pq(B<JR3|@I#X!h1(E)aP-Lt%o)fgB6jnA4~DNrJCni8T%F_h9F9wugKc+{wvv z%92>#?e6XNKdA$AhkSFlb+9-C(<B)p0<xjVY)F>)R-h^zb27!@Pj9#l{rq>hCb_D; z<{c{m*C~f`B@em30T#1}p*H`nCF6I2Sy(+<oq`pJZTe)m!9ky++b6%uEc2vy_ihws z=T~t)uCm$Nm{HzNjt;)*t^IW@`=h`HA*Ze;r{gS0v45M4=(Vhjfz_j|T|b|#b(S6d zI=64G$GReVg*)c*`$92R>%4qkTXj6nmZgHWrNahS0?NN*h$}I=b|V4sbGP-s=Two3 z*A6?Yzt8}VSa_Biu?9FQ+ca8*oSHA*H2qxNICdZ(+&iacG;K6-jTz<XG@mKg<w<Cp zu@;2r1pf7^ayXbxq)WTJXcw`>^=Z?$gG8{`_MKY_b@jw-l<H~@)<}ysko3**w$kGg z(ODmLu*C?zukQ{k49Z;VtZQiHG2?dtTNr-R0vz%iNMN8&e2V|Vq*md*Xw^a)MV7NE zVG<vz-9_<B62haXj5{WsdDfvmF@1e!qct%`=JF?S<<A(B-g^&bQMh1s+vR|#<DGvd zS`~bKA#Kk{P*R7Z${E_LkVM1Jz1?lKtse?Lx5-?^U4Zk?0xYM>gfBJ<I6E~o#*<S? z7MvNhfA@qNF7`~^Pp$-PW(tG#)wDf>y%0NQJFt5jjymuW@0U@XmA}lx4HAh~h(hIX zIo2St@8p;^6Ss*7N&4xFQMo<V^yBb7Jjs{f2DQL29x+^w9+PIsG+jA?AUMv`C3>s} z*g?$r)1I#dA&Q+m`6G325s-MBqXWv2qSj<hlo=<6$VpUcWHnGsA?i=#iiUVE03`Ov zl9gYL|Lren-`_#JDexZa2VhU(AK7bEVx+#ZwBsGgnhk(93XV~99z2|~N^d2QzFZJ} za;P0)7K^$EhaU-^yb$`qIdtyZv~RC|To*vT{XfJ!=!fjgN1NU51r<AyYn|Q-dw0M! z{U{TTu_|JmQ&wgy6VoBGhhXOjv74bO+c0W`I3rc+Itfq9kX9Qk3|iFx^N8YV<Cs_8 zbXjMyX|XOLjk9AT2f_s(F!uHAA@8C0-~$362NS$>t_5z2nxpDW>3{Qej2!44__*%Y z-zVbux|QYQYss2apntV{Yk4vrmKwfgdue_Akp3Ep!?9)SMfv)$9n@n{`;k3`qd(9A zYz#?|&;CAvqA%@u<%bWqPd`8_)2pojswAq*@p&zadibi?4`zoSKC^Ai5?74Rk7b?- zBO#>~5O-4esS|m2kga-aXZb2g-8F&s8+<y`=z$0Fz~zwEiXBoUjF<ZH%Y}&<6V)w@ zt^du0zyUH&d}9YhmA&6TNsg5Bln(L}Y>ZJ4iZ%P`f(enF(o*sidH1+GG;g2bG0L4) z-q`k(@7uWQKia(~jR)Hf;fT<RmEwBTSI6WV53#Uk2*R|qu|Jzu`-{tChIPGYPiN9d zh8VXOWb07}B9^?K-(x6AaZzl->OioxIb%g%h;5)_xr%<bF53@ggC9PyZEQz?MCS`@ zEhs=vlN9bfT-|g^3~P-)MP_7cm~~eH;mm|`hpaUf8)_vSh5e-?MGCy)eK{+<4biOe zKxE>*+A}5s%zOfv(wwJBDBe|tOD0Q|M4TLQ!TEHMJ+QH29VphHhpl?;Hf3%%%9%<m z{YoECp|p3FLt$SYZy3waBHS1EC8X(?Oip`2syEAk71SuiPAGMb#Di;KMWGh$wb*0R zUaD#0N<WC##KlH$V!xvSsv<IB#L*5a0{Y-`9BIZ%xn{13tJ}@2$F<H$Ezu~5ce#VY z1ne`0%jx2TgnVdC4>ND3$Z`#~$rGX;lVpi=W562K!uPz{EMOD+w+)bXwFjxAIJ&s6 z3_TxEpZ0F<VObu5N*pBdnz~PccO4=P`vH9y>yc+%L2ou^{iIpI4(TeP)?)8CZ5suJ z6n>EN=jj&ElW6yw{;9dGwB_Vw@0im+h{MQbdsu8eeinT1uL&otwf=qrn9y_D1vdfq z$4>vaY9=T{D7`Z{(GxCy2Ze-pYp&Gze<SQi;N)qwKMPupd)=#4o8MT<%6l1fi;T#l zd=rvL*HRS)og-^x>-T;Wd;5{pTYWL+p6gfBt2~E*R;*(DHuFczirq0-LSxdDzGBJk z4Y)}DQqrz+8b|5c=S3EFdNx*y^Nfr=XSePFTuMNJ`@6iuXe@t^OMKqmN~MQ+{&7p3 zOL+4G=UCdzw;f2#G7tdDaG=RL1CP?oFNNo4LUXj_{NF;vLoZPZN2l>24Z2}|@6QsB zcW65jo|Qgkk>bge3=Oa{23Z?Y-xZm@v2k*g2m|gHgQ$JX{^VQApDFf%AEm!73lVR2 z@^{Xub7<UZq=rPtn(jr{?bO&+SB!g@tm2kzK5rCL05&7m*mQhVh~Uf&TE>M&P&yjI zsn?|%!t!XXs}cb6&4U#PHKz`-G-2AdI){3Fu*s_-BqIv$r`t`k-pI=*rnHq7v$=qJ zYF0%7t?sG+_(8zoLyNwY%w#;rdjp`<yCe4Fl`#wUA~etl9R03VE`Jg-7og^cakUu! zvlw1u_PKj+m9eF(a2=>2KH8tB_hjbl)lC1IOWy^wOxf($9(4>y7W}}kl0<D+5T12Z zb-%LA6)q5|6n>$8FwCvQq+0RCE1R6A-?tJ`c}uqEo&yQ;C{5W*s}O9A9!v9bUxlW+ zD9<DOwD98AY%se>?`uP`W+G8V7#WqmW?mg8L?#^Sq-jWf7EnogLqWFAVJjn#sYF|y zt9sp7XM-jM1vd3v^CgN7UrI|U`DLzxCAF~^$%omV=%ggkV`a8wL;HW!vhwC*tl4Pu z>A`2_)7L!@?>qJ^i;U1CR+s%W?hXFDw`!?w;)Z)IO(TL5k=yT!Q0%VKbc={zj7;si z0{A#IGOzTp66NLK0q7bO>AC5T50->R3KSj0b1`s!f=X}85`V1laO29c`xH{(Oa-lX zoL5M<YkuKpqrss^bUhtpp~vtWbIMiYQ%Rz6l70|Y^*Gb;7u?_qw@q>nKK{_j9l~%- zDH%nLZ9zVK5MSFTuGOnV@*NEX+Z0Nu53ba1<`+i@Jo{O7g9c#yN_r2}DpA8?Mg%(j zrs-<%P(V9I)f~hixm%@D;Z-Uo-te>+LOP!mioy}29EYaxevec_BExRUEp;<y&ZsRg zZSA-sAibjfpmM4zgfSGo(@n10&-l#<D$~PxAlOGT)?m*pTws?YE?51%wOJR(u7iEF zRu6{;UWgz#w>x<e<8lGiJcjXD+-}UQnd(I(sg+q-;W4^x|JzMJq*3w%y-IfM5NUCl zom>;dh$n)Ykj#E;&?kh;T}isSiT6O=KW>35+&ZLp2bkm2Tuj!c42?kxy$2yi8Dp1= z+WE|;cp=rXkqIlI3PMMg+?GkR$6<CltXrD8J^BOqC@N|cetj1hgw4Q#*Ug3dc%)dg zA8#IA8?_Vrm>p<FuL&zVSV6^aCF_qJ8nqT2`5Jcw#b`3Zu@qQ6?53*kiK}eTngfDY znROLD!lj;i&KEmu;s|mH0xtSGfp@A%$~V&1L+HsoZ~skFvk6LCPd9gVQS~{E-*??H zkC=TSx~pc;M0<~D!)pDz=7k*+;aKwLz2o+>$br#xL36}7?zhe1)L(}&lGde?WUA)) z8d<HDr|~gRg>4<qbRpodxUVsZ$vK~JFf76|v_~j=nlS@p(pQI6pl&UC4Uqm@)d7vX zgZP83meqGKCx)c}iL8Ecb-AVMXk3Jn8c(-XT?<FFrVl*4ti3W#onO1^s&4dl#>k#~ zrhG1%h5q>+oF6P86Jx<UixP{uc187SG4huzq~{=thw#K0qe9LI%UmV~%DJUp)VveG z$-piV15Nf)A42~o-o#ER2)92iW@I!rO=RXOdMBlV7}x(XcsMQ=hTpi}Nqs<!>-iXb z^e^uF|8UQ@Yzf#-{<3hZM6e<aUImF%jlI)Z=v&m;%Ww}c20G`(Je%e+hxC&s@abn5 z1YUZlN>t*f6jA+g;#F)5tR|~~$8<s}FaiGsbNr`sU|FjPeZdS)y7!GBq4n+W^fRh2 zzX82K;_wQ1hj(n9qCy~J#JG_ZG<PXVMxyDMZSj)=PsA$GOE;7xH1|cv7PpM!?GkNI zja;b9e$POWmaSZL1w+}>NKiJXyl~agb?cx8-F^MwN;7K^NnmI6t5uhXwoP<lFBha} zH^`l5NacbT{!rdb_|T8mkW&=hF#3AXKx0E;HX)Q%W_Ewt!rcuf^z5sK@a`ia7{_VO ztEnyX!_*Ik=^cCN#?BHb)=I(5)k}P~FUew_;~s`6DWV(&GBT3MueZ$nn}a&Nd?kPs zs;1L7d+zv=CFL>ks+;QmG(s>|!3H{@IELp!nUO4RMJ^v6@lVskylaWd9d~@jP(+Z0 zMQVp{?m%iE?viU~v;^Q!MX+39VidQKTBVFnUo}iOoHJEMM8pkk<&itS)_EV<L*lDr z%$Ilzt(FRw|FenwzfL^BzU>DIul4#>YfSqiL9jSId2xC53+puC?%s@rgxq|EgS4j? zLf$ZK|80ZnJ&c8@xiO09sM<LmGNQ}N8|n-mZ;Y#eS`vL{(zTT3l5^wDH_>Lt!NR%# z^rjb@&*Ad9HSh1oLM-u!bT4MXP)*P^HbHL)+`!Vn@2y@Hka}u%-Rtl2^d*U(dAt9L z;kn+IJWMUR1U41bgn8tXL*UPiEpQ82ph)bGOu}<2Aa_EeyGxTp_Za}lKP7Sx|Jhtj z$44i9n|Ixe>UG5UnelNfWY4Xv5K_rThEF%iHxDzDiHV@iv8O7cGpo;oVcZub0TXgt z<$xm5V*O4<nkqL3pd;s>Frs%V*buLjnLU|{#aBg<dQ`%moXz-VxC34r%D<rkA^>su z?CZlpphU`lQjr3XRJQoE0qAeFZc>qu4i8i3`{1>cI+6<^Ck!hEgcyXq@RR8<Rg1{l zd;gp$d3i4}`@avIB%I%0qFqk3i_O0%lCWvCvJC}2ExR%^>?0p7i3u<SGd>ah>H2D& z=@L3rVfxl__J!T?%Kw#w*P4Sq)dit%rvI1QDVBu(Fy<1?@>|CSx{eH&P<px6I5LR* zwE2cHx+qLj7xWzo?+Uy#%nIkmWE@$`1M`~2MiKe~4+(zZ=LxM{wtZwJKw$l+2Y-Ve zG*1#<oMT(djiQOj)`|AMA+Pk9vG%@iuQKe8&LZy|XN_C_LoL|&hrIo#gowAjn1r`| zajRl=Uqs>i0`Myt1Gp&p@1e-c-%lyy;+)amh&Mf1oAFH_uf~jiJ%lxGoos!}2{%ni zj`re}|17(OgMLF1+)eGtD}NKAopcF2Kx*vz;L1A~5+*~<{%JElBna;tg^3P|Xg_Td z(cp#j*k(Br9_7Ch7chw{;6Z+%SE>GZ?mq%ioAD`KG$1-bcptGd$FvR1M(SPMQFL3c zQoy};7pj1rVM%Oo)q#K2x2_NW$L{Zrt`CN@2P~Hd@iA_xLs8zM5~HZZ`yFQYmkWSb zSojRgH=wT;8#^`Q?Qk%>kB5YJCy0die#GKG^diRz7+t7lO=xPm-h>5iU-W4ptp19M z5lC7uB@(z{|JIQM2X21~HYNxrt3QN8*<Qxm+K)Qi8nbe8m4-2q!N%RIuu-FiEwmE< z#s%@M!40|}tUvy>_kofA4@WJG1JHm)edMnfRLF`g*-QM}r7#%)6G?MAy=*=dge_@# zoupqM7f+`2<CsKI|0M?liJf3>6GuU>EmK`W{}-yA@HZ|-P?;@{@LTw-&^Zu>23!DO zZS|&gWGrPI-oFb+xH{y~b~rBh;qR~@h(o&Q5lG?8h+u(%RjDNA8>0UM$jmnYFzo*i zFwHl5iQ&w!VFEDlUw!{2_pg@!#0M!KO|>VE4B9oC>4M-)bpO`zDt2N&2-39=Gs%{% z?-nGKNbs`%jdZ0Qv(flnhJk6!N2&_#MUR>N6&+Tv5t;UPoidLDoo!@D?3K_NrUH!& zz=!Raff1fDX*>|`eYueE7gIpUf~FoT5L*4i^g`{2bZsmLr7qM|0@7|VtQ_H<z^IX< zB2w~%jzeknhekB)dKPZnBns(o<5`o$U*9wL4gGJ!N52SET!!LExP}Tpx6z2hcqpF8 zO#1>MT~E1t$EeVUba>zHdkg+!o<guBG4^@zHyY5{K+`;bt?lLhtQM~RgYIx@RQ)+y zGcBxg-TbH99@Y*0T&4bFJhxf+^6Q!auGczSu2fV5C8-1oMx!wuq)QG-Y`c}OzCvrc zD6Wq|G75lQR}6^I=D^F??AD&J6^ZEN>PsyBPYtLW8YBVnCY?~m?|cu^9P?WGPo<gb z%4f(4oSrC_r!JW2NRc#UY<87U+BY+`br1^!{+i9ny@tJh$qi@i4N#fu-##(L&Bger z;}uR~P~hEcPU7*QQxp=d$*NC4@ETZ<Q`zjBdIC+GQZ6RYvOTd2<rKUk;k5HF%ZDQM z^d|VPgt3OJp8_?q3p99@Db`uFLA!qKiv``+*HkZEQek<}#`l!<JMgct*cIj<T*1QN z?;`=(f%#tfBSO3PHA4I9|H0nBB7-5Y^ekcCI>w@|-uovzU+MU3lzQ7$wk9>iZ-WMc zo-#>FpHpqI^34`Deb93FAC5I%*4H$KJjm!QNOi3rj;sxK!XgT<Lbo-BD3V=5Ey4=o z$h;G)S75;Tzrj5i)|E<0cL*^awWK(k9Z+hK(TGzS#)N@d<zdWO8I4I8nEY>|fAVO% z*demqYJfH=>3C_LrTP$~u<W>o-<32hRS9>#Ed(Co8``YF=J<cVv#_>JOdk&L{{ce4 BGL`@U literal 0 HcmV?d00001 From e6012c916b4122b4a6e2e0d0c0b1cf006c14ebc9 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Sat, 29 Nov 2025 20:59:05 +0100 Subject: [PATCH 007/112] application: improved info about attributes, added #[Deprecated] --- application/cs/presenters.texy | 8 ++++++++ application/en/presenters.texy | 8 ++++++++ nette/cs/vulnerability-protection.texy | 13 +------------ nette/en/vulnerability-protection.texy | 13 +------------ 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/application/cs/presenters.texy b/application/cs/presenters.texy index ca1a69f0dd..04cd942e32 100644 --- a/application/cs/presenters.texy +++ b/application/cs/presenters.texy @@ -493,6 +493,14 @@ class MyPresenter extends Nette\Application\UI\Presenter 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. +Označení zastaralých akcí .{data-version:3.2.3} +----------------------------------------------- + +Atribut `#[Deprecated]` slouží k označení akcí, signálů nebo celých presenterů, které jsou zastaralé a měly by být v budoucnu odstraněny. Při generování odkazů na takto označené části aplikace Nette vyhodí varování, které vývojáře upozorní. + +Atribut lze aplikovat jak na celou třídu presenteru, tak na jednotlivé metody `action<Action>()`, `render<View>()` a `handle<Signal>()`. + + Další četba =========== diff --git a/application/en/presenters.texy b/application/en/presenters.texy index daa9cc87c9..7658686bbf 100644 --- a/application/en/presenters.texy +++ b/application/en/presenters.texy @@ -493,6 +493,14 @@ class MyPresenter extends Nette\Application\UI\Presenter 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. +Marking Deprecated Actions .{data-version:3.2.3} +------------------------------------------------ + +The `#[Deprecated]` attribute marks actions, signals, or entire presenters as deprecated and scheduled for future removal. When generating links to deprecated parts of the application, Nette throws a warning to alert developers. + +You can apply the attribute to either the entire presenter class or to individual `action<Action>()`, `render<View>()`, and `handle<Signal>()` methods. + + Further Reading =============== diff --git a/nette/cs/vulnerability-protection.texy b/nette/cs/vulnerability-protection.texy index d5323d3566..b28e24cf5a 100644 --- a/nette/cs/vulnerability-protection.texy +++ b/nette/cs/vulnerability-protection.texy @@ -48,18 +48,7 @@ Nette Framework **automaticky chrání formuláře a signály v presenterech** p $form->allowCrossOrigin(); ``` -nebo v případě signálu přidejte anotaci `@crossOrigin`: - -```php -/** - * @crossOrigin - */ -public function handleXyz() -{ -} -``` - -V Nette Application 3.2 můžete použít také atributy: +nebo v případě signálu přidejte atribut: ```php use Nette\Application\Attributes\Requires; diff --git a/nette/en/vulnerability-protection.texy b/nette/en/vulnerability-protection.texy index 721d493f5b..ceda856ccc 100644 --- a/nette/en/vulnerability-protection.texy +++ b/nette/en/vulnerability-protection.texy @@ -48,18 +48,7 @@ Nette Framework **automatically protects forms and signals in presenters** again $form->allowCrossOrigin(); ``` -or in the case of a signal, add the `@crossOrigin` annotation: - -```php -/** - * @crossOrigin - */ -public function handleXyz() -{ -} -``` - -In Nette Application 3.2, you can also use attributes: +or in the case of a signal, add the attribute: ```php use Nette\Application\Attributes\Requires; From 6e824a022ede5f856db317b089cf01db4d509347 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Sat, 29 Nov 2025 21:41:39 +0100 Subject: [PATCH 008/112] application: improved info about template variables and extensions --- application/cs/templates.texy | 115 +++++++++++++++++++++++--------- application/en/templates.texy | 119 +++++++++++++++++++++++++--------- 2 files changed, 174 insertions(+), 60 deletions(-) diff --git a/application/cs/templates.texy b/application/cs/templates.texy index feb0346ea4..72b2f1e0cf 100644 --- a/application/cs/templates.texy +++ b/application/cs/templates.texy @@ -114,15 +114,34 @@ Soubory, kde se dohledávají šablony layoutu, lze změnit překrytím metody [ Proměnné v šabloně ------------------ -Proměnné do šablony předáváme tak, že je zapíšeme do `$this->template` a potom je máme k dispozici v šabloně jako lokální proměnné: +Proměnné do šablony předáváme zápisem do `$this->template`. V šabloně jsou pak dostupné jako lokální proměnné: ```php $this->template->article = $this->articles->getById($id); ``` -Takto jednoduše můžeme do šablon předat jakékoliv proměnné. Při vývoji robustních aplikací ale bývá užitečnější se omezit. Například tak, že explicitně nadefinujeme výčet proměnných, které šablona očekává, a jejich typů. Díky tomu nám bude moci PHP kontrolovat typy, IDE správně našeptávat a statická analýza odhalovat chyby. -A jak takový výčet nadefinujeme? Jednoduše v podobě třídy a její properties. Pojmenujeme ji podobně jako presenter, jen s `Template` na konci: +Výchozí proměnné +---------------- + +Presentery a komponenty předávají do šablon několik užitečných proměnných automaticky: + +- `$basePath` je absolutní URL cesta ke kořenovému adresáři (např. `/eshop`) +- `$baseUrl` je absolutní URL ke kořenovému adresáři (např. `http://localhost/eshop`) +- `$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()` + +Pokud používáte vlastní třídu šablony, tyto proměnné se předají, pokud pro ně vytvoříte property. + + +Typově bezpečné šablony +----------------------- + +Při vývoji robustních aplikací je užitečné explicitně nadefinovat, jaké proměnné šablona očekává a jakého jsou typu. Získáte tak typovou kontrolu v PHP, chytré našeptávání v IDE a schopnost statické analýzy odhalovat chyby. + +Jak takový výčet nadefinovat? Jednoduše v podobě třídy s properties reprezentujícími proměnné šablony. Pojmenujeme ji podobně jako presenter, jen s `Template` na konci: ```php /** @@ -141,22 +160,22 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -Objekt `$this->template` v presenteru bude nyní instancí třídy `ArticleTemplate`. Takže PHP bude při zápisu kontrolovat deklarované typy. A počínaje verzí PHP 8.2 upozorní i na zápis do neexistující proměnné, v předchozích verzích lze téhož dosáhnout použitím traity [Nette\SmartObject |utils:smartobject]. +Objekt `$this->template` v presenteru bude nyní instancí třídy `ArticleTemplate`. PHP tak bude při zápisu kontrolovat deklarované typy. -Anotace `@property-read` je určená pro IDE a statickou analýzu, díky ní bude fungovat našeptávání, viz "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. +Anotace `@property-read` slouží pro IDE a statickou analýzu, díky ní bude fungovat našeptávání, viz "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. [* 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: +Našeptávání můžete využít i přímo v šablonách. Stačí do PhpStorm nainstalovat plugin pro Latte a uvést na začátek šablony název třídy parametrů šablony, více v článku "Latte: jak na typový systém":https://blog.nette.org/cs/latte-jak-na-typovy-system: ```latte {templateType App\Presentation\Article\ArticleTemplate} ... ``` -Takto fungují i šablony v komponentách, stačí jen dodržet jmennou konvenci a pro komponentu např. `FifteenControl` vytvořit třídu šablony `FifteenTemplate`. +Totéž platí i pro komponenty. Stačí dodržet jmennou konvenci a pro komponentu např. `FifteenControl` vytvořit třídu parametrů `FifteenTemplate`. -Pokud potřebujete vytvořit `$template` jako instanci jiné třídy, využijte metodu `createTemplate()`: +Pokud potřebujete použít jinou třídu parametrů, využijte metodu `createTemplate()`: ```php public function renderDefault(): void @@ -169,21 +188,6 @@ public function renderDefault(): void ``` -Výchozí proměnné ----------------- - -Presentery a komponenty předávají do šablon několik užitečných proměnných automaticky: - -- `$basePath` je absolutní URL cesta ke kořenovému adresáři (např. `/eshop`) -- `$baseUrl` je absolutní URL ke kořenovému adresáři (např. `http://localhost/eshop`) -- `$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()` - -Pokud používáte vlastní třídu šablony, tyto proměnné se předají, pokud pro ně vytvoříte property. - - Vytváření odkazů ---------------- @@ -205,21 +209,67 @@ Více informací najdete v kapitole [Vytváření odkazů URL|creating-links]. Vlastní filtry, značky apod. ---------------------------- -Šablonovací systém Latte lze rozšířit o vlastní filtry, funkce, značky apod. Lze tak učinit přímo v metodě `render<View>` nebo `beforeRender()`: +Šablonovací systém Latte lze rozšířit o vlastní filtry, funkce, značky a další prvky. K dispozici jsou tři způsoby, jak to udělat, od nejrychlejších ad-hoc řešení až po architektonický přístup pro celou aplikaci. + +**Ad-hoc v metodách presenteru** + +Nejrychlejší způsob je přidat filtr nebo funkci přímo v kódu presenteru či komponenty. V presenteru je k tomu vhodná metoda `beforeRender()` nebo `render<View>()`: ```php -public function beforeRender(): void +protected function beforeRender(): void { // přidání filtru - $this->template->addFilter('foo', /* ... */); + $this->template->addFilter('money', fn($val) => round($val) . ' Kč'); + + // přidání funkce + $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); +} +``` - // nebo konfigurujeme přímo objekt Latte\Engine +V šabloně pak: + +```latte +<p>Cena: {$price|money}</p> + +{if isWeekend($now)} ... {/if} +``` + +Pro složitější logiku můžete konfigurovat přímo objekt `Latte\Engine`: + +```php +protected function beforeRender(): void +{ $latte = $this->template->getLatte(); $latte->setFeature(Latte\Feature::MigrationWarnings); } ``` -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: +**Pomocí atributů** + +Elegantní způsob je definovat filtry a funkce jako metody přímo ve [třídě parametrů šablony|#Typově bezpečné šablony] presenteru nebo komponenty a označit je atributy: + +```php +class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template +{ + #[Latte\Attributes\TemplateFilter] + public function money(float $val): string + { + return round($val) . ' Kč'; + } + + #[Latte\Attributes\TemplateFunction] + public function isWeekend(DateTimeInterface $date): bool + { + return $date->format('N') >= 6 + } +} +``` + +Latte automaticky rozpozná a zaregistruje metody označené těmito atributy. Název filtru nebo funkce v šabloně odpovídá názvu metody. Tyto metody nesmí být privátní. + +**Globálně pomocí Extension** + +Předchozí způsoby jsou vhodné pro filtry a funkce, které potřebujete jen v konkrétním presenteru nebo komponentě, nikoliv v celé aplikaci. Pro celou aplikaci je nejvhodnější vytvořit si [extension |latte:extending-latte#Latte Extension]. Jde o třídu, která centralizuje všechna rozšíření Latte pro celý projekt. Kusý příklad: ```php namespace App\Presentation\Accessory; @@ -251,11 +301,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -Zaregistrujeme ji pomocí [konfigurace |configuration#Šablony Latte]: +Extension zaregistrujeme pomocí [konfigurace |configuration#Šablony Latte]: ```neon latte: @@ -263,6 +318,8 @@ latte: - App\Presentation\Accessory\LatteExtension ``` +Výhodou extension je, že lze využít dependency injection, mít přístup k modelové vrstvě aplikace a všechna rozšíření mít přehledně na jednom místě. Extension umožnuje definovat i vlastní značky, providery, průchody pro Latte kompilátor a další. + Překládání ---------- diff --git a/application/en/templates.texy b/application/en/templates.texy index 90f641d221..1c4d0315eb 100644 --- a/application/en/templates.texy +++ b/application/en/templates.texy @@ -111,18 +111,37 @@ Using `$this->setLayout(false)` or the `{layout none}` tag inside the template d 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 -------------------------- +Template Variables +------------------ -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 templates by writing them to `$this->template`. They then become available in the template as local variables: ```php $this->template->article = $this->articles->getById($id); ``` -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 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: +Default Variables +----------------- + +Presenters and components automatically pass several useful variables to templates: + +- `$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` 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. + + +Type-Safe Templates +------------------- + +When developing robust applications, it's useful to explicitly define which variables the template expects and their types. This provides type checking in PHP, smart hints in your IDE, and enables static analysis to catch errors. + +How do you define such a list? Simply as a class with properties representing template variables. Name it similarly to the presenter, just with `Template` at the end: ```php /** @@ -141,22 +160,22 @@ class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template } ``` -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 `$this->template` object in the presenter will now be an instance of the `ArticleTemplate` class. PHP will thus check the declared types when writing. -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. +The `@property-read` annotation is for the IDE and static analysis, enabling code completion, 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 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: +You can also use code completion directly in templates. Just install the Latte plugin for PhpStorm and specify the template parameter class name at the beginning of the template, more in the article "Latte: how to use the type system":https://blog.nette.org/en/latte-how-to-use-type-system: ```latte {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 a component like `FifteenControl`. +The same applies to components. Just follow the naming convention and create a parameter class `FifteenTemplate` for a component like `FifteenControl`. -If you need to create `$template` as an instance of another class, use the `createTemplate()` method: +If you need to use a different parameter class, use the `createTemplate()` method: ```php public function renderDefault(): void @@ -169,21 +188,6 @@ public function renderDefault(): void ``` -Default Variables ------------------ - -Presenters and components automatically pass several useful variables to templates: - -- `$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` 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. - - Creating Links -------------- @@ -205,21 +209,67 @@ More information can be found in the chapter [Creating URL Links|creating-links] Custom Filters, Tags, etc. -------------------------- -The Latte templating system can be extended with custom filters, functions, tags, etc. This can be done directly in the `render<View>` or `beforeRender()` method: +The Latte templating system can be extended with custom filters, functions, tags, and other elements. There are three approaches available, ranging from quick ad-hoc solutions to architectural patterns for entire applications. + +**Ad-hoc in Presenter Methods** + +The quickest approach is adding filters or functions directly in presenter or component code. In presenters, the `beforeRender()` or `render<View>()` methods work well for this: ```php -public function beforeRender(): void +protected function beforeRender(): void { // adding a filter - $this->template->addFilter('foo', /* ... */); + $this->template->addFilter('money', fn($val) => '$' . number_format($val, 2)); + + // adding a function + $this->template->addFunction('isWeekend', fn($date) => $date->format('N') >= 6); +} +``` + +In the template: + +```latte +<p>Price: {$price|money}</p> + +{if isWeekend($now)} ... {/if} +``` - // or configure the Latte\Engine object directly +For more complex logic, you can configure the `Latte\Engine` object directly: + +```php +protected function beforeRender(): void +{ $latte = $this->template->getLatte(); $latte->setFeature(Latte\Feature::MigrationWarnings); } ``` -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: +**Using Attributes** + +A more elegant approach is defining filters and functions as methods directly in the presenter or component's [template parameter class|#Type-safe templates], marked with attributes: + +```php +class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template +{ + #[Latte\Attributes\TemplateFilter] + public function money(float $val): string + { + return '$' . number_format($val, 2); + } + + #[Latte\Attributes\TemplateFunction] + public function isWeekend(DateTimeInterface $date): bool + { + return $date->format('N') >= 6 + } +} +``` + +Latte automatically discovers and registers methods marked with these attributes. The filter or function name in templates matches the method name. These methods must not be private. + +**Globally Using Extensions** + +The previous approaches suit filters and functions needed only in specific presenters or components, not application-wide. For the entire application, creating an [extension |latte:extending-latte#Latte Extension] works best. This class centralizes all Latte extensions for your project. A brief example: ```php namespace App\Presentation\Accessory; @@ -251,11 +301,16 @@ final class LatteExtension extends Latte\Extension ]; } + private function filterTimeAgoInWords(DateTimeInterface $time): string + { + // ... + } + // ... } ``` -We register it using [configuration |configuration#Latte Templates]: +Register the extension through [configuration |configuration#Latte Templates]: ```neon latte: @@ -263,6 +318,8 @@ latte: - App\Presentation\Accessory\LatteExtension ``` +Extensions offer several advantages: dependency injection support, access to your application's model layer, and centralized management of all extensions. They also support custom tags, providers, compiler passes, and more. + Translating ----------- From a3c532aec1f425b0e8f1295738dd55e3086734c3 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Mon, 1 Dec 2025 19:05:14 +0100 Subject: [PATCH 009/112] nette/utils 4.0.10 --- utils/cs/datetime.texy | 14 +++++++++++ utils/cs/helpers.texy | 11 +++++++++ utils/cs/iterables.texy | 21 +++++++++++++++++ utils/cs/type.texy | 51 ++++++++++++++++++++++++++++++++++++++--- utils/en/datetime.texy | 14 +++++++++++ utils/en/helpers.texy | 11 +++++++++ utils/en/iterables.texy | 21 +++++++++++++++++ utils/en/type.texy | 51 ++++++++++++++++++++++++++++++++++++++--- 8 files changed, 188 insertions(+), 6 deletions(-) diff --git a/utils/cs/datetime.texy b/utils/cs/datetime.texy index 6b2788d3e1..f685af7909 100644 --- a/utils/cs/datetime.texy +++ b/utils/cs/datetime.texy @@ -4,6 +4,9 @@ Datum a čas .[perex] [api:Nette\Utils\DateTime] je třída, která rozšiřuje nativní [php:DateTime] o další funkce. +Oproti svému předchůdci je striktní. Zatímco PHP **tiše akceptuje** nesmyslná data jako `0000-00-00` (převede na `-0001-11-30`) nebo `2024-02-31` (převede na `2024-03-02`), `Nette\Utils\DateTime` v takových případech vyhodí výjimku. + +Zároveň **opravuje chování** při přechodu na letní/zimní čas, kdy v nativním PHP může přičtení relativního času (např. `+100 minutes`) "vést k dřívějšímu výslednému času":https://phpfashion.com/cs/100-minut-je-mene-nez-50-paradoxy-php-pri-zmene-casu než přičtení kratšího úseku (např. `+50 minutes`). Nette zajišťuje, že aritmetika funguje intuitivně a `+100 minutes` je vždy více než `+50 minutes`. Instalace: @@ -46,6 +49,17 @@ DateTime::createFromFormat('d.m.Y', '26.02.1994', 'Europe/London'); ``` +static relativeToSeconds(string $str): int .[method]{data-version:4.0.7} +------------------------------------------------------------------------ +Převede relativní časový údaj na sekundy. Hodí se pro převod časů jako `5 minutes` nebo `2 hours` na číselnou hodnotu. + +```php +DateTime::relativeToSeconds('1 minute'); // 60 +DateTime::relativeToSeconds('10 minutes'); // 600 +DateTime::relativeToSeconds('-1 hour'); // -3600 +``` + + modifyClone(string $modify=''): static .[method] ------------------------------------------------ Vytvoří kopii s upraveným časem. diff --git a/utils/cs/helpers.texy b/utils/cs/helpers.texy index 2b5bcbe27e..49e37ebaed 100644 --- a/utils/cs/helpers.texy +++ b/utils/cs/helpers.texy @@ -84,3 +84,14 @@ Helpers::getSuggestion($items, 'fo'); // 'foo' Helpers::getSuggestion($items, 'barr'); // 'bar' Helpers::getSuggestion($items, 'baz'); // 'bar', ne 'baz' ``` + + +splitClassName(string $name): array .[method]{data-version:4.0.10} +------------------------------------------------------------------ + +Rozdělí celý název třídy v PHP na jmenný prostor a zkrácený název třídy. Vrací pole dvou řetězců, kde první prvek je namespace a druhý název třídy. + +```php +Helpers::splitClassName('Nette\Utils\Helpers'); // ['Nette\Utils', 'Helpers'] +Helpers::splitClassName('Foo'); // ['', 'Foo'] +``` diff --git a/utils/cs/iterables.texy b/utils/cs/iterables.texy index 77af7696df..ebf73344b7 100644 --- a/utils/cs/iterables.texy +++ b/utils/cs/iterables.texy @@ -140,6 +140,27 @@ $memoized = Iterables::memoize($iterator); Tato metoda je užitečná v situacích, kdy potřebujete vícekrát projít stejnou sadu dat, ale původní iterátor neumožňuje opakovanou iteraci nebo by opakované procházení bylo nákladné (např. při čtení dat z databáze nebo souboru). +repeatable(callable $factory): IteratorAggregate .[method]{data-version:4.0.10} +------------------------------------------------------------------------------- + +Umožňuje opakovanou iteraci objektů, které to běžně nedovolují, typicky [PHP generátorů |https://www.php.net/manual/en/language.generators.overview.php]. Metoda `repeatable()` tento problém elegantně řeší: místo samotného iterátoru jí předáte funkci, která iterátor vytváří. Tato továrna je pak zavolána automaticky při každém novém průchodu cyklem. + +```php +// Běžný generátor, který nelze projít dvakrát +$generator = function () { + yield 'A'; + yield 'B'; +}; + +$iterator = Iterables::repeatable($generator); + +foreach ($iterator as $v) echo $v; // Vypíše: AB +foreach ($iterator as $v) echo $v; // Vypíše: AB (generátor se spustil znovu) +``` + +Tato metoda je alternativou k [#memoize()] v situacích, kdy pracujete s **velkým objemem dat**, jelikož `repeatable()` si data neukládá, ale při každém průchodu je generuje znovu. + + some(iterable $iterable, callable $predicate): bool .[method] ------------------------------------------------------------- diff --git a/utils/cs/type.texy b/utils/cs/type.texy index d4c539ee67..f2af19e5f8 100644 --- a/utils/cs/type.texy +++ b/utils/cs/type.texy @@ -2,8 +2,13 @@ PHP Typ ******* .[perex] -[api:Nette\Utils\Type] je třída pro práci s datovými typy PHP. +[api:Nette\Utils\Type] reprezentuje datový typ PHP. Slouží k analýze, porovnávání a manipulaci s typy, ať už pocházejí z řetězce nebo z reflexe. +PHP má dnes velmi bohatý typový systém: od skalárních typů (`int`, `string`) přes objekty a rozhraní až po složené typy (union `A|B`, intersection `A&B` nebo disjunktivní normální formy `(A&B)|D`). Navíc existují speciální typy jako `void`, `never`, `mixed` nebo relativní `self` či `static`. + +Práce s těmito typy nativně, zejména přes `ReflectionType`, je často zdlouhavá, protože musíte rekurzivně rozlišovat mezi `ReflectionNamedType`, `ReflectionUnionType` a dalšími objekty. Třída `Nette\Utils\Type` toto vše zapouzdřuje a poskytuje **jednotné a srozumitelné API** pro práci s jakýmkoliv typem, který PHP podporuje. + +Umožňuje například snadno zjistit, zda jeden typ [akceptuje|#allows] druhý (kompatibilita), [rozšiřovat typy|#with] nebo převádět reflexe na čitelný zápis. Instalace: @@ -45,6 +50,25 @@ echo $type; // 'Foo|Bar' ``` +fromValue(mixed $value): Type .[method]{data-version:4.0.10} +------------------------------------------------------------ + +Statická metoda, která vytvoří objekt Type podle typu předané hodnoty. + +```php +$type = Type::fromValue('hello'); // 'string' +$type = Type::fromValue(123); // 'int' +$type = Type::fromValue(new stdClass); // 'stdClass' +``` + +Pro resources vrací `mixed`, protože PHP typ `resource` nezná. U anonymních tříd vrací nejbližšího předka nebo typ `object`. + +```php +$obj = new class extends Foo { }; +$type = Type::fromValue($obj); // 'Foo' +``` + + getNames(): (string|array)[] .[method] -------------------------------------- @@ -183,8 +207,8 @@ $type->isClassKeyword(); // false ``` -allows(string $type): bool .[method] ------------------------------------- +allows(string|Type $type): bool .[method] +----------------------------------------- Metoda `allows()` ověřuje kompatibilitu typů. Například umožní zjistit, jestli hodnota určitého typu by mohla být předaná jako parametr. @@ -197,3 +221,24 @@ $type->allows('Foo'); // false $type = Type::fromString('mixed'); $type->allows('null'); // true ``` + + +with(string|Type $type): Type .[method]{data-version:4.0.10} +------------------------------------------------------------ + +Vrací objekt Type, který akceptuje jak původní typ, tak i nově přidaný. Vytváří tzv. union type. + +Metoda je chytrá a typy zbytečně nezdvojuje. Pokud přidáte typ, který je již obsažen, nebo je nadmnožinou stávajícího typu (např. přidání `mixed` k `string`), vrátí se zjednodušený výsledek. + +```php +$type = Type::fromString('string'); + +// Rozšíření na nullable string +echo $type->with('null'); // '?string' + +// Vytvoření union typu +echo $type->with('int'); // 'string|int' + +// Přidání typu, který "přebije" vše ostatní +echo $type->with('mixed'); // 'mixed' +``` diff --git a/utils/en/datetime.texy b/utils/en/datetime.texy index 3fc7cdf0ac..5b00d789d3 100644 --- a/utils/en/datetime.texy +++ b/utils/en/datetime.texy @@ -4,6 +4,9 @@ Date and Time .[perex] [api:Nette\Utils\DateTime] is a class that extends the native [php:DateTime] with additional useful features. +Compared to the native class, it is strict. While PHP **silently accepts** invalid dates like `0000-00-00` (converts to `-0001-11-30`) or `2024-02-31` (converts to `2024-03-02`), `Nette\Utils\DateTime` throws an exception in such cases. + +It also **fixes the behavior** during Daylight Saving Time (DST) transitions, where in native PHP adding a relative time (e.g., `+100 minutes`) can "result in an earlier time":https://phpfashion.com/en/100-minutes-is-less-than-50-php-paradoxes-during-time-changes than adding a shorter period (e.g., `+50 minutes`). Nette ensures that arithmetic works intuitively and `+100 minutes` is always more than `+50 minutes`. Installation: @@ -46,6 +49,17 @@ DateTime::createFromFormat('d.m.Y', '26.02.1994', 'Europe/London'); // create wi ``` +static relativeToSeconds(string $str): int .[method]{data-version:4.0.7} +------------------------------------------------------------------------ +Converts a relative time string to seconds. It is useful for converting times like `5 minutes` or `2 hours` to a numeric value. + +```php +DateTime::relativeToSeconds('1 minute'); // 60 +DateTime::relativeToSeconds('10 minutes'); // 600 +DateTime::relativeToSeconds('-1 hour'); // -3600 +``` + + modifyClone(string $modify=''): static .[method] ------------------------------------------------ Creates a copy with a modified time. diff --git a/utils/en/helpers.texy b/utils/en/helpers.texy index 280cdb8e4c..4c69556b64 100644 --- a/utils/en/helpers.texy +++ b/utils/en/helpers.texy @@ -84,3 +84,14 @@ Helpers::getSuggestion($items, 'fo'); // 'foo' Helpers::getSuggestion($items, 'barr'); // 'bar' Helpers::getSuggestion($items, 'baz'); // 'bar', not 'baz' ``` + + +splitClassName(string $name): array .[method]{data-version:4.0.10} +------------------------------------------------------------------ + +Splits a PHP class name into a namespace and a short class name. Returns an array of two strings where the first is the namespace and the second is the class name. + +```php +Helpers::splitClassName('Nette\Utils\Helpers'); // ['Nette\Utils', 'Helpers'] +Helpers::splitClassName('Foo'); // ['', 'Foo'] +``` diff --git a/utils/en/iterables.texy b/utils/en/iterables.texy index a5f06f8be0..56c86f60d7 100644 --- a/utils/en/iterables.texy +++ b/utils/en/iterables.texy @@ -140,6 +140,27 @@ $memoized = Iterables::memoize($iterator); This method is useful in situations where you need to iterate over the same dataset multiple times, but the original iterator doesn't allow repeated iteration, or re-traversing would be costly (e.g., reading data from a database or file). +repeatable(callable $factory): IteratorAggregate .[method]{data-version:4.0.10} +------------------------------------------------------------------------------- + +Allows repeated iteration of objects that otherwise do not support it, typically [PHP generators |https://www.php.net/manual/en/language.generators.overview.php]. The `repeatable()` method solves this problem elegantly: instead of passing the iterator itself, you pass a function that creates it. This factory is then called automatically during each iteration loop. + +```php +// A standard generator that cannot be iterated twice +$generator = function () { + yield 'A'; + yield 'B'; +}; + +$iterator = Iterables::repeatable($generator); + +foreach ($iterator as $v) echo $v; // Prints: AB +foreach ($iterator as $v) echo $v; // Prints: AB (generator ran again) +``` + +This method is an alternative to [#memoize()] in situations where you are working with **large amounts of data**, since `repeatable()` does not cache data, but generates it again during each iteration. + + some(iterable $iterable, callable $predicate): bool .[method] ------------------------------------------------------------- diff --git a/utils/en/type.texy b/utils/en/type.texy index 08886b2e91..f462bbb31c 100644 --- a/utils/en/type.texy +++ b/utils/en/type.texy @@ -2,8 +2,13 @@ PHP Type ******** .[perex] -[api:Nette\Utils\Type] is a class for working with PHP data types. +[api:Nette\Utils\Type] represents a PHP data type. It is used for analyzing, comparing, and manipulating types, whether obtained from a string or reflection. +PHP currently has a very rich type system: from scalar types (`int`, `string`), through objects and interfaces, to complex types (union `A|B`, intersection `A&B`, or disjunctive normal forms `(A&B)|D`). Additionally, there are special types like `void`, `never`, `mixed`, or relative types `self` and `static`. + +Working with these types natively, especially via `ReflectionType`, is often cumbersome because you must recursively distinguish between `ReflectionNamedType`, `ReflectionUnionType`, and other objects. The `Nette\Utils\Type` class encapsulates all of this and provides a **unified and intuitive API** for working with any type supported by PHP. + +For example, it allows you to easily check if one type [accepts|#allows] another (compatibility), [extend types|#with], or convert reflections into a readable notation. Installation: @@ -45,6 +50,25 @@ echo $type; // 'Foo|Bar' ``` +fromValue(mixed $value): Type .[method]{data-version:4.0.10} +------------------------------------------------------------ + +Static method that creates a Type object based on the type of the passed value. + +```php +$type = Type::fromValue('hello'); // 'string' +$type = Type::fromValue(123); // 'int' +$type = Type::fromValue(new stdClass); // 'stdClass' +``` + +For resources, it returns `mixed`, as PHP does not support the `resource` type. For anonymous classes, it returns the name of the nearest ancestor or `object`. + +```php +$obj = new class extends Foo { }; +$type = Type::fromValue($obj); // 'Foo' +``` + + getNames(): (string|array)[] .[method] -------------------------------------- @@ -183,8 +207,8 @@ $type->isClassKeyword(); // false ``` -allows(string $type): bool .[method] ------------------------------------- +allows(string|Type $type): bool .[method] +----------------------------------------- The `allows()` method checks type compatibility. For example, it can determine if a value of a certain type could be passed as a parameter to a function expecting this type. @@ -197,3 +221,24 @@ $type->allows('Foo'); // false $type = Type::fromString('mixed'); $type->allows('null'); // true ``` + + +with(string|Type $type): Type .[method]{data-version:4.0.10} +------------------------------------------------------------ + +Returns a Type object that accepts both the original type and the one being added. It creates a so-called union type. + +The method is clever and does not duplicate types unnecessarily. If you add a type that is already present, or is a superset of the current type (e.g., adding `mixed` to `string`), the result is simplified. + +```php +$type = Type::fromString('string'); + +// Extending to nullable string +echo $type->with('null'); // '?string' + +// Creating a union type +echo $type->with('int'); // 'string|int' + +// Adding a type that supersedes everything +echo $type->with('mixed'); // 'mixed' +``` From 56d972aa4e026861d9fdbbc488514d0e9131362b Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Sat, 29 Nov 2025 20:59:05 +0100 Subject: [PATCH 010/112] nette/application 3.2.9 --- application/cs/templates.texy | 14 ++++++++++++++ application/en/templates.texy | 14 ++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/application/cs/templates.texy b/application/cs/templates.texy index 72b2f1e0cf..568b40837e 100644 --- a/application/cs/templates.texy +++ b/application/cs/templates.texy @@ -120,6 +120,20 @@ Proměnné do šablony předáváme zápisem do `$this->template`. V šabloně j $this->template->article = $this->articles->getById($id); ``` +Pokud chcete, aby se hodnota určité property automaticky předala do šablony jako proměnná, označte ji atributem `#[TemplateVariable]` a viditelností public nebo protected: .{data-version:3.2.9} + +```php +use Nette\Application\Attributes\TemplateVariable; + +class ArticlePresenter extends Nette\Application\UI\Presenter +{ + #[TemplateVariable] + public string $siteName = 'Můj blog'; +} +``` + +Pokud do šablony vložíte proměnnou se stejným názvem, `#[TemplateVariable]` ji nepřepíše. + Výchozí proměnné ---------------- diff --git a/application/en/templates.texy b/application/en/templates.texy index 1c4d0315eb..6a5ba43fd3 100644 --- a/application/en/templates.texy +++ b/application/en/templates.texy @@ -120,6 +120,20 @@ Variables are passed to templates by writing them to `$this->template`. They the $this->template->article = $this->articles->getById($id); ``` +To automatically pass a property value to the template as a variable, mark it with the `#[TemplateVariable]` attribute and public or protected visibility: .{data-version:3.2.9} + +```php +use Nette\Application\Attributes\TemplateVariable; + +class ArticlePresenter extends Nette\Application\UI\Presenter +{ + #[TemplateVariable] + public string $siteName = 'My blog'; +} +``` + +If you pass a variable with the same name to the template, `#[TemplateVariable]` won’t override it. + Default Variables ----------------- From 748f09a575b77f9b1ef129af169faa47a8b58fba Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Sat, 27 Dec 2025 19:02:41 +0100 Subject: [PATCH 011/112] adding missing stuff in nette/http --- http/cs/request.texy | 29 +++++++++++++++++++++++++++++ http/cs/response.texy | 10 +++++----- http/cs/urls.texy | 6 ++++++ http/en/request.texy | 29 +++++++++++++++++++++++++++++ http/en/response.texy | 10 +++++----- http/en/urls.texy | 6 ++++++ nette/cs/glossary.texy | 7 +++++++ nette/en/glossary.texy | 7 +++++++ 8 files changed, 94 insertions(+), 10 deletions(-) diff --git a/http/cs/request.texy b/http/cs/request.texy index 5dc90de308..1827f489e0 100644 --- a/http/cs/request.texy +++ b/http/cs/request.texy @@ -184,6 +184,30 @@ $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +Vrací origin, ze kterého požadavek přišel. Origin se skládá z protokolu, hostname a portu - například `https://example.com:8080`. Vrací `null`, pokud hlavička origin není přítomna nebo je nastavena na `'null'`. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +Prohlížeč posílá hlavičku `Origin` v následujících případech: +- Požadavky mezi doménami (AJAX volání na jinou doménu) +- POST, PUT, DELETE a další modifikující požadavky +- Požadavky provedené pomocí Fetch API + +Prohlížeč NEPOSÍLÁ hlavičku `Origin` při: +- Běžných GET požadavcích na stejnou doménu (navigace v rámci téže domény) +- Přímé navigaci zadáním URL do adresního řádku +- Požadavcích z jiných klientů než prohlížeče (pokud není ručně přidána) + +.[note] +Na rozdíl od hlavičky `Referer` obsahuje `Origin` pouze schéma, host a port - nikoli celou cestu URL. To ji činí vhodnější pro bezpečnostní kontroly při zachování soukromí uživatele. Hlavička `Origin` se primárně používá pro validaci [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing). + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- Detekuje jazyk. Jako parametr `$lang` předáme pole s jazyky, které aplikace podporuje, a ona vrátí ten, který by viděl návštěvníkův prohlížeč nejraději. Nejsou to žádná kouzla, jen se využívá hlavičky `Accept-Language`. Pokud nedojde k žádné shodě, vrací `null`. @@ -389,6 +413,11 @@ getTemporaryFile(): string .[method] Vrací cestu k dočasné lokaci uploadovaného souboru. V případě, že upload nebyl úspěšný, vrací `''`. +__toString(): string .[method] +------------------------------ +Vrací cestu k dočasnému umístění nahraného souboru. To umožňuje objekt `FileUpload` použít přímo jako řetězec. + + isImage(): bool .[method] ------------------------- 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]. diff --git a/http/cs/response.texy b/http/cs/response.texy index baea11f900..4732bc7a76 100644 --- a/http/cs/response.texy +++ b/http/cs/response.texy @@ -34,9 +34,9 @@ isSent(): bool .[method] Vrací, zda už došlo k odeslání hlaviček ze serveru do prohlížeče, a tedy již není možné odesílat hlavičky či měnit stavový kód. -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Odešle HTTP hlavičku a **přepíše** dříve odeslanou hlavičkou stejného jména. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Odešle HTTP hlavičku a **přepíše** dříve odeslanou hlavičkou stejného jména. Pokud je `$value` `null`, bude záhlaví odstraněno. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -115,8 +115,8 @@ $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='Lax') .[method] +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- Odešle cookie. Výchozí hodnoty parametrů: | `$path` | `'/'` | cookie má dosah na všechny cesty v (sub)doméně *(konfigurovatelné)* diff --git a/http/cs/urls.texy b/http/cs/urls.texy index 9e722a9d7c..e40a157e18 100644 --- a/http/cs/urls.texy +++ b/http/cs/urls.texy @@ -82,6 +82,7 @@ Můžeme pracovat i s jednotlivými query parametry pomocí: |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] @@ -107,6 +108,11 @@ $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +Převede URL do kanonického tvaru. To zahrnuje například seřazení parametrů v query stringu podle abecedy, převod hostname na malá písmena a odstranění nadbytečných znaků. + + 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. diff --git a/http/en/request.texy b/http/en/request.texy index ae45438508..47e0232f54 100644 --- a/http/en/request.texy +++ b/http/en/request.texy @@ -184,6 +184,30 @@ $body = $httpRequest->getRawBody(); ``` +getOrigin(): ?UrlImmutable .[method] +------------------------------------ +Returns the origin from which the request came. An origin consists of the scheme (protocol), hostname, and port - for example, `https://example.com:8080`. Returns `null` if the origin header is not present or is set to `'null'`. + +```php +$origin = $httpRequest->getOrigin(); +echo $origin; // https://example.com:8080 +echo $origin?->getHost(); // example.com +``` + +The browser sends the `Origin` header in the following cases: +- Cross-origin requests (AJAX calls to a different domain) +- POST, PUT, DELETE, and other modifying requests +- Requests made using the Fetch API + +The browser does NOT send the `Origin` header for: +- Regular GET requests to the same domain (same-origin navigation) +- Direct navigation by typing a URL into the address bar +- Requests from non-browser clients + +.[note] +Unlike the `Referer` header, `Origin` contains only the scheme, host, and port - not the full URL path. This makes it more suitable for security checks while preserving user privacy. The `Origin` header is primarily used for [CORS |nette:glossary#Cross-Origin Resource Sharing (CORS)] (Cross-Origin Resource Sharing) validation. + + detectLanguage(array $langs): ?string .[method] ----------------------------------------------- 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`. @@ -389,6 +413,11 @@ getTemporaryFile(): string .[method] Returns the path to the temporary location of the uploaded file. If the upload was not successful, it returns `''`. +__toString(): string .[method] +------------------------------ +Returns the path to the temporary location of the uploaded file. This allows the `FileUpload` object to be used directly as a string. + + isImage(): bool .[method] ------------------------- 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]. diff --git a/http/en/response.texy b/http/en/response.texy index 7654f039e9..7dbd4dca00 100644 --- a/http/en/response.texy +++ b/http/en/response.texy @@ -34,9 +34,9 @@ isSent(): bool .[method] 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** a previously sent header of the same name. +setHeader(string $name, ?string $value) .[method] +------------------------------------------------- +Sends an HTTP header and **overwrites** a previously sent header of the same name. If `$value` is `null`, the header will be removed. ```php $httpResponse->setHeader('Pragma', 'no-cache'); @@ -115,8 +115,8 @@ $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='Lax') .[method] +-------------------------------------------------------------------------------------------------------------------------------------------------------------------- Sends a cookie. Default parameter values: | `$path` | `'/'` | cookie is available for all paths within the (sub)domain *(configurable)* diff --git a/http/en/urls.texy b/http/en/urls.texy index 84a947c12f..608fa0064f 100644 --- a/http/en/urls.texy +++ b/http/en/urls.texy @@ -82,6 +82,7 @@ We can also work with individual query parameters using: |--------------------------------------------------- | `setQuery(string\|array $query)` | `getQueryParameters(): array` | `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` +| `appendQuery(string|array $query)` | getDomain(int $level = 2): string .[method] @@ -107,6 +108,11 @@ $url->isEqual('https://nette.org'); ``` +canonicalize() .[method] +------------------------ +Converts the URL to canonical form. This includes, for example, sorting the parameters in the query string alphabetically, converting the hostname to lowercase, and removing redundant characters. + + 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. diff --git a/nette/cs/glossary.texy b/nette/cs/glossary.texy index 05ade5cdbc..7b273ba481 100644 --- a/nette/cs/glossary.texy +++ b/nette/cs/glossary.texy @@ -36,6 +36,13 @@ Cross-Site Request Forgery (CSRF) 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. +Cross-Origin Resource Sharing (CORS) +------------------------------------ +CORS je bezpečnostní mechanismus, který umožňuje webové stránce provádět JavaScriptové požadavky na jinou doménu, než ze které byla stránka načtena. Bez CORS prohlížeče takové požadavky z bezpečnostních důvodů blokují. + +Například pokud vaše webová stránka běží na `https://myapp.com` a pokusí se pomocí JavaScriptu (AJAX, Fetch API) načíst data z `https://api.example.com`, prohlížeč ověří, zda API server tento požadavek mezi doménami povoluje. API server musí odpovědět speciálními HTTP hlavičkami, jako je `Access-Control-Allow-Origin: https://myapp.com`, aby udělil povolení. + + Dependency Injection -------------------- Dependency Injection (DI) je návrhový vzor, který říká, jak oddělit vytváření objektů od jejich závislostí. Tedy že třída není zodpovědná za vytváření nebo inicializaci svých závislostí, ale místo toho jsou jí tyto závislosti poskytovány externím kódem (tím může i [DI kontejner |#Dependency Injection kontejner]). Výhoda spočívá v tom, že umožňuje větší flexibilitu kódu, lepší srozumitelnost a snazší testování aplikace, protože závislosti jsou snadno nahraditelné a izolované od ostatních částí kódu. Více v kapitole [Co je Dependency Injection? |dependency-injection:introduction] diff --git a/nette/en/glossary.texy b/nette/en/glossary.texy index e23d51d384..bc555933a0 100644 --- a/nette/en/glossary.texy +++ b/nette/en/glossary.texy @@ -36,6 +36,13 @@ A Cross-Site Request Forgery attack involves the attacker luring a victim to a p Nette Framework **automatically protects forms and signals in presenters** against this type of attack by preventing them from being submitted or triggered from another domain. +Cross-Origin Resource Sharing (CORS) +------------------------------------ +CORS is a security mechanism that allows a web page to make JavaScript requests to a different domain than the one from which the page was loaded. Without CORS, browsers block such requests for security reasons. + +For example, if your website runs at `https://myapp.com` and tries to fetch data from `https://api.example.com` using JavaScript (AJAX, Fetch API), the browser will check if the API server allows this cross-origin request. The API server must respond with special HTTP headers, such as `Access-Control-Allow-Origin: https://myapp.com`, to grant permission. + + Dependency Injection -------------------- Dependency Injection (DI) is a design pattern that dictates how to separate the creation of objects from their dependencies. This means a class is not responsible for creating or initializing its dependencies; instead, these dependencies are provided by external code (which could be a [DI container |#Dependency Injection Container]). The advantage lies in increased code flexibility, better understandability, and easier application testing, as dependencies are easily replaceable and isolated from other code parts. More in the chapter [What is Dependency Injection? |dependency-injection:introduction] From ae083e73c9096e1c3d9ab946f2707021ed7b68a0 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Sun, 4 Jan 2026 06:13:49 +0100 Subject: [PATCH 012/112] used first-class callables --- best-practices/cs/creating-editing-form.texy | 16 ++++++++-------- best-practices/cs/form-reuse.texy | 8 ++++---- best-practices/cs/lets-create-contact-form.texy | 4 ++-- best-practices/cs/restore-request.texy | 4 ++-- best-practices/en/creating-editing-form.texy | 16 ++++++++-------- best-practices/en/form-reuse.texy | 8 ++++---- best-practices/en/lets-create-contact-form.texy | 4 ++-- best-practices/en/restore-request.texy | 4 ++-- dependency-injection/cs/services.texy | 2 +- dependency-injection/en/services.texy | 2 +- forms/cs/in-presenter.texy | 14 +++++++------- forms/cs/validation.texy | 4 ++-- forms/en/in-presenter.texy | 16 ++++++++-------- forms/en/validation.texy | 4 ++-- 14 files changed, 53 insertions(+), 53 deletions(-) diff --git a/best-practices/cs/creating-editing-form.texy b/best-practices/cs/creating-editing-form.texy index 8babb5c58c..4530f060d4 100644 --- a/best-practices/cs/creating-editing-form.texy +++ b/best-practices/cs/creating-editing-form.texy @@ -29,11 +29,11 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... přidáme políčka formuláře ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // přidání záznamu do databáze $this->flashMessage('Successfully added'); @@ -91,11 +91,11 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... přidáme políčka formuláře ... $form->setDefaults($this->record); // nastavení výchozích hodnot - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->update($this->record->id, $data); // aktualizace záznamu $this->flashMessage('Successfully updated'); @@ -153,7 +153,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void @@ -168,7 +168,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter $form = $this->getComponent('recordForm'); $form->setDefaults($record); // nastavení výchozích hodnot - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form @@ -185,14 +185,14 @@ class RecordPresenter extends Nette\Application\UI\Presenter return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private 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 + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); $this->facade->update($id, $data); // aktualizace záznamu diff --git a/best-practices/cs/form-reuse.texy b/best-practices/cs/form-reuse.texy index bd6c3b8afd..ac95ca2617 100644 --- a/best-practices/cs/form-reuse.texy +++ b/best-practices/cs/form-reuse.texy @@ -193,11 +193,11 @@ class EditFormFactory $form->addText('title', 'Titulek:'); // zde se přidávají další formulářová pole $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = [$this, 'processForm']; + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // zpracování odeslaných dat @@ -284,12 +284,12 @@ class EditControl extends Nette\Application\UI\Control $form->addText('title', 'Titulek:'); // zde se přidávají další formulářová pole $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = [$this, 'processForm']; + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // zpracování odeslaných dat diff --git a/best-practices/cs/lets-create-contact-form.texy b/best-practices/cs/lets-create-contact-form.texy index ec298951be..e5d44812fa 100644 --- a/best-practices/cs/lets-create-contact-form.texy +++ b/best-practices/cs/lets-create-contact-form.texy @@ -24,11 +24,11 @@ class HomePresenter extends Presenter $form->addTextarea('message', 'Zpráva:') ->setRequired('Zadejte zprávu'); $form->addSubmit('send', 'Odeslat'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { // odeslání emailu } diff --git a/best-practices/cs/restore-request.texy b/best-practices/cs/restore-request.texy index 46a25c7f87..05a54331ae 100644 --- a/best-practices/cs/restore-request.texy +++ b/best-practices/cs/restore-request.texy @@ -41,11 +41,11 @@ class SignPresenter extends Nette\Application\UI\Presenter { $form = new Nette\Application\UI\Form; // ... přidáme políčka formuláře ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + $form->onSuccess[] = $this->signInFormSubmitted(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSubmitted($form) { // ... tady uživatele přihlásíme ... diff --git a/best-practices/en/creating-editing-form.texy b/best-practices/en/creating-editing-form.texy index 9349473ca3..27e0af202b 100644 --- a/best-practices/en/creating-editing-form.texy +++ b/best-practices/en/creating-editing-form.texy @@ -29,11 +29,11 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... add form fields ... - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // add record to the database $this->flashMessage('Successfully added'); @@ -91,11 +91,11 @@ class RecordPresenter extends Nette\Application\UI\Presenter // ... add form fields ... $form->setDefaults($this->record); // set default values - $form->onSuccess[] = [$this, 'recordFormSucceeded']; + $form->onSuccess[] = $this->recordFormSucceeded(...); return $form; } - public function recordFormSucceeded(Form $form, array $data): void + private function recordFormSucceeded(Form $form, array $data): void { $this->facade->update($this->record->id, $data); // update record $this->flashMessage('Successfully updated'); @@ -153,7 +153,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter public function actionAdd(): void { $form = $this->getComponent('recordForm'); - $form->onSuccess[] = [$this, 'addingFormSucceeded']; + $form->onSuccess[] = $this->addingFormSucceeded(...); } public function actionEdit(int $id): void @@ -168,7 +168,7 @@ class RecordPresenter extends Nette\Application\UI\Presenter $form = $this->getComponent('recordForm'); $form->setDefaults($record); // set default values - $form->onSuccess[] = [$this, 'editingFormSucceeded']; + $form->onSuccess[] = $this->editingFormSucceeded(...); } protected function createComponentRecordForm(): Form @@ -185,14 +185,14 @@ class RecordPresenter extends Nette\Application\UI\Presenter return $form; } - public function addingFormSucceeded(Form $form, array $data): void + private function addingFormSucceeded(Form $form, array $data): void { $this->facade->add($data); // add record to the database $this->flashMessage('Successfully added'); $this->redirect('...'); } - public function editingFormSucceeded(Form $form, array $data): void + private function editingFormSucceeded(Form $form, array $data): void { $id = (int) $this->getParameter('id'); $this->facade->update($id, $data); // update record diff --git a/best-practices/en/form-reuse.texy b/best-practices/en/form-reuse.texy index c2027ab6f0..2d91c006fc 100644 --- a/best-practices/en/form-reuse.texy +++ b/best-practices/en/form-reuse.texy @@ -193,11 +193,11 @@ class EditFormFactory $form->addText('title', 'Title:'); // additional form fields are added here $form->addSubmit('send', 'Save'); - $form->onSuccess[] = [$this, 'processForm']; + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // processing of submitted data @@ -284,12 +284,12 @@ class EditControl extends Nette\Application\UI\Control $form->addText('title', 'Title:'); // additional form fields are added here $form->addSubmit('send', 'Save'); - $form->onSuccess[] = [$this, 'processForm']; + $form->onSuccess[] = $this->processForm(...); return $form; } - public function processForm(Form $form, array $data): void + private function processForm(Form $form, array $data): void { try { // processing of submitted data diff --git a/best-practices/en/lets-create-contact-form.texy b/best-practices/en/lets-create-contact-form.texy index dab7982265..0fd677048d 100644 --- a/best-practices/en/lets-create-contact-form.texy +++ b/best-practices/en/lets-create-contact-form.texy @@ -24,11 +24,11 @@ class HomePresenter extends Presenter $form->addTextarea('message', 'Message:') ->setRequired('Please enter a message'); $form->addSubmit('send', 'Send'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; + $form->onSuccess[] = $this->contactFormSucceeded(...); return $form; } - public function contactFormSucceeded(Form $form, $data): void + private function contactFormSucceeded(Form $form, $data): void { // sending an email } diff --git a/best-practices/en/restore-request.texy b/best-practices/en/restore-request.texy index 6a95370d1c..cea14d7b66 100644 --- a/best-practices/en/restore-request.texy +++ b/best-practices/en/restore-request.texy @@ -41,11 +41,11 @@ class SignPresenter extends Nette\Application\UI\Presenter { $form = new Nette\Application\UI\Form; // ... add form fields ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; + $form->onSuccess[] = $this->signInFormSubmitted(...); return $form; } - public function signInFormSubmitted($form) + private function signInFormSubmitted($form) { // ... log the user in here ... diff --git a/dependency-injection/cs/services.texy b/dependency-injection/cs/services.texy index 00a55fed56..8a86b230e9 100644 --- a/dependency-injection/cs/services.texy +++ b/dependency-injection/cs/services.texy @@ -181,7 +181,7 @@ public function createServiceFoo(): Foo { $service = new Foo; $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; + $service->onClick[] = $this->getService('bar')->clickHandler(...); return $service; } ``` diff --git a/dependency-injection/en/services.texy b/dependency-injection/en/services.texy index 7b3ad6b364..d724a229cf 100644 --- a/dependency-injection/en/services.texy +++ b/dependency-injection/en/services.texy @@ -181,7 +181,7 @@ public function createServiceFoo(): Foo { $service = new Foo; $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; + $service->onClick[] = $this->getService('bar')->clickHandler(...); return $service; } ``` diff --git a/forms/cs/in-presenter.texy b/forms/cs/in-presenter.texy index 33ac7fa95d..2c827d31f7 100644 --- a/forms/cs/in-presenter.texy +++ b/forms/cs/in-presenter.texy @@ -19,7 +19,7 @@ $form = new Form; $form->addText('name', 'Jméno:'); $form->addPassword('password', 'Heslo:'); $form->addSubmit('send', 'Registrovat'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->onSuccess[] = $this->formSucceeded(...); ``` a v prohlížeči se zobrazí takto: @@ -42,11 +42,11 @@ class HomePresenter extends Nette\Application\UI\Presenter $form->addText('name', 'Jméno:'); $form->addPassword('password', 'Heslo:'); $form->addSubmit('send', 'Registrovat'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { // tady zpracujeme data odeslaná formulářem // $data->name obsahuje jméno @@ -303,16 +303,16 @@ Pokud má formulář více než jedno tlačítko, potřebujeme zpravidla rozliš ```php $form->addSubmit('save', 'Uložit') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', 'Smazat') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` Tyto handlery se volají pouze v případě validně vyplněného formuláře, stejně jako v případě události `onSuccess`. Rozdíl je v tom, že jako první parametr se místo formulář může předat odesílací tlačítko, záleží na typu, který uvedete: ```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... @@ -403,7 +403,7 @@ protected function createComponentSignInForm(): Form $form = $this->formFactory->create(); // můžeme formulář pozměnit, zde například měníme popisku na tlačítku $form['send']->setCaption('Pokračovat'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // a přidáme handler + $form->onSuccess[] = $this->signInFormSuceeded(...); // a přidáme handler return $form; } ``` diff --git a/forms/cs/validation.texy b/forms/cs/validation.texy index e313be57b9..479e12df74 100644 --- a/forms/cs/validation.texy +++ b/forms/cs/validation.texy @@ -223,11 +223,11 @@ protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { $form->addError('Tato kombinace není možná.'); diff --git a/forms/en/in-presenter.texy b/forms/en/in-presenter.texy index a1543bcaa1..b4cde43641 100644 --- a/forms/en/in-presenter.texy +++ b/forms/en/in-presenter.texy @@ -19,14 +19,14 @@ $form = new Form; $form->addText('name', 'Name:'); $form->addPassword('password', 'Password:'); $form->addSubmit('send', 'Sign up'); -$form->onSuccess[] = [$this, 'formSucceeded']; +$form->onSuccess[] = $this->formSucceeded(...); ``` and in the browser, it will be displayed like this: [* form-en.webp *] -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. +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` states that after submission and successful validation, the method `$this->formSucceeded()` should be called. 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: @@ -42,11 +42,11 @@ class HomePresenter extends Nette\Application\UI\Presenter $form->addText('name', 'Name:'); $form->addPassword('password', 'Password:'); $form->addSubmit('send', 'Sign up'); - $form->onSuccess[] = [$this, 'formSucceeded']; + $form->onSuccess[] = $this->formSucceeded(...); return $form; } - public function formSucceeded(Form $form, $data): void + private function formSucceeded(Form $form, $data): void { // here we will process the data sent by the form // $data->name contains name @@ -303,16 +303,16 @@ If the form has more than one button, we usually need to distinguish which one w ```php $form->addSubmit('save', 'Save') - ->onClick[] = [$this, 'saveButtonPressed']; + ->onClick[] = $this->saveButtonPressed(...); $form->addSubmit('delete', 'Delete') - ->onClick[] = [$this, 'deleteButtonPressed']; + ->onClick[] = $this->deleteButtonPressed(...); ``` 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) +private function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) { $form = $button->getForm(); // ... @@ -403,7 +403,7 @@ protected function createComponentSignInForm(): Form $form = $this->formFactory->create(); // we can change the form, here for example we change the label on the button $form['login']->setCaption('Continue'); - $form->onSuccess[] = [$this, 'signInFormSubmitted']; // and add handler + $form->onSuccess[] = $this->signInFormSubmitted(...); // and add handler return $form; } ``` diff --git a/forms/en/validation.texy b/forms/en/validation.texy index 0c32b3bb85..d9a9626cc6 100644 --- a/forms/en/validation.texy +++ b/forms/en/validation.texy @@ -223,11 +223,11 @@ protected function createComponentSignInForm(): Form { $form = new Form; // ... - $form->onValidate[] = [$this, 'validateSignInForm']; + $form->onValidate[] = $this->validateSignInForm(...); return $form; } -public function validateSignInForm(Form $form, \stdClass $data): void +private function validateSignInForm(Form $form, \stdClass $data): void { if ($data->foo > 1 && $data->bar > 5) { $form->addError('This combination is not possible.'); From f41ca807431d061d13a209d02de58178c15937b0 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Tue, 6 Jan 2026 15:54:33 +0100 Subject: [PATCH 013/112] added coding standard section about global functions and constants --- contributing/cs/coding-standard.texy | 17 +++++++++++++++++ contributing/en/coding-standard.texy | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/contributing/cs/coding-standard.texy b/contributing/cs/coding-standard.texy index d9e3cc8c85..aaef617655 100644 --- a/contributing/cs/coding-standard.texy +++ b/contributing/cs/coding-standard.texy @@ -109,6 +109,23 @@ public function find(string $dir, array $options): array ``` +Globální funkce a konstanty +=========================== + +Globální funkce a konstanty se píší bez úvodního zpětného lomítka, tedy `count($arr)` nikoliv `\count($arr)`. Pro funkce, které umí PHP optimalizovat, uvedeme na začátku souboru `use function`, aby je kompilátor mohl přeložit efektivněji. Jedná se zejména o funkce jako `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf` aj. Funkce se uvádějí na jednom řádku, aby úvodní blok importů nebyl zbytečně velký: + +```php +use Nette; +use function count, is_array, is_scalar, sprintf; +``` + +Výjimečně takto uvádíme i konstanty, u kterých může znalost hodnoty posloužit kompilátoru: + +```php +use const PHP_OS_FAMILY; +``` + + Tabulátory místo mezer ====================== diff --git a/contributing/en/coding-standard.texy b/contributing/en/coding-standard.texy index e98f821d4d..df9a0a86cb 100644 --- a/contributing/en/coding-standard.texy +++ b/contributing/en/coding-standard.texy @@ -109,6 +109,23 @@ public function find(string $dir, array $options): array ``` +Global Functions and Constants +============================== + +Global functions and constants are written without a leading backslash, i.e., `count($arr)` not `\count($arr)`. For functions that PHP can optimize, add `use function` at the beginning of the file so the compiler can translate them more efficiently. These include functions like `count`, `strlen`, `is_array`, `is_string`, `is_scalar`, `sprintf`, etc. Functions are listed on a single line to keep the import block compact: + +```php +use Nette; +use function count, is_array, is_scalar, sprintf; +``` + +Occasionally, we also import constants whose value knowledge may help the compiler: + +```php +use const PHP_OS_FAMILY; +``` + + Tabs Instead of Spaces ====================== From 24ac08ea5d7fee65d05c67f52af587c6e4863004 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Tue, 6 Jan 2026 23:36:00 +0100 Subject: [PATCH 014/112] Tester 2.6: updated documentation for php.ini loading behavior change --- tester/cs/guide.texy | 8 ++++---- tester/cs/running-tests.texy | 27 +++++++++++---------------- tester/en/guide.texy | 8 ++++---- tester/en/running-tests.texy | 27 +++++++++++---------------- 4 files changed, 30 insertions(+), 40 deletions(-) diff --git a/tester/cs/guide.texy b/tester/cs/guide.texy index c2fc76c3fe..bc75c64ec9 100644 --- a/tester/cs/guide.texy +++ b/tester/cs/guide.texy @@ -116,10 +116,9 @@ Výstup může vypadat takto: /--pre .[terminal] _____ ___ ___ _____ ___ ___ |_ _/ __)( __/_ _/ __)| _ ) - |_| \___ /___) |_| \___ |_|_\ v2.5.2 + |_| \___ /___) |_| \___ |_|_\ v2.6.0 -Note: No php.ini is used. -PHP 8.3.2 (cli) | php -n | 8 threads +PHP 8.5.2 (cli) | php | 8 threads ........s................<span style="color: #FFF; background-color: #900">F</span>......... @@ -160,7 +159,8 @@ Podporované verze PHP | verze | kompatibilní s PHP |------------------|------------------- -| Tester 2.5 | PHP 8.0 – 8.3 +| Tester 2.6 | PHP 8.0 – 8.5 +| Tester 2.5 | PHP 8.0 – 8.5 | Tester 2.4 | PHP 7.2 – 8.2 | Tester 2.3 | PHP 7.1 – 8.0 | Tester 2.1 – 2.2 | PHP 7.1 – 7.3 diff --git a/tester/cs/running-tests.texy b/tester/cs/running-tests.texy index c2afe8df0a..667d8253de 100644 --- a/tester/cs/running-tests.texy +++ b/tester/cs/running-tests.texy @@ -23,10 +23,9 @@ Výstup může vypadat třeba takto: /--pre .[terminal] _____ ___ ___ _____ ___ ___ |_ _/ __)( __/_ _/ __)| _ ) - |_| \___ /___) |_| \___ |_|_\ v2.5.2 + |_| \___ /___) |_| \___ |_|_\ v2.6.0 -Note: No php.ini is used. -PHP 8.3.2 (cli) | php -n | 8 threads +PHP 8.5.2 (cli) | php | 8 threads ........s.......................... @@ -37,9 +36,6 @@ Při opakovaném spuštění nejprve provádí testy, které při předchozím b Pokud žádný test neselže, návratový kód Testeru je nula. Jinak je návratový kód nenulový. -.[warning] -Tester spouští PHP procesy bez `php.ini`. Detailněji v části [#Vlastní php.ini]. - Parametry příkazové řádky ========================= @@ -56,8 +52,8 @@ Usage: Options: -p <path> Specify PHP interpreter to run (default: php). - -c <path> Look for php.ini file (or look in directory) <path>. - -C Use system-wide php.ini. + -c <path> Use custom php.ini, ignore system configuration. + -C With -c, include system configuration as well. -d <key=value>... Define INI entry 'key' with value 'value'. -s Show information about skipped tests. --stop-on-fail Stop execution upon the first failure. @@ -86,12 +82,12 @@ tester -p /home/user/php-7.2.0-beta/php-cgi tests -c <path> .[filter] ------------------- -Určuje, který `php.ini` se bude používat při spouštění testů. Ve výchozím stavu se žádný php.ini nepoužije. Více v části [#Vlastní php.ini]. +Použije vlastní `php.ini` soubor a ignoruje systémovou konfiguraci. Více v části [#Vlastní php.ini]. -C .[filter] ------------ -Použije se systémové `php.ini`. Na UNIXu také všechny příslušné INI soubory `/etc/php/{sapi}/conf.d/*.ini`. Více v části [#Vlastní php.ini]. +Při použití společně s `-c` zachová i systémovou konfiguraci (neignoruje ji). Více v části [#Vlastní php.ini]. -d <key=value> .[filter] @@ -221,11 +217,7 @@ Použijeme současně s volbou `--coverage`. `<path>` je cesta ke zdrojovým kó Vlastní php.ini =============== -Tester spouští PHP procesy s parametrem `-n`, což znamená, že žádné `php.ini` není načteno. V UNIXu ani ty z `/etc/php/conf.d/*.ini`. To zajistí shodné prostředí pro běh testů, ale také vyřadí všechna PHP rozšíření běžně načtená systémovým PHP. - -Chcete-li načítání systémových php.ini souborů zachovat, použijte parametr `-C`. - -Pokud nějaká rozšíření nebo speciální INI nastavení pro testy potřebujete, doporučujeme vytvoření vlastního `php.ini` souboru, který bude distribuován s testy. Tester pak spouštíme s parametrem `-c`, například `tester -c tests/php.ini tests`, kde INI soubor může vypadat takto: +Pro testy můžete použít vlastní `php.ini` soubor. Pokud potřebujete specifická rozšíření nebo speciální INI nastavení, doporučujeme vytvořit vlastní `php.ini`, který bude distribuován s testy. Tester pak spouštíme s parametrem `-c`, například `tester -c tests/php.ini tests`, kde INI soubor může vypadat takto: ```ini [PHP] @@ -236,4 +228,7 @@ extension=php_pdo_pgsql.dll memory_limit=512M ``` -Spuštění Testeru v UNIXu se systémovým `php.ini`, například `tester -c /etc/php/cli/php.ini` nenačte ostatní INI z `/etc/php/conf.d/*.ini`. To je vlastnost PHP, ne Testeru. +Při použití `-c` Tester **ignoruje systémovou konfiguraci** (spouští PHP s příznakem `-n`). Chcete-li zachovat i systémovou konfiguraci, přidejte volbu `-C`: `tester -c tests/php.ini -C tests`. Ani při kombinaci `-c` a `-C` se nenačtou v UNIXu ostatní INI soubory z `/etc/php/conf.d/*.ini`. To je vlastnost PHP, ne Testeru. + +.[note] +Do verze 2.6 Tester bez uvedení `-c` spouštěl PHP s parametrem `-n`, tedy bez php.ini; volba `-C` toto potlačila. Od verze 2.6 se systémové php.ini načítá automaticky. Chování při použití `-c` zůstává stejné. diff --git a/tester/en/guide.texy b/tester/en/guide.texy index ed2dcdd8b8..e5361308ac 100644 --- a/tester/en/guide.texy +++ b/tester/en/guide.texy @@ -116,10 +116,9 @@ The output may look like this: /--pre .[terminal] _____ ___ ___ _____ ___ ___ |_ _/ __)( __/_ _/ __)| _ ) - |_| \___ /___) |_| \___ |_|_\ v2.5.2 + |_| \___ /___) |_| \___ |_|_\ v2.6.0 -Note: No php.ini is used. -PHP 8.3.2 (cli) | php -n | 8 threads +PHP 8.5.2 (cli) | php | 8 threads ........s................<span style="color: #FFF; background-color: #900">F</span>......... @@ -160,7 +159,8 @@ Supported PHP versions | Version | Compatible with PHP |------------------|------------------- -| Tester 2.5 | PHP 8.0 – 8.3 +| Tester 2.6 | PHP 8.0 – 8.5 +| Tester 2.5 | PHP 8.0 – 8.5 | Tester 2.4 | PHP 7.2 – 8.2 | Tester 2.3 | PHP 7.1 – 8.0 | Tester 2.1 – 2.2 | PHP 7.1 – 7.3 diff --git a/tester/en/running-tests.texy b/tester/en/running-tests.texy index 553ba3e3be..0f89235870 100644 --- a/tester/en/running-tests.texy +++ b/tester/en/running-tests.texy @@ -23,10 +23,9 @@ The output may look like this: /--pre .[terminal] _____ ___ ___ _____ ___ ___ |_ _/ __)( __/_ _/ __)| _ ) - |_| \___ /___) |_| \___ |_|_\ v2.5.2 + |_| \___ /___) |_| \___ |_|_\ v2.6.0 -Note: No php.ini is used. -PHP 8.3.2 (cli) | php -n | 8 threads +PHP 8.5.2 (cli) | php | 8 threads ........s.......................... @@ -37,9 +36,6 @@ When run again, it first executes the tests that failed in the previous run, so The Tester's exit code is zero if no test fails. Otherwise, it is non-zero. -.[warning] -The Tester runs PHP processes without `php.ini`. More details in the [#Own php.ini] section. - Command-Line Options ==================== @@ -56,8 +52,8 @@ Usage: Options: -p <path> Specify PHP interpreter to run (default: php). - -c <path> Look for php.ini file (or look in directory) <path>. - -C Use system-wide php.ini. + -c <path> Use custom php.ini, ignore system configuration. + -C With -c, include system configuration as well. -d <key=value>... Define INI entry 'key' with value 'value'. -s Show information about skipped tests. --stop-on-fail Stop execution upon the first failure. @@ -86,12 +82,12 @@ tester -p /home/user/php-7.2.0-beta/php-cgi tests -c <path> .[filter] ------------------- -Specifies which `php.ini` will be used when running tests. By default, no php.ini is used. See [#Own php.ini] for more information. +Uses a custom `php.ini` file and ignores the system configuration. This is useful for running tests with specific settings. See [#Own php.ini] for more information. -C .[filter] ------------ -A system-wide `php.ini` is used. So on UNIX platform, all the `/etc/php/{sapi}/conf.d/*.ini` files too. See [#Own php.ini] section. +When used together with `-c`, includes the system configuration as well (does not ignore it). See [#Own php.ini] section. -d <key=value> .[filter] @@ -221,11 +217,7 @@ Used in conjunction with the `--coverage` option. `<path>` is the path to the so Own php.ini =========== -Tester runs PHP processes with the `-n` option, which means that no `php.ini` is loaded (not even those from `/etc/php/conf.d/*.ini` on UNIX systems). This ensures a consistent environment for running tests, but it also disables all external PHP extensions normally loaded by the system's PHP. - -To preserve the loading of system php.ini files, use the `-C` parameter. - -If you need specific extensions or special INI settings for your tests, we recommend creating your own `php.ini` file and distributing it with your tests. Then, run Tester with the `-c` option, for example, `tester -c tests/php.ini tests`. The INI file might look like this: +You can use a custom `php.ini` file for your tests. If you need specific extensions or special INI settings, we recommend creating your own `php.ini` file and distributing it with your tests. Then, run Tester with the `-c` option, for example, `tester -c tests/php.ini tests`. The INI file might look like this: ```ini [PHP] @@ -236,4 +228,7 @@ extension=php_pdo_pgsql.dll memory_limit=512M ``` -Running Tester on UNIX with a system `php.ini`, like `tester -c /etc/php/cli/php.ini`, does not load other INIs from `/etc/php/conf.d/*.ini`. This is a PHP behavior, not specific to Tester. +When using `-c`, Tester **ignores the system configuration** (runs PHP with `-n` flag). If you want to include system configuration as well, add the `-C` option: `tester -c tests/php.ini -C tests`. Even when combining `-c` and `-C`, other INI files from `/etc/php/conf.d/*.ini` are not loaded on UNIX. This is a PHP behavior, not specific to Tester. + +.[note] +Prior to version 2.6, without `-c`, Tester ran PHP with `-n` flag, i.e., without php.ini; the `-C` option suppressed this. From version 2.6, system php.ini is loaded by default. The behavior when using `-c` remains unchanged. From d98def6d5f6583ec218c36d53071e5713dece7e3 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Sat, 1 Aug 2026 22:23:54 +0200 Subject: [PATCH 015/112] typo --- database/cs/@home.texy | 2 -- database/de/@home.texy | 2 -- database/en/@home.texy | 2 -- database/es/@home.texy | 2 -- database/fr/@home.texy | 2 -- database/it/@home.texy | 2 -- database/ja/@home.texy | 2 -- database/pl/@home.texy | 2 -- database/ru/@home.texy | 2 -- database/tr/@home.texy | 2 -- 10 files changed, 20 deletions(-) diff --git a/database/cs/@home.texy b/database/cs/@home.texy index 58162d133d..2ecc6d492a 100644 --- a/database/cs/@home.texy +++ b/database/cs/@home.texy @@ -1,5 +1,3 @@ - - Podporované databáze ==================== diff --git a/database/de/@home.texy b/database/de/@home.texy index 45a96f08ed..08dc9239ef 100644 --- a/database/de/@home.texy +++ b/database/de/@home.texy @@ -1,5 +1,3 @@ - - Unterstützte Datenbanken ======================== diff --git a/database/en/@home.texy b/database/en/@home.texy index 7d7452d8d1..330104209f 100644 --- a/database/en/@home.texy +++ b/database/en/@home.texy @@ -1,5 +1,3 @@ - - Supported Databases =================== diff --git a/database/es/@home.texy b/database/es/@home.texy index 847f8e4d76..7b51fd460e 100644 --- a/database/es/@home.texy +++ b/database/es/@home.texy @@ -1,5 +1,3 @@ - - Bases de datos compatibles ========================== diff --git a/database/fr/@home.texy b/database/fr/@home.texy index f05b61a24f..b620dbd10b 100644 --- a/database/fr/@home.texy +++ b/database/fr/@home.texy @@ -1,5 +1,3 @@ - - Bases de données prises en charge ================================= diff --git a/database/it/@home.texy b/database/it/@home.texy index 97f1e1e9db..87e771d368 100644 --- a/database/it/@home.texy +++ b/database/it/@home.texy @@ -1,5 +1,3 @@ - - Database supportati =================== diff --git a/database/ja/@home.texy b/database/ja/@home.texy index 965d3ecec8..7b2a378ac3 100644 --- a/database/ja/@home.texy +++ b/database/ja/@home.texy @@ -1,5 +1,3 @@ - - サポートされているデータベース =============== diff --git a/database/pl/@home.texy b/database/pl/@home.texy index 4093fa6893..db62f89e70 100644 --- a/database/pl/@home.texy +++ b/database/pl/@home.texy @@ -1,5 +1,3 @@ - - Obsługiwane bazy danych ======================= diff --git a/database/ru/@home.texy b/database/ru/@home.texy index 0927782214..5dd4d7ff96 100644 --- a/database/ru/@home.texy +++ b/database/ru/@home.texy @@ -1,5 +1,3 @@ - - Поддерживаемые базы данных ========================== diff --git a/database/tr/@home.texy b/database/tr/@home.texy index 7700df2b84..290d2a782d 100644 --- a/database/tr/@home.texy +++ b/database/tr/@home.texy @@ -1,5 +1,3 @@ - - Desteklenen Veritabanları ========================= From 3c65f2bf9c3a7c6dab9b4ee3db6ece60af113423 Mon Sep 17 00:00:00 2001 From: David Grudl <david@grudl.com> Date: Sat, 1 Aug 2026 22:22:21 +0200 Subject: [PATCH 016/112] replaced bg, el, hu, pt, ro, sl and uk with redirects to English Every page of these mutations now holds a {{redirect: en:name}} directive, from which the web builds a permanent redirect, so backlinks and search indexing carry over to the English page instead of being thrown away. Menu and @meta files are deleted outright, having no URL of their own. Together they accounted for 804 engaged sessions a year out of 33 604, that is 2.4 percent of real documentation traffic; between 90 and 97 percent of their visits showed no engagement at all. Reasoning and data are in _tools/docs/0001-redukce-jazykovych-mutaci.md. --- application/bg/@home.texy | 85 -- application/bg/@left-menu.texy | 22 - application/bg/@meta.texy | 1 - application/bg/ajax.texy | 249 ---- application/bg/bootstrapping.texy | 297 ----- application/bg/components.texy | 485 ------- application/bg/configuration.texy | 191 --- application/bg/creating-links.texy | 286 ----- application/bg/directory-structure.texy | 526 -------- application/bg/how-it-works.texy | 200 --- application/bg/multiplier.texy | 63 - application/bg/presenters.texy | 500 -------- application/bg/routing.texy | 721 ----------- application/bg/templates.texy | 323 ----- application/el/@home.texy | 85 -- application/el/@left-menu.texy | 22 - application/el/@meta.texy | 1 - application/el/ajax.texy | 249 ---- application/el/bootstrapping.texy | 297 ----- application/el/components.texy | 485 ------- application/el/configuration.texy | 191 --- application/el/creating-links.texy | 286 ----- application/el/directory-structure.texy | 526 -------- application/el/how-it-works.texy | 200 --- application/el/multiplier.texy | 63 - application/el/presenters.texy | 500 -------- application/el/routing.texy | 721 ----------- application/el/templates.texy | 323 ----- application/hu/@home.texy | 85 -- application/hu/@left-menu.texy | 22 - application/hu/@meta.texy | 1 - application/hu/ajax.texy | 249 ---- application/hu/bootstrapping.texy | 297 ----- application/hu/components.texy | 485 ------- application/hu/configuration.texy | 191 --- application/hu/creating-links.texy | 286 ----- application/hu/directory-structure.texy | 526 -------- application/hu/how-it-works.texy | 200 --- application/hu/multiplier.texy | 63 - application/hu/presenters.texy | 500 -------- application/hu/routing.texy | 721 ----------- application/hu/templates.texy | 323 ----- application/pt/@home.texy | 85 -- application/pt/@left-menu.texy | 22 - application/pt/@meta.texy | 1 - application/pt/ajax.texy | 249 ---- application/pt/bootstrapping.texy | 297 ----- application/pt/components.texy | 485 ------- application/pt/configuration.texy | 191 --- application/pt/creating-links.texy | 286 ----- application/pt/directory-structure.texy | 526 -------- application/pt/how-it-works.texy | 200 --- application/pt/multiplier.texy | 63 - application/pt/presenters.texy | 500 -------- application/pt/routing.texy | 721 ----------- application/pt/templates.texy | 323 ----- application/ro/@home.texy | 85 -- application/ro/@left-menu.texy | 22 - application/ro/@meta.texy | 1 - application/ro/ajax.texy | 249 ---- application/ro/bootstrapping.texy | 297 ----- application/ro/components.texy | 485 ------- application/ro/configuration.texy | 191 --- application/ro/creating-links.texy | 286 ----- application/ro/directory-structure.texy | 526 -------- application/ro/how-it-works.texy | 200 --- application/ro/multiplier.texy | 63 - application/ro/presenters.texy | 500 -------- application/ro/routing.texy | 721 ----------- application/ro/templates.texy | 323 ----- application/sl/@home.texy | 85 -- application/sl/@left-menu.texy | 22 - application/sl/@meta.texy | 1 - application/sl/ajax.texy | 249 ---- application/sl/bootstrapping.texy | 297 ----- application/sl/components.texy | 485 ------- application/sl/configuration.texy | 191 --- application/sl/creating-links.texy | 286 ----- application/sl/directory-structure.texy | 526 -------- application/sl/how-it-works.texy | 200 --- application/sl/multiplier.texy | 63 - application/sl/presenters.texy | 500 -------- application/sl/routing.texy | 721 ----------- application/sl/templates.texy | 323 ----- application/uk/@home.texy | 85 -- application/uk/@left-menu.texy | 22 - application/uk/@meta.texy | 1 - application/uk/ajax.texy | 249 ---- application/uk/bootstrapping.texy | 297 ----- application/uk/components.texy | 485 ------- application/uk/configuration.texy | 191 --- application/uk/creating-links.texy | 286 ----- application/uk/directory-structure.texy | 526 -------- application/uk/how-it-works.texy | 200 --- application/uk/multiplier.texy | 63 - application/uk/presenters.texy | 500 -------- application/uk/routing.texy | 721 ----------- application/uk/templates.texy | 323 ----- assets/bg/@home.texy | 432 ------- assets/bg/@left-menu.texy | 5 - assets/bg/@meta.texy | 1 - assets/bg/configuration.texy | 188 --- assets/bg/vite.texy | 508 -------- assets/el/@home.texy | 432 ------- assets/el/@left-menu.texy | 5 - assets/el/@meta.texy | 1 - assets/el/configuration.texy | 188 --- assets/el/vite.texy | 508 -------- assets/hu/@home.texy | 432 ------- assets/hu/@left-menu.texy | 5 - assets/hu/@meta.texy | 1 - assets/hu/configuration.texy | 188 --- assets/hu/vite.texy | 508 -------- assets/pt/@home.texy | 432 ------- assets/pt/@left-menu.texy | 5 - assets/pt/@meta.texy | 1 - assets/pt/configuration.texy | 188 --- assets/pt/vite.texy | 508 -------- assets/ro/@home.texy | 432 ------- assets/ro/@left-menu.texy | 5 - assets/ro/@meta.texy | 1 - assets/ro/configuration.texy | 188 --- assets/ro/vite.texy | 508 -------- assets/sl/@home.texy | 432 ------- assets/sl/@left-menu.texy | 5 - assets/sl/@meta.texy | 1 - assets/sl/configuration.texy | 188 --- assets/sl/vite.texy | 508 -------- assets/uk/@home.texy | 432 ------- assets/uk/@left-menu.texy | 5 - assets/uk/@meta.texy | 1 - assets/uk/configuration.texy | 188 --- assets/uk/vite.texy | 508 -------- best-practices/bg/@home.texy | 69 - best-practices/bg/@meta.texy | 2 - best-practices/bg/attribute-requires.texy | 177 --- best-practices/bg/composer.texy | 282 ---- best-practices/bg/creating-editing-form.texy | 205 --- best-practices/bg/dynamic-snippets.texy | 173 --- best-practices/bg/editors-and-tools.texy | 84 -- best-practices/bg/form-reuse.texy | 348 ----- .../bg/inject-method-attribute.texy | 61 - .../bg/lets-create-contact-form.texy | 221 ---- best-practices/bg/microsites.texy | 63 - best-practices/bg/pagination.texy | 273 ---- .../bg/passing-settings-to-presenters.texy | 49 - best-practices/bg/post-links.texy | 56 - best-practices/bg/presenter-traits.texy | 47 - best-practices/bg/restore-request.texy | 62 - best-practices/el/@home.texy | 69 - best-practices/el/@meta.texy | 2 - best-practices/el/attribute-requires.texy | 177 --- best-practices/el/composer.texy | 282 ---- best-practices/el/creating-editing-form.texy | 205 --- best-practices/el/dynamic-snippets.texy | 173 --- best-practices/el/editors-and-tools.texy | 84 -- best-practices/el/form-reuse.texy | 348 ----- .../el/inject-method-attribute.texy | 61 - .../el/lets-create-contact-form.texy | 221 ---- best-practices/el/microsites.texy | 63 - best-practices/el/pagination.texy | 273 ---- .../el/passing-settings-to-presenters.texy | 49 - best-practices/el/post-links.texy | 56 - best-practices/el/presenter-traits.texy | 47 - best-practices/el/restore-request.texy | 62 - best-practices/hu/@home.texy | 69 - best-practices/hu/@meta.texy | 2 - best-practices/hu/attribute-requires.texy | 177 --- best-practices/hu/composer.texy | 282 ---- best-practices/hu/creating-editing-form.texy | 205 --- best-practices/hu/dynamic-snippets.texy | 173 --- best-practices/hu/editors-and-tools.texy | 84 -- best-practices/hu/form-reuse.texy | 348 ----- .../hu/inject-method-attribute.texy | 61 - .../hu/lets-create-contact-form.texy | 221 ---- best-practices/hu/microsites.texy | 63 - best-practices/hu/pagination.texy | 273 ---- .../hu/passing-settings-to-presenters.texy | 49 - best-practices/hu/post-links.texy | 56 - best-practices/hu/presenter-traits.texy | 47 - best-practices/hu/restore-request.texy | 62 - best-practices/pt/@home.texy | 69 - best-practices/pt/@meta.texy | 2 - best-practices/pt/attribute-requires.texy | 177 --- best-practices/pt/composer.texy | 282 ---- best-practices/pt/creating-editing-form.texy | 205 --- best-practices/pt/dynamic-snippets.texy | 173 --- best-practices/pt/editors-and-tools.texy | 84 -- best-practices/pt/form-reuse.texy | 348 ----- .../pt/inject-method-attribute.texy | 61 - .../pt/lets-create-contact-form.texy | 221 ---- best-practices/pt/microsites.texy | 63 - best-practices/pt/pagination.texy | 273 ---- .../pt/passing-settings-to-presenters.texy | 49 - best-practices/pt/post-links.texy | 56 - best-practices/pt/presenter-traits.texy | 47 - best-practices/pt/restore-request.texy | 62 - best-practices/ro/@home.texy | 69 - best-practices/ro/@meta.texy | 2 - best-practices/ro/attribute-requires.texy | 177 --- best-practices/ro/composer.texy | 282 ---- best-practices/ro/creating-editing-form.texy | 205 --- best-practices/ro/dynamic-snippets.texy | 173 --- best-practices/ro/editors-and-tools.texy | 84 -- best-practices/ro/form-reuse.texy | 348 ----- .../ro/inject-method-attribute.texy | 61 - .../ro/lets-create-contact-form.texy | 221 ---- best-practices/ro/microsites.texy | 63 - best-practices/ro/pagination.texy | 273 ---- .../ro/passing-settings-to-presenters.texy | 49 - best-practices/ro/post-links.texy | 56 - best-practices/ro/presenter-traits.texy | 47 - best-practices/ro/restore-request.texy | 62 - best-practices/sl/@home.texy | 69 - best-practices/sl/@meta.texy | 2 - best-practices/sl/attribute-requires.texy | 177 --- best-practices/sl/composer.texy | 282 ---- best-practices/sl/creating-editing-form.texy | 205 --- best-practices/sl/dynamic-snippets.texy | 173 --- best-practices/sl/editors-and-tools.texy | 84 -- best-practices/sl/form-reuse.texy | 348 ----- .../sl/inject-method-attribute.texy | 61 - .../sl/lets-create-contact-form.texy | 221 ---- best-practices/sl/microsites.texy | 63 - best-practices/sl/pagination.texy | 273 ---- .../sl/passing-settings-to-presenters.texy | 49 - best-practices/sl/post-links.texy | 56 - best-practices/sl/presenter-traits.texy | 47 - best-practices/sl/restore-request.texy | 62 - best-practices/uk/@home.texy | 69 - best-practices/uk/@meta.texy | 2 - best-practices/uk/attribute-requires.texy | 177 --- best-practices/uk/composer.texy | 282 ---- best-practices/uk/creating-editing-form.texy | 205 --- best-practices/uk/dynamic-snippets.texy | 173 --- best-practices/uk/editors-and-tools.texy | 84 -- best-practices/uk/form-reuse.texy | 348 ----- .../uk/inject-method-attribute.texy | 61 - .../uk/lets-create-contact-form.texy | 221 ---- best-practices/uk/microsites.texy | 63 - best-practices/uk/pagination.texy | 273 ---- .../uk/passing-settings-to-presenters.texy | 49 - best-practices/uk/post-links.texy | 56 - best-practices/uk/presenter-traits.texy | 47 - best-practices/uk/restore-request.texy | 62 - bootstrap/bg/@home.texy | 96 -- bootstrap/bg/@meta.texy | 2 - bootstrap/el/@home.texy | 96 -- bootstrap/el/@meta.texy | 2 - bootstrap/hu/@home.texy | 96 -- bootstrap/hu/@meta.texy | 2 - bootstrap/pt/@home.texy | 96 -- bootstrap/pt/@meta.texy | 2 - bootstrap/ro/@home.texy | 96 -- bootstrap/ro/@meta.texy | 2 - bootstrap/sl/@home.texy | 96 -- bootstrap/sl/@meta.texy | 2 - bootstrap/uk/@home.texy | 96 -- bootstrap/uk/@meta.texy | 2 - caching/bg/@home.texy | 484 ------- caching/bg/@meta.texy | 2 - caching/el/@home.texy | 484 ------- caching/el/@meta.texy | 2 - caching/hu/@home.texy | 484 ------- caching/hu/@meta.texy | 2 - caching/pt/@home.texy | 484 ------- caching/pt/@meta.texy | 2 - caching/ro/@home.texy | 484 ------- caching/ro/@meta.texy | 2 - caching/sl/@home.texy | 484 ------- caching/sl/@meta.texy | 2 - caching/uk/@home.texy | 484 ------- caching/uk/@meta.texy | 2 - code-checker/bg/@home.texy | 65 - code-checker/el/@home.texy | 65 - code-checker/hu/@home.texy | 65 - code-checker/pt/@home.texy | 65 - code-checker/ro/@home.texy | 65 - code-checker/sl/@home.texy | 65 - code-checker/uk/@home.texy | 65 - component-model/bg/@home.texy | 67 - component-model/bg/@meta.texy | 2 - component-model/el/@home.texy | 67 - component-model/el/@meta.texy | 2 - component-model/hu/@home.texy | 67 - component-model/hu/@meta.texy | 2 - component-model/pt/@home.texy | 67 - component-model/pt/@meta.texy | 2 - component-model/ro/@home.texy | 67 - component-model/ro/@meta.texy | 2 - component-model/sl/@home.texy | 67 - component-model/sl/@meta.texy | 2 - component-model/uk/@home.texy | 67 - component-model/uk/@meta.texy | 2 - contributing/bg/@home.texy | 17 - contributing/bg/@left-menu.texy | 10 - contributing/bg/code.texy | 118 -- contributing/bg/coding-standard.texy | 128 -- contributing/bg/documentation.texy | 68 - contributing/bg/syntax.texy | 142 --- contributing/el/@home.texy | 17 - contributing/el/@left-menu.texy | 10 - contributing/el/code.texy | 118 -- contributing/el/coding-standard.texy | 128 -- contributing/el/documentation.texy | 68 - contributing/el/syntax.texy | 142 --- contributing/hu/@home.texy | 17 - contributing/hu/@left-menu.texy | 10 - contributing/hu/code.texy | 118 -- contributing/hu/coding-standard.texy | 128 -- contributing/hu/documentation.texy | 68 - contributing/hu/syntax.texy | 142 --- contributing/pt/@home.texy | 17 - contributing/pt/@left-menu.texy | 10 - contributing/pt/code.texy | 118 -- contributing/pt/coding-standard.texy | 128 -- contributing/pt/documentation.texy | 68 - contributing/pt/syntax.texy | 142 --- contributing/ro/@home.texy | 17 - contributing/ro/@left-menu.texy | 10 - contributing/ro/code.texy | 118 -- contributing/ro/coding-standard.texy | 128 -- contributing/ro/documentation.texy | 68 - contributing/ro/syntax.texy | 142 --- contributing/sl/@home.texy | 17 - contributing/sl/@left-menu.texy | 10 - contributing/sl/code.texy | 118 -- contributing/sl/coding-standard.texy | 128 -- contributing/sl/documentation.texy | 68 - contributing/sl/syntax.texy | 142 --- contributing/uk/@home.texy | 17 - contributing/uk/@left-menu.texy | 10 - contributing/uk/code.texy | 118 -- contributing/uk/coding-standard.texy | 128 -- contributing/uk/documentation.texy | 68 - contributing/uk/syntax.texy | 142 --- database/bg/@home.texy | 21 - database/bg/@left-menu.texy | 12 - database/bg/@meta.texy | 1 - database/bg/configuration.texy | 110 -- database/bg/exceptions.texy | 34 - database/bg/explorer.texy | 912 ------------- database/bg/guide.texy | 216 ---- database/bg/mapping.texy | 55 - database/bg/reflection.texy | 125 -- database/bg/security.texy | 185 --- database/bg/sql-way.texy | 513 -------- database/bg/transactions.texy | 43 - database/el/@home.texy | 21 - database/el/@left-menu.texy | 12 - database/el/@meta.texy | 1 - database/el/configuration.texy | 110 -- database/el/exceptions.texy | 34 - database/el/explorer.texy | 912 ------------- database/el/guide.texy | 216 ---- database/el/mapping.texy | 55 - database/el/reflection.texy | 125 -- database/el/security.texy | 185 --- database/el/sql-way.texy | 513 -------- database/el/transactions.texy | 43 - database/hu/@home.texy | 21 - database/hu/@left-menu.texy | 12 - database/hu/@meta.texy | 1 - database/hu/configuration.texy | 110 -- database/hu/exceptions.texy | 34 - database/hu/explorer.texy | 912 ------------- database/hu/guide.texy | 216 ---- database/hu/mapping.texy | 55 - database/hu/reflection.texy | 125 -- database/hu/security.texy | 185 --- database/hu/sql-way.texy | 513 -------- database/hu/transactions.texy | 43 - database/pt/@home.texy | 21 - database/pt/@left-menu.texy | 12 - database/pt/@meta.texy | 1 - database/pt/configuration.texy | 110 -- database/pt/exceptions.texy | 34 - database/pt/explorer.texy | 912 ------------- database/pt/guide.texy | 216 ---- database/pt/mapping.texy | 55 - database/pt/reflection.texy | 125 -- database/pt/security.texy | 185 --- database/pt/sql-way.texy | 513 -------- database/pt/transactions.texy | 43 - database/ro/@home.texy | 21 - database/ro/@left-menu.texy | 12 - database/ro/@meta.texy | 1 - database/ro/configuration.texy | 110 -- database/ro/exceptions.texy | 34 - database/ro/explorer.texy | 912 ------------- database/ro/guide.texy | 216 ---- database/ro/mapping.texy | 55 - database/ro/reflection.texy | 125 -- database/ro/security.texy | 185 --- database/ro/sql-way.texy | 513 -------- database/ro/transactions.texy | 43 - database/sl/@home.texy | 21 - database/sl/@left-menu.texy | 12 - database/sl/@meta.texy | 1 - database/sl/configuration.texy | 110 -- database/sl/exceptions.texy | 34 - database/sl/explorer.texy | 912 ------------- database/sl/guide.texy | 216 ---- database/sl/mapping.texy | 55 - database/sl/reflection.texy | 125 -- database/sl/security.texy | 185 --- database/sl/sql-way.texy | 513 -------- database/sl/transactions.texy | 43 - database/uk/@home.texy | 21 - database/uk/@left-menu.texy | 12 - database/uk/@meta.texy | 1 - database/uk/configuration.texy | 110 -- database/uk/exceptions.texy | 34 - database/uk/explorer.texy | 912 ------------- database/uk/guide.texy | 216 ---- database/uk/mapping.texy | 55 - database/uk/reflection.texy | 125 -- database/uk/security.texy | 185 --- database/uk/sql-way.texy | 513 -------- database/uk/transactions.texy | 43 - dependency-injection/bg/@home.texy | 21 - dependency-injection/bg/@left-menu.texy | 17 - dependency-injection/bg/@meta.texy | 1 - dependency-injection/bg/autowiring.texy | 258 ---- dependency-injection/bg/configuration.texy | 326 ----- dependency-injection/bg/container.texy | 142 --- dependency-injection/bg/extensions.texy | 194 --- dependency-injection/bg/factory.texy | 226 ---- dependency-injection/bg/faq.texy | 106 -- dependency-injection/bg/global-state.texy | 294 ----- dependency-injection/bg/introduction.texy | 526 -------- dependency-injection/bg/nette-container.texy | 80 -- .../bg/passing-dependencies.texy | 215 ---- dependency-injection/bg/services.texy | 458 ------- dependency-injection/el/@home.texy | 21 - dependency-injection/el/@left-menu.texy | 17 - dependency-injection/el/@meta.texy | 1 - dependency-injection/el/autowiring.texy | 258 ---- dependency-injection/el/configuration.texy | 326 ----- dependency-injection/el/container.texy | 142 --- dependency-injection/el/extensions.texy | 194 --- dependency-injection/el/factory.texy | 226 ---- dependency-injection/el/faq.texy | 106 -- dependency-injection/el/global-state.texy | 294 ----- dependency-injection/el/introduction.texy | 526 -------- dependency-injection/el/nette-container.texy | 80 -- .../el/passing-dependencies.texy | 215 ---- dependency-injection/el/services.texy | 458 ------- dependency-injection/hu/@home.texy | 21 - dependency-injection/hu/@left-menu.texy | 17 - dependency-injection/hu/@meta.texy | 1 - dependency-injection/hu/autowiring.texy | 258 ---- dependency-injection/hu/configuration.texy | 326 ----- dependency-injection/hu/container.texy | 142 --- dependency-injection/hu/extensions.texy | 194 --- dependency-injection/hu/factory.texy | 226 ---- dependency-injection/hu/faq.texy | 106 -- dependency-injection/hu/global-state.texy | 294 ----- dependency-injection/hu/introduction.texy | 526 -------- dependency-injection/hu/nette-container.texy | 80 -- .../hu/passing-dependencies.texy | 215 ---- dependency-injection/hu/services.texy | 458 ------- dependency-injection/pt/@home.texy | 21 - dependency-injection/pt/@left-menu.texy | 17 - dependency-injection/pt/@meta.texy | 1 - dependency-injection/pt/autowiring.texy | 258 ---- dependency-injection/pt/configuration.texy | 326 ----- dependency-injection/pt/container.texy | 142 --- dependency-injection/pt/extensions.texy | 194 --- dependency-injection/pt/factory.texy | 226 ---- dependency-injection/pt/faq.texy | 106 -- dependency-injection/pt/global-state.texy | 294 ----- dependency-injection/pt/introduction.texy | 526 -------- dependency-injection/pt/nette-container.texy | 80 -- .../pt/passing-dependencies.texy | 215 ---- dependency-injection/pt/services.texy | 458 ------- dependency-injection/ro/@home.texy | 21 - dependency-injection/ro/@left-menu.texy | 17 - dependency-injection/ro/@meta.texy | 1 - dependency-injection/ro/autowiring.texy | 258 ---- dependency-injection/ro/configuration.texy | 326 ----- dependency-injection/ro/container.texy | 142 --- dependency-injection/ro/extensions.texy | 194 --- dependency-injection/ro/factory.texy | 226 ---- dependency-injection/ro/faq.texy | 106 -- dependency-injection/ro/global-state.texy | 294 ----- dependency-injection/ro/introduction.texy | 526 -------- dependency-injection/ro/nette-container.texy | 80 -- .../ro/passing-dependencies.texy | 215 ---- dependency-injection/ro/services.texy | 458 ------- dependency-injection/sl/@home.texy | 21 - dependency-injection/sl/@left-menu.texy | 17 - dependency-injection/sl/@meta.texy | 1 - dependency-injection/sl/autowiring.texy | 258 ---- dependency-injection/sl/configuration.texy | 326 ----- dependency-injection/sl/container.texy | 142 --- dependency-injection/sl/extensions.texy | 194 --- dependency-injection/sl/factory.texy | 226 ---- dependency-injection/sl/faq.texy | 106 -- dependency-injection/sl/global-state.texy | 294 ----- dependency-injection/sl/introduction.texy | 526 -------- dependency-injection/sl/nette-container.texy | 80 -- .../sl/passing-dependencies.texy | 215 ---- dependency-injection/sl/services.texy | 458 ------- dependency-injection/uk/@home.texy | 21 - dependency-injection/uk/@left-menu.texy | 17 - dependency-injection/uk/@meta.texy | 1 - dependency-injection/uk/autowiring.texy | 258 ---- dependency-injection/uk/configuration.texy | 326 ----- dependency-injection/uk/container.texy | 142 --- dependency-injection/uk/extensions.texy | 194 --- dependency-injection/uk/factory.texy | 226 ---- dependency-injection/uk/faq.texy | 106 -- dependency-injection/uk/global-state.texy | 294 ----- dependency-injection/uk/introduction.texy | 526 -------- dependency-injection/uk/nette-container.texy | 80 -- .../uk/passing-dependencies.texy | 215 ---- dependency-injection/uk/services.texy | 458 ------- forms/bg/@home.texy | 32 - forms/bg/@left-menu.texy | 14 - forms/bg/@meta.texy | 1 - forms/bg/configuration.texy | 61 - forms/bg/controls.texy | 559 -------- forms/bg/in-presenter.texy | 431 ------- forms/bg/rendering.texy | 592 --------- forms/bg/standalone.texy | 317 ----- forms/bg/validation.texy | 376 ------ forms/el/@home.texy | 32 - forms/el/@left-menu.texy | 14 - forms/el/@meta.texy | 1 - forms/el/configuration.texy | 61 - forms/el/controls.texy | 559 -------- forms/el/in-presenter.texy | 431 ------- forms/el/rendering.texy | 592 --------- forms/el/standalone.texy | 317 ----- forms/el/validation.texy | 376 ------ forms/hu/@home.texy | 32 - forms/hu/@left-menu.texy | 14 - forms/hu/@meta.texy | 1 - forms/hu/configuration.texy | 61 - forms/hu/controls.texy | 559 -------- forms/hu/in-presenter.texy | 431 ------- forms/hu/rendering.texy | 592 --------- forms/hu/standalone.texy | 317 ----- forms/hu/validation.texy | 376 ------ forms/pt/@home.texy | 32 - forms/pt/@left-menu.texy | 14 - forms/pt/@meta.texy | 1 - forms/pt/configuration.texy | 61 - forms/pt/controls.texy | 559 -------- forms/pt/in-presenter.texy | 431 ------- forms/pt/rendering.texy | 592 --------- forms/pt/standalone.texy | 317 ----- forms/pt/validation.texy | 376 ------ forms/ro/@home.texy | 32 - forms/ro/@left-menu.texy | 14 - forms/ro/@meta.texy | 1 - forms/ro/configuration.texy | 61 - forms/ro/controls.texy | 559 -------- forms/ro/in-presenter.texy | 431 ------- forms/ro/rendering.texy | 592 --------- forms/ro/standalone.texy | 317 ----- forms/ro/validation.texy | 376 ------ forms/sl/@home.texy | 32 - forms/sl/@left-menu.texy | 14 - forms/sl/@meta.texy | 1 - forms/sl/configuration.texy | 61 - forms/sl/controls.texy | 559 -------- forms/sl/in-presenter.texy | 431 ------- forms/sl/rendering.texy | 592 --------- forms/sl/standalone.texy | 317 ----- forms/sl/validation.texy | 376 ------ forms/uk/@home.texy | 32 - forms/uk/@left-menu.texy | 14 - forms/uk/@meta.texy | 1 - forms/uk/configuration.texy | 61 - forms/uk/controls.texy | 559 -------- forms/uk/in-presenter.texy | 431 ------- forms/uk/rendering.texy | 592 --------- forms/uk/standalone.texy | 317 ----- forms/uk/validation.texy | 376 ------ http/bg/@home.texy | 15 - http/bg/@left-menu.texy | 8 - http/bg/@meta.texy | 1 - http/bg/configuration.texy | 171 --- http/bg/request.texy | 407 ------ http/bg/response.texy | 150 --- http/bg/sessions.texy | 211 --- http/bg/urls.texy | 266 ---- http/el/@home.texy | 15 - http/el/@left-menu.texy | 8 - http/el/@meta.texy | 1 - http/el/configuration.texy | 171 --- http/el/request.texy | 407 ------ http/el/response.texy | 150 --- http/el/sessions.texy | 211 --- http/el/urls.texy | 266 ---- http/hu/@home.texy | 15 - http/hu/@left-menu.texy | 8 - http/hu/@meta.texy | 1 - http/hu/configuration.texy | 171 --- http/hu/request.texy | 407 ------ http/hu/response.texy | 150 --- http/hu/sessions.texy | 211 --- http/hu/urls.texy | 266 ---- http/pt/@home.texy | 15 - http/pt/@left-menu.texy | 8 - http/pt/@meta.texy | 1 - http/pt/configuration.texy | 171 --- http/pt/request.texy | 407 ------ http/pt/response.texy | 150 --- http/pt/sessions.texy | 211 --- http/pt/urls.texy | 266 ---- http/ro/@home.texy | 15 - http/ro/@left-menu.texy | 8 - http/ro/@meta.texy | 1 - http/ro/configuration.texy | 171 --- http/ro/request.texy | 407 ------ http/ro/response.texy | 150 --- http/ro/sessions.texy | 211 --- http/ro/urls.texy | 266 ---- http/sl/@home.texy | 15 - http/sl/@left-menu.texy | 8 - http/sl/@meta.texy | 1 - http/sl/configuration.texy | 171 --- http/sl/request.texy | 407 ------ http/sl/response.texy | 150 --- http/sl/sessions.texy | 211 --- http/sl/urls.texy | 266 ---- http/uk/@home.texy | 15 - http/uk/@left-menu.texy | 8 - http/uk/@meta.texy | 1 - http/uk/configuration.texy | 171 --- http/uk/request.texy | 407 ------ http/uk/response.texy | 150 --- http/uk/sessions.texy | 211 --- http/uk/urls.texy | 266 ---- latte/bg/@home.texy | 2 - latte/bg/@left-menu.texy | 24 - latte/bg/@menu.texy | 12 - latte/bg/@meta.texy | 1 - latte/bg/compiler-passes.texy | 555 -------- latte/bg/cookbook/@home.texy | 13 - latte/bg/cookbook/@meta.texy | 2 - latte/bg/cookbook/grouping.texy | 251 ---- .../how-to-write-sql-queries-in-latte.texy | 40 - latte/bg/cookbook/migration-from-php.texy | 70 - latte/bg/cookbook/migration-from-twig.texy | 79 -- latte/bg/cookbook/passing-variables.texy | 158 --- latte/bg/cookbook/slim-framework.texy | 157 --- latte/bg/custom-filters.texy | 231 ---- latte/bg/custom-functions.texy | 144 --- latte/bg/custom-tags.texy | 1135 ----------------- latte/bg/develop.texy | 355 ------ latte/bg/extending-latte.texy | 227 ---- latte/bg/filters.texy | 873 ------------- latte/bg/functions.texy | 156 --- latte/bg/guide.texy | 45 - latte/bg/loaders.texy | 198 --- latte/bg/recipes.texy | 162 --- latte/bg/safety-first.texy | 383 ------ latte/bg/sandbox.texy | 56 - latte/bg/syntax.texy | 276 ---- latte/bg/tags.texy | 1079 ---------------- latte/bg/template-inheritance.texy | 748 ----------- latte/bg/type-system.texy | 73 -- latte/bg/why-use.texy | 80 -- latte/el/@home.texy | 2 - latte/el/@left-menu.texy | 24 - latte/el/@menu.texy | 12 - latte/el/@meta.texy | 1 - latte/el/compiler-passes.texy | 555 -------- latte/el/cookbook/@home.texy | 13 - latte/el/cookbook/@meta.texy | 2 - latte/el/cookbook/grouping.texy | 251 ---- .../how-to-write-sql-queries-in-latte.texy | 40 - latte/el/cookbook/migration-from-php.texy | 70 - latte/el/cookbook/migration-from-twig.texy | 79 -- latte/el/cookbook/passing-variables.texy | 158 --- latte/el/cookbook/slim-framework.texy | 157 --- latte/el/custom-filters.texy | 231 ---- latte/el/custom-functions.texy | 144 --- latte/el/custom-tags.texy | 1135 ----------------- latte/el/develop.texy | 355 ------ latte/el/extending-latte.texy | 227 ---- latte/el/filters.texy | 873 ------------- latte/el/functions.texy | 156 --- latte/el/guide.texy | 45 - latte/el/loaders.texy | 198 --- latte/el/recipes.texy | 162 --- latte/el/safety-first.texy | 383 ------ latte/el/sandbox.texy | 56 - latte/el/syntax.texy | 276 ---- latte/el/tags.texy | 1079 ---------------- latte/el/template-inheritance.texy | 748 ----------- latte/el/type-system.texy | 73 -- latte/el/why-use.texy | 80 -- latte/hu/@home.texy | 2 - latte/hu/@left-menu.texy | 24 - latte/hu/@menu.texy | 12 - latte/hu/@meta.texy | 1 - latte/hu/compiler-passes.texy | 555 -------- latte/hu/cookbook/@home.texy | 13 - latte/hu/cookbook/@meta.texy | 2 - latte/hu/cookbook/grouping.texy | 251 ---- .../how-to-write-sql-queries-in-latte.texy | 40 - latte/hu/cookbook/migration-from-php.texy | 70 - latte/hu/cookbook/migration-from-twig.texy | 79 -- latte/hu/cookbook/passing-variables.texy | 158 --- latte/hu/cookbook/slim-framework.texy | 157 --- latte/hu/custom-filters.texy | 231 ---- latte/hu/custom-functions.texy | 144 --- latte/hu/custom-tags.texy | 1135 ----------------- latte/hu/develop.texy | 355 ------ latte/hu/extending-latte.texy | 227 ---- latte/hu/filters.texy | 873 ------------- latte/hu/functions.texy | 156 --- latte/hu/guide.texy | 45 - latte/hu/loaders.texy | 198 --- latte/hu/recipes.texy | 162 --- latte/hu/safety-first.texy | 383 ------ latte/hu/sandbox.texy | 56 - latte/hu/syntax.texy | 276 ---- latte/hu/tags.texy | 1079 ---------------- latte/hu/template-inheritance.texy | 748 ----------- latte/hu/type-system.texy | 73 -- latte/hu/why-use.texy | 80 -- latte/pt/@home.texy | 2 - latte/pt/@left-menu.texy | 24 - latte/pt/@menu.texy | 12 - latte/pt/@meta.texy | 1 - latte/pt/compiler-passes.texy | 555 -------- latte/pt/cookbook/@home.texy | 13 - latte/pt/cookbook/@meta.texy | 2 - latte/pt/cookbook/grouping.texy | 251 ---- .../how-to-write-sql-queries-in-latte.texy | 40 - latte/pt/cookbook/migration-from-php.texy | 70 - latte/pt/cookbook/migration-from-twig.texy | 79 -- latte/pt/cookbook/passing-variables.texy | 158 --- latte/pt/cookbook/slim-framework.texy | 157 --- latte/pt/custom-filters.texy | 231 ---- latte/pt/custom-functions.texy | 144 --- latte/pt/custom-tags.texy | 1135 ----------------- latte/pt/develop.texy | 355 ------ latte/pt/extending-latte.texy | 227 ---- latte/pt/filters.texy | 873 ------------- latte/pt/functions.texy | 156 --- latte/pt/guide.texy | 45 - latte/pt/loaders.texy | 198 --- latte/pt/recipes.texy | 162 --- latte/pt/safety-first.texy | 383 ------ latte/pt/sandbox.texy | 56 - latte/pt/syntax.texy | 276 ---- latte/pt/tags.texy | 1079 ---------------- latte/pt/template-inheritance.texy | 748 ----------- latte/pt/type-system.texy | 73 -- latte/pt/why-use.texy | 80 -- latte/ro/@home.texy | 2 - latte/ro/@left-menu.texy | 24 - latte/ro/@menu.texy | 12 - latte/ro/@meta.texy | 1 - latte/ro/compiler-passes.texy | 555 -------- latte/ro/cookbook/@home.texy | 13 - latte/ro/cookbook/@meta.texy | 2 - latte/ro/cookbook/grouping.texy | 251 ---- .../how-to-write-sql-queries-in-latte.texy | 40 - latte/ro/cookbook/migration-from-php.texy | 70 - latte/ro/cookbook/migration-from-twig.texy | 79 -- latte/ro/cookbook/passing-variables.texy | 158 --- latte/ro/cookbook/slim-framework.texy | 157 --- latte/ro/custom-filters.texy | 231 ---- latte/ro/custom-functions.texy | 144 --- latte/ro/custom-tags.texy | 1135 ----------------- latte/ro/develop.texy | 355 ------ latte/ro/extending-latte.texy | 227 ---- latte/ro/filters.texy | 873 ------------- latte/ro/functions.texy | 156 --- latte/ro/guide.texy | 45 - latte/ro/loaders.texy | 198 --- latte/ro/recipes.texy | 162 --- latte/ro/safety-first.texy | 383 ------ latte/ro/sandbox.texy | 56 - latte/ro/syntax.texy | 276 ---- latte/ro/tags.texy | 1079 ---------------- latte/ro/template-inheritance.texy | 748 ----------- latte/ro/type-system.texy | 73 -- latte/ro/why-use.texy | 80 -- latte/sl/@home.texy | 2 - latte/sl/@left-menu.texy | 24 - latte/sl/@menu.texy | 12 - latte/sl/@meta.texy | 1 - latte/sl/compiler-passes.texy | 555 -------- latte/sl/cookbook/@home.texy | 13 - latte/sl/cookbook/@meta.texy | 2 - latte/sl/cookbook/grouping.texy | 251 ---- .../how-to-write-sql-queries-in-latte.texy | 40 - latte/sl/cookbook/migration-from-php.texy | 70 - latte/sl/cookbook/migration-from-twig.texy | 79 -- latte/sl/cookbook/passing-variables.texy | 158 --- latte/sl/cookbook/slim-framework.texy | 157 --- latte/sl/custom-filters.texy | 231 ---- latte/sl/custom-functions.texy | 144 --- latte/sl/custom-tags.texy | 1135 ----------------- latte/sl/develop.texy | 355 ------ latte/sl/extending-latte.texy | 227 ---- latte/sl/filters.texy | 873 ------------- latte/sl/functions.texy | 156 --- latte/sl/guide.texy | 45 - latte/sl/loaders.texy | 198 --- latte/sl/recipes.texy | 162 --- latte/sl/safety-first.texy | 383 ------ latte/sl/sandbox.texy | 56 - latte/sl/syntax.texy | 276 ---- latte/sl/tags.texy | 1079 ---------------- latte/sl/template-inheritance.texy | 748 ----------- latte/sl/type-system.texy | 73 -- latte/sl/why-use.texy | 80 -- latte/uk/@home.texy | 2 - latte/uk/@left-menu.texy | 24 - latte/uk/@menu.texy | 12 - latte/uk/@meta.texy | 1 - latte/uk/compiler-passes.texy | 555 -------- latte/uk/cookbook/@home.texy | 13 - latte/uk/cookbook/@meta.texy | 2 - latte/uk/cookbook/grouping.texy | 251 ---- .../how-to-write-sql-queries-in-latte.texy | 40 - latte/uk/cookbook/migration-from-php.texy | 70 - latte/uk/cookbook/migration-from-twig.texy | 79 -- latte/uk/cookbook/passing-variables.texy | 158 --- latte/uk/cookbook/slim-framework.texy | 157 --- latte/uk/custom-filters.texy | 231 ---- latte/uk/custom-functions.texy | 144 --- latte/uk/custom-tags.texy | 1135 ----------------- latte/uk/develop.texy | 355 ------ latte/uk/extending-latte.texy | 227 ---- latte/uk/filters.texy | 873 ------------- latte/uk/functions.texy | 156 --- latte/uk/guide.texy | 45 - latte/uk/loaders.texy | 198 --- latte/uk/recipes.texy | 162 --- latte/uk/safety-first.texy | 383 ------ latte/uk/sandbox.texy | 56 - latte/uk/syntax.texy | 276 ---- latte/uk/tags.texy | 1079 ---------------- latte/uk/template-inheritance.texy | 748 ----------- latte/uk/type-system.texy | 73 -- latte/uk/why-use.texy | 80 -- mail/bg/@home.texy | 315 ----- mail/bg/@meta.texy | 2 - mail/el/@home.texy | 315 ----- mail/el/@meta.texy | 2 - mail/hu/@home.texy | 315 ----- mail/hu/@meta.texy | 2 - mail/pt/@home.texy | 315 ----- mail/pt/@meta.texy | 2 - mail/ro/@home.texy | 315 ----- mail/ro/@meta.texy | 2 - mail/sl/@home.texy | 315 ----- mail/sl/@meta.texy | 2 - mail/uk/@home.texy | 315 ----- mail/uk/@meta.texy | 2 - neon/bg/@home.texy | 87 -- neon/bg/@meta.texy | 2 - neon/bg/format.texy | 460 ------- neon/el/@home.texy | 87 -- neon/el/@meta.texy | 2 - neon/el/format.texy | 460 ------- neon/hu/@home.texy | 87 -- neon/hu/@meta.texy | 2 - neon/hu/format.texy | 460 ------- neon/pt/@home.texy | 87 -- neon/pt/@meta.texy | 2 - neon/pt/format.texy | 460 ------- neon/ro/@home.texy | 87 -- neon/ro/@meta.texy | 2 - neon/ro/format.texy | 460 ------- neon/sl/@home.texy | 87 -- neon/sl/@meta.texy | 2 - neon/sl/format.texy | 460 ------- neon/uk/@home.texy | 87 -- neon/uk/@meta.texy | 2 - neon/uk/format.texy | 460 ------- nette/bg/@home.texy | 102 -- nette/bg/@menu-topics.texy | 21 - nette/bg/@meta.texy | 1 - nette/bg/configuring.texy | 36 - nette/bg/glossary.texy | 161 --- nette/bg/installation.texy | 67 - ...uction-to-object-oriented-programming.texy | 841 ------------ nette/bg/troubleshooting.texy | 213 ---- nette/bg/vulnerability-protection.texy | 99 -- nette/el/@home.texy | 102 -- nette/el/@menu-topics.texy | 21 - nette/el/@meta.texy | 1 - nette/el/configuring.texy | 36 - nette/el/glossary.texy | 161 --- nette/el/installation.texy | 67 - ...uction-to-object-oriented-programming.texy | 841 ------------ nette/el/troubleshooting.texy | 213 ---- nette/el/vulnerability-protection.texy | 99 -- nette/hu/@home.texy | 102 -- nette/hu/@menu-topics.texy | 21 - nette/hu/@meta.texy | 1 - nette/hu/configuring.texy | 36 - nette/hu/glossary.texy | 161 --- nette/hu/installation.texy | 67 - ...uction-to-object-oriented-programming.texy | 841 ------------ nette/hu/troubleshooting.texy | 213 ---- nette/hu/vulnerability-protection.texy | 99 -- nette/pt/@home.texy | 102 -- nette/pt/@menu-topics.texy | 21 - nette/pt/@meta.texy | 1 - nette/pt/configuring.texy | 36 - nette/pt/glossary.texy | 161 --- nette/pt/installation.texy | 67 - ...uction-to-object-oriented-programming.texy | 841 ------------ nette/pt/troubleshooting.texy | 213 ---- nette/pt/vulnerability-protection.texy | 99 -- nette/ro/@home.texy | 102 -- nette/ro/@menu-topics.texy | 21 - nette/ro/@meta.texy | 1 - nette/ro/configuring.texy | 36 - nette/ro/glossary.texy | 161 --- nette/ro/installation.texy | 67 - ...uction-to-object-oriented-programming.texy | 841 ------------ nette/ro/troubleshooting.texy | 213 ---- nette/ro/vulnerability-protection.texy | 99 -- nette/sl/@home.texy | 102 -- nette/sl/@menu-topics.texy | 21 - nette/sl/@meta.texy | 1 - nette/sl/configuring.texy | 36 - nette/sl/glossary.texy | 161 --- nette/sl/installation.texy | 67 - ...uction-to-object-oriented-programming.texy | 841 ------------ nette/sl/troubleshooting.texy | 213 ---- nette/sl/vulnerability-protection.texy | 99 -- nette/uk/@home.texy | 102 -- nette/uk/@menu-topics.texy | 21 - nette/uk/@meta.texy | 1 - nette/uk/configuring.texy | 36 - nette/uk/glossary.texy | 161 --- nette/uk/installation.texy | 67 - ...uction-to-object-oriented-programming.texy | 841 ------------ nette/uk/troubleshooting.texy | 213 ---- nette/uk/vulnerability-protection.texy | 99 -- php-generator/bg/@home.texy | 973 -------------- php-generator/bg/@meta.texy | 2 - php-generator/el/@home.texy | 973 -------------- php-generator/el/@meta.texy | 2 - php-generator/hu/@home.texy | 973 -------------- php-generator/hu/@meta.texy | 2 - php-generator/pt/@home.texy | 973 -------------- php-generator/pt/@meta.texy | 2 - php-generator/ro/@home.texy | 973 -------------- php-generator/ro/@meta.texy | 2 - php-generator/sl/@home.texy | 973 -------------- php-generator/sl/@meta.texy | 2 - php-generator/uk/@home.texy | 973 -------------- php-generator/uk/@meta.texy | 2 - quickstart/bg/@home.texy | 119 -- quickstart/bg/@left-menu.texy | 9 - quickstart/bg/@meta.texy | 1 - quickstart/bg/authentication.texy | 179 --- quickstart/bg/comments.texy | 171 --- quickstart/bg/creating-posts.texy | 187 --- quickstart/bg/home-page.texy | 201 --- quickstart/bg/model.texy | 84 -- quickstart/bg/single-post.texy | 124 -- quickstart/el/@home.texy | 119 -- quickstart/el/@left-menu.texy | 9 - quickstart/el/@meta.texy | 1 - quickstart/el/authentication.texy | 179 --- quickstart/el/comments.texy | 171 --- quickstart/el/creating-posts.texy | 187 --- quickstart/el/home-page.texy | 201 --- quickstart/el/model.texy | 84 -- quickstart/el/single-post.texy | 124 -- quickstart/hu/@home.texy | 119 -- quickstart/hu/@left-menu.texy | 9 - quickstart/hu/@meta.texy | 1 - quickstart/hu/authentication.texy | 179 --- quickstart/hu/comments.texy | 171 --- quickstart/hu/creating-posts.texy | 187 --- quickstart/hu/home-page.texy | 201 --- quickstart/hu/model.texy | 84 -- quickstart/hu/single-post.texy | 124 -- quickstart/pt/@home.texy | 119 -- quickstart/pt/@left-menu.texy | 9 - quickstart/pt/@meta.texy | 1 - quickstart/pt/authentication.texy | 179 --- quickstart/pt/comments.texy | 171 --- quickstart/pt/creating-posts.texy | 187 --- quickstart/pt/home-page.texy | 201 --- quickstart/pt/model.texy | 84 -- quickstart/pt/single-post.texy | 124 -- quickstart/ro/@home.texy | 119 -- quickstart/ro/@left-menu.texy | 9 - quickstart/ro/@meta.texy | 1 - quickstart/ro/authentication.texy | 179 --- quickstart/ro/comments.texy | 171 --- quickstart/ro/creating-posts.texy | 187 --- quickstart/ro/home-page.texy | 201 --- quickstart/ro/model.texy | 84 -- quickstart/ro/single-post.texy | 124 -- quickstart/sl/@home.texy | 119 -- quickstart/sl/@left-menu.texy | 9 - quickstart/sl/@meta.texy | 1 - quickstart/sl/authentication.texy | 179 --- quickstart/sl/comments.texy | 171 --- quickstart/sl/creating-posts.texy | 187 --- quickstart/sl/home-page.texy | 201 --- quickstart/sl/model.texy | 84 -- quickstart/sl/single-post.texy | 124 -- quickstart/uk/@home.texy | 119 -- quickstart/uk/@left-menu.texy | 9 - quickstart/uk/@meta.texy | 1 - quickstart/uk/authentication.texy | 179 --- quickstart/uk/comments.texy | 171 --- quickstart/uk/creating-posts.texy | 187 --- quickstart/uk/home-page.texy | 201 --- quickstart/uk/model.texy | 84 -- quickstart/uk/single-post.texy | 124 -- robot-loader/bg/@home.texy | 142 --- robot-loader/el/@home.texy | 142 --- robot-loader/hu/@home.texy | 142 --- robot-loader/pt/@home.texy | 142 --- robot-loader/ro/@home.texy | 142 --- robot-loader/sl/@home.texy | 142 --- robot-loader/uk/@home.texy | 142 --- safe-stream/bg/@home.texy | 63 - safe-stream/el/@home.texy | 63 - safe-stream/hu/@home.texy | 63 - safe-stream/pt/@home.texy | 63 - safe-stream/ro/@home.texy | 63 - safe-stream/sl/@home.texy | 63 - safe-stream/uk/@home.texy | 63 - schema/bg/@home.texy | 538 -------- schema/bg/@meta.texy | 2 - schema/el/@home.texy | 538 -------- schema/el/@meta.texy | 2 - schema/hu/@home.texy | 538 -------- schema/hu/@meta.texy | 2 - schema/pt/@home.texy | 538 -------- schema/pt/@meta.texy | 2 - schema/ro/@home.texy | 538 -------- schema/ro/@meta.texy | 2 - schema/sl/@home.texy | 538 -------- schema/sl/@meta.texy | 2 - schema/uk/@home.texy | 538 -------- schema/uk/@meta.texy | 2 - security/bg/@home.texy | 15 - security/bg/@left-menu.texy | 7 - security/bg/@meta.texy | 1 - security/bg/authentication.texy | 289 ----- security/bg/authorization.texy | 292 ----- security/bg/configuration.texy | 85 -- security/bg/passwords.texy | 81 -- security/el/@home.texy | 15 - security/el/@left-menu.texy | 7 - security/el/@meta.texy | 1 - security/el/authentication.texy | 289 ----- security/el/authorization.texy | 292 ----- security/el/configuration.texy | 85 -- security/el/passwords.texy | 81 -- security/hu/@home.texy | 15 - security/hu/@left-menu.texy | 7 - security/hu/@meta.texy | 1 - security/hu/authentication.texy | 289 ----- security/hu/authorization.texy | 292 ----- security/hu/configuration.texy | 85 -- security/hu/passwords.texy | 81 -- security/pt/@home.texy | 15 - security/pt/@left-menu.texy | 7 - security/pt/@meta.texy | 1 - security/pt/authentication.texy | 289 ----- security/pt/authorization.texy | 292 ----- security/pt/configuration.texy | 85 -- security/pt/passwords.texy | 81 -- security/ro/@home.texy | 15 - security/ro/@left-menu.texy | 7 - security/ro/@meta.texy | 1 - security/ro/authentication.texy | 289 ----- security/ro/authorization.texy | 292 ----- security/ro/configuration.texy | 85 -- security/ro/passwords.texy | 81 -- security/sl/@home.texy | 15 - security/sl/@left-menu.texy | 7 - security/sl/@meta.texy | 1 - security/sl/authentication.texy | 289 ----- security/sl/authorization.texy | 292 ----- security/sl/configuration.texy | 85 -- security/sl/passwords.texy | 81 -- security/uk/@home.texy | 15 - security/uk/@left-menu.texy | 7 - security/uk/@meta.texy | 1 - security/uk/authentication.texy | 289 ----- security/uk/authorization.texy | 292 ----- security/uk/configuration.texy | 85 -- security/uk/passwords.texy | 81 -- tester/bg/@home.texy | 2 - tester/bg/@left-menu.texy | 8 - tester/bg/@menu.texy | 3 - tester/bg/@meta.texy | 1 - tester/bg/assertions.texy | 286 ----- tester/bg/guide.texy | 175 --- tester/bg/helpers.texy | 166 --- tester/bg/running-tests.texy | 239 ---- tester/bg/test-annotations.texy | 170 --- tester/bg/testcase.texy | 180 --- tester/bg/writing-tests.texy | 289 ----- tester/el/@home.texy | 2 - tester/el/@left-menu.texy | 8 - tester/el/@menu.texy | 3 - tester/el/@meta.texy | 1 - tester/el/assertions.texy | 286 ----- tester/el/guide.texy | 175 --- tester/el/helpers.texy | 166 --- tester/el/running-tests.texy | 239 ---- tester/el/test-annotations.texy | 170 --- tester/el/testcase.texy | 180 --- tester/el/writing-tests.texy | 289 ----- tester/hu/@home.texy | 2 - tester/hu/@left-menu.texy | 8 - tester/hu/@menu.texy | 3 - tester/hu/@meta.texy | 1 - tester/hu/assertions.texy | 286 ----- tester/hu/guide.texy | 175 --- tester/hu/helpers.texy | 166 --- tester/hu/running-tests.texy | 239 ---- tester/hu/test-annotations.texy | 170 --- tester/hu/testcase.texy | 180 --- tester/hu/writing-tests.texy | 289 ----- tester/pt/@home.texy | 2 - tester/pt/@left-menu.texy | 8 - tester/pt/@menu.texy | 3 - tester/pt/@meta.texy | 1 - tester/pt/assertions.texy | 286 ----- tester/pt/guide.texy | 175 --- tester/pt/helpers.texy | 166 --- tester/pt/running-tests.texy | 239 ---- tester/pt/test-annotations.texy | 170 --- tester/pt/testcase.texy | 180 --- tester/pt/writing-tests.texy | 289 ----- tester/ro/@home.texy | 2 - tester/ro/@left-menu.texy | 8 - tester/ro/@menu.texy | 3 - tester/ro/@meta.texy | 1 - tester/ro/assertions.texy | 286 ----- tester/ro/guide.texy | 175 --- tester/ro/helpers.texy | 166 --- tester/ro/running-tests.texy | 239 ---- tester/ro/test-annotations.texy | 170 --- tester/ro/testcase.texy | 180 --- tester/ro/writing-tests.texy | 289 ----- tester/sl/@home.texy | 2 - tester/sl/@left-menu.texy | 8 - tester/sl/@menu.texy | 3 - tester/sl/@meta.texy | 1 - tester/sl/assertions.texy | 286 ----- tester/sl/guide.texy | 175 --- tester/sl/helpers.texy | 166 --- tester/sl/running-tests.texy | 239 ---- tester/sl/test-annotations.texy | 170 --- tester/sl/testcase.texy | 180 --- tester/sl/writing-tests.texy | 289 ----- tester/uk/@home.texy | 2 - tester/uk/@left-menu.texy | 8 - tester/uk/@menu.texy | 3 - tester/uk/@meta.texy | 1 - tester/uk/assertions.texy | 286 ----- tester/uk/guide.texy | 175 --- tester/uk/helpers.texy | 166 --- tester/uk/running-tests.texy | 239 ---- tester/uk/test-annotations.texy | 170 --- tester/uk/testcase.texy | 180 --- tester/uk/writing-tests.texy | 289 ----- tracy/bg/@home.texy | 2 - tracy/bg/@left-menu.texy | 7 - tracy/bg/@menu.texy | 3 - tracy/bg/@meta.texy | 1 - tracy/bg/configuring.texy | 180 --- tracy/bg/dumper.texy | 48 - tracy/bg/extensions.texy | 122 -- tracy/bg/guide.texy | 217 ---- tracy/bg/open-files-in-ide.texy | 144 --- tracy/bg/recipes.texy | 177 --- tracy/bg/stopwatch.texy | 35 - tracy/el/@home.texy | 2 - tracy/el/@left-menu.texy | 7 - tracy/el/@menu.texy | 3 - tracy/el/@meta.texy | 1 - tracy/el/configuring.texy | 180 --- tracy/el/dumper.texy | 48 - tracy/el/extensions.texy | 122 -- tracy/el/guide.texy | 217 ---- tracy/el/open-files-in-ide.texy | 144 --- tracy/el/recipes.texy | 177 --- tracy/el/stopwatch.texy | 35 - tracy/hu/@home.texy | 2 - tracy/hu/@left-menu.texy | 7 - tracy/hu/@menu.texy | 3 - tracy/hu/@meta.texy | 1 - tracy/hu/configuring.texy | 180 --- tracy/hu/dumper.texy | 48 - tracy/hu/extensions.texy | 122 -- tracy/hu/guide.texy | 217 ---- tracy/hu/open-files-in-ide.texy | 144 --- tracy/hu/recipes.texy | 177 --- tracy/hu/stopwatch.texy | 35 - tracy/pt/@home.texy | 2 - tracy/pt/@left-menu.texy | 7 - tracy/pt/@menu.texy | 3 - tracy/pt/@meta.texy | 1 - tracy/pt/configuring.texy | 180 --- tracy/pt/dumper.texy | 48 - tracy/pt/extensions.texy | 122 -- tracy/pt/guide.texy | 217 ---- tracy/pt/open-files-in-ide.texy | 144 --- tracy/pt/recipes.texy | 177 --- tracy/pt/stopwatch.texy | 35 - tracy/ro/@home.texy | 2 - tracy/ro/@left-menu.texy | 7 - tracy/ro/@menu.texy | 3 - tracy/ro/@meta.texy | 1 - tracy/ro/configuring.texy | 180 --- tracy/ro/dumper.texy | 48 - tracy/ro/extensions.texy | 122 -- tracy/ro/guide.texy | 217 ---- tracy/ro/open-files-in-ide.texy | 144 --- tracy/ro/recipes.texy | 177 --- tracy/ro/stopwatch.texy | 35 - tracy/sl/@home.texy | 2 - tracy/sl/@left-menu.texy | 7 - tracy/sl/@menu.texy | 3 - tracy/sl/@meta.texy | 1 - tracy/sl/configuring.texy | 180 --- tracy/sl/dumper.texy | 48 - tracy/sl/extensions.texy | 122 -- tracy/sl/guide.texy | 217 ---- tracy/sl/open-files-in-ide.texy | 144 --- tracy/sl/recipes.texy | 177 --- tracy/sl/stopwatch.texy | 35 - tracy/uk/@home.texy | 2 - tracy/uk/@left-menu.texy | 7 - tracy/uk/@menu.texy | 3 - tracy/uk/@meta.texy | 1 - tracy/uk/configuring.texy | 180 --- tracy/uk/dumper.texy | 48 - tracy/uk/extensions.texy | 122 -- tracy/uk/guide.texy | 217 ---- tracy/uk/open-files-in-ide.texy | 144 --- tracy/uk/recipes.texy | 177 --- tracy/uk/stopwatch.texy | 35 - utils/bg/@home.texy | 46 - utils/bg/@left-menu.texy | 28 - utils/bg/@meta.texy | 1 - utils/bg/arrays.texy | 561 -------- utils/bg/callback.texy | 81 -- utils/bg/datetime.texy | 74 -- utils/bg/filesystem.texy | 214 ---- utils/bg/finder.texy | 250 ---- utils/bg/floats.texy | 148 --- utils/bg/helpers.texy | 86 -- utils/bg/html-elements.texy | 316 ----- utils/bg/images.texy | 716 ----------- utils/bg/iterables.texy | 170 --- utils/bg/json.texy | 97 -- utils/bg/paginator.texy | 65 - utils/bg/random.texy | 25 - utils/bg/reflection.texy | 134 -- utils/bg/smartobject.texy | 240 ---- utils/bg/staticclass.texy | 21 - utils/bg/strings.texy | 637 --------- utils/bg/type.texy | 199 --- utils/bg/validators.texy | 315 ----- utils/el/@home.texy | 46 - utils/el/@left-menu.texy | 28 - utils/el/@meta.texy | 1 - utils/el/arrays.texy | 561 -------- utils/el/callback.texy | 81 -- utils/el/datetime.texy | 74 -- utils/el/filesystem.texy | 214 ---- utils/el/finder.texy | 250 ---- utils/el/floats.texy | 148 --- utils/el/helpers.texy | 86 -- utils/el/html-elements.texy | 316 ----- utils/el/images.texy | 716 ----------- utils/el/iterables.texy | 170 --- utils/el/json.texy | 97 -- utils/el/paginator.texy | 65 - utils/el/random.texy | 25 - utils/el/reflection.texy | 134 -- utils/el/smartobject.texy | 240 ---- utils/el/staticclass.texy | 21 - utils/el/strings.texy | 637 --------- utils/el/type.texy | 199 --- utils/el/validators.texy | 315 ----- utils/hu/@home.texy | 46 - utils/hu/@left-menu.texy | 28 - utils/hu/@meta.texy | 1 - utils/hu/arrays.texy | 561 -------- utils/hu/callback.texy | 81 -- utils/hu/datetime.texy | 74 -- utils/hu/filesystem.texy | 214 ---- utils/hu/finder.texy | 250 ---- utils/hu/floats.texy | 148 --- utils/hu/helpers.texy | 86 -- utils/hu/html-elements.texy | 316 ----- utils/hu/images.texy | 716 ----------- utils/hu/iterables.texy | 170 --- utils/hu/json.texy | 97 -- utils/hu/paginator.texy | 65 - utils/hu/random.texy | 25 - utils/hu/reflection.texy | 134 -- utils/hu/smartobject.texy | 240 ---- utils/hu/staticclass.texy | 21 - utils/hu/strings.texy | 637 --------- utils/hu/type.texy | 199 --- utils/hu/validators.texy | 315 ----- utils/pt/@home.texy | 46 - utils/pt/@left-menu.texy | 28 - utils/pt/@meta.texy | 1 - utils/pt/arrays.texy | 561 -------- utils/pt/callback.texy | 81 -- utils/pt/datetime.texy | 74 -- utils/pt/filesystem.texy | 214 ---- utils/pt/finder.texy | 250 ---- utils/pt/floats.texy | 148 --- utils/pt/helpers.texy | 86 -- utils/pt/html-elements.texy | 316 ----- utils/pt/images.texy | 716 ----------- utils/pt/iterables.texy | 170 --- utils/pt/json.texy | 97 -- utils/pt/paginator.texy | 65 - utils/pt/random.texy | 25 - utils/pt/reflection.texy | 134 -- utils/pt/smartobject.texy | 240 ---- utils/pt/staticclass.texy | 21 - utils/pt/strings.texy | 637 --------- utils/pt/type.texy | 199 --- utils/pt/validators.texy | 315 ----- utils/ro/@home.texy | 46 - utils/ro/@left-menu.texy | 28 - utils/ro/@meta.texy | 1 - utils/ro/arrays.texy | 561 -------- utils/ro/callback.texy | 81 -- utils/ro/datetime.texy | 74 -- utils/ro/filesystem.texy | 214 ---- utils/ro/finder.texy | 250 ---- utils/ro/floats.texy | 148 --- utils/ro/helpers.texy | 86 -- utils/ro/html-elements.texy | 316 ----- utils/ro/images.texy | 716 ----------- utils/ro/iterables.texy | 170 --- utils/ro/json.texy | 97 -- utils/ro/paginator.texy | 65 - utils/ro/random.texy | 25 - utils/ro/reflection.texy | 134 -- utils/ro/smartobject.texy | 240 ---- utils/ro/staticclass.texy | 21 - utils/ro/strings.texy | 637 --------- utils/ro/type.texy | 199 --- utils/ro/validators.texy | 315 ----- utils/sl/@home.texy | 46 - utils/sl/@left-menu.texy | 28 - utils/sl/@meta.texy | 1 - utils/sl/arrays.texy | 561 -------- utils/sl/callback.texy | 81 -- utils/sl/datetime.texy | 74 -- utils/sl/filesystem.texy | 214 ---- utils/sl/finder.texy | 250 ---- utils/sl/floats.texy | 148 --- utils/sl/helpers.texy | 86 -- utils/sl/html-elements.texy | 316 ----- utils/sl/images.texy | 716 ----------- utils/sl/iterables.texy | 170 --- utils/sl/json.texy | 97 -- utils/sl/paginator.texy | 65 - utils/sl/random.texy | 25 - utils/sl/reflection.texy | 134 -- utils/sl/smartobject.texy | 240 ---- utils/sl/staticclass.texy | 21 - utils/sl/strings.texy | 637 --------- utils/sl/type.texy | 199 --- utils/sl/validators.texy | 315 ----- utils/uk/@home.texy | 46 - utils/uk/@left-menu.texy | 28 - utils/uk/@meta.texy | 1 - utils/uk/arrays.texy | 561 -------- utils/uk/callback.texy | 81 -- utils/uk/datetime.texy | 74 -- utils/uk/filesystem.texy | 214 ---- utils/uk/finder.texy | 250 ---- utils/uk/floats.texy | 148 --- utils/uk/helpers.texy | 86 -- utils/uk/html-elements.texy | 316 ----- utils/uk/images.texy | 716 ----------- utils/uk/iterables.texy | 170 --- utils/uk/json.texy | 97 -- utils/uk/paginator.texy | 65 - utils/uk/random.texy | 25 - utils/uk/reflection.texy | 134 -- utils/uk/smartobject.texy | 240 ---- utils/uk/staticclass.texy | 21 - utils/uk/strings.texy | 637 --------- utils/uk/type.texy | 199 --- utils/uk/validators.texy | 315 ----- www/bg/10-reasons-why-nette.texy | 93 -- www/bg/@home.texy | 2 - www/bg/@menu-common.texy | 22 - www/bg/donate.texy | 20 - www/bg/history.texy | 36 - www/bg/license.texy | 44 - www/bg/maintenance.texy | 27 - www/bg/packages.texy | 27 - www/el/10-reasons-why-nette.texy | 93 -- www/el/@home.texy | 2 - www/el/@menu-common.texy | 22 - www/el/donate.texy | 20 - www/el/history.texy | 36 - www/el/license.texy | 44 - www/el/maintenance.texy | 27 - www/el/packages.texy | 27 - www/hu/10-reasons-why-nette.texy | 93 -- www/hu/@home.texy | 2 - www/hu/@menu-common.texy | 22 - www/hu/donate.texy | 20 - www/hu/history.texy | 36 - www/hu/license.texy | 44 - www/hu/maintenance.texy | 27 - www/hu/packages.texy | 27 - www/pt/10-reasons-why-nette.texy | 93 -- www/pt/@home.texy | 2 - www/pt/@menu-common.texy | 22 - www/pt/donate.texy | 20 - www/pt/history.texy | 36 - www/pt/license.texy | 44 - www/pt/maintenance.texy | 27 - www/pt/packages.texy | 27 - www/ro/10-reasons-why-nette.texy | 93 -- www/ro/@home.texy | 2 - www/ro/@menu-common.texy | 22 - www/ro/donate.texy | 20 - www/ro/history.texy | 36 - www/ro/license.texy | 44 - www/ro/maintenance.texy | 27 - www/ro/packages.texy | 27 - www/sl/10-reasons-why-nette.texy | 93 -- www/sl/@home.texy | 2 - www/sl/@menu-common.texy | 22 - www/sl/donate.texy | 20 - www/sl/history.texy | 36 - www/sl/license.texy | 44 - www/sl/maintenance.texy | 27 - www/sl/packages.texy | 27 - www/uk/10-reasons-why-nette.texy | 93 -- www/uk/@home.texy | 2 - www/uk/@menu-common.texy | 22 - www/uk/donate.texy | 20 - www/uk/history.texy | 36 - www/uk/license.texy | 44 - www/uk/maintenance.texy | 27 - www/uk/packages.texy | 27 - 1463 files changed, 265062 deletions(-) delete mode 100644 application/bg/@home.texy delete mode 100644 application/bg/@left-menu.texy delete mode 100644 application/bg/@meta.texy delete mode 100644 application/bg/ajax.texy delete mode 100644 application/bg/bootstrapping.texy delete mode 100644 application/bg/components.texy delete mode 100644 application/bg/configuration.texy delete mode 100644 application/bg/creating-links.texy delete mode 100644 application/bg/directory-structure.texy delete mode 100644 application/bg/how-it-works.texy delete mode 100644 application/bg/multiplier.texy delete mode 100644 application/bg/presenters.texy delete mode 100644 application/bg/routing.texy delete mode 100644 application/bg/templates.texy delete mode 100644 application/el/@home.texy delete mode 100644 application/el/@left-menu.texy delete mode 100644 application/el/@meta.texy delete mode 100644 application/el/ajax.texy delete mode 100644 application/el/bootstrapping.texy delete mode 100644 application/el/components.texy delete mode 100644 application/el/configuration.texy delete mode 100644 application/el/creating-links.texy delete mode 100644 application/el/directory-structure.texy delete mode 100644 application/el/how-it-works.texy delete mode 100644 application/el/multiplier.texy delete mode 100644 application/el/presenters.texy delete mode 100644 application/el/routing.texy delete mode 100644 application/el/templates.texy delete mode 100644 application/hu/@home.texy delete mode 100644 application/hu/@left-menu.texy delete mode 100644 application/hu/@meta.texy delete mode 100644 application/hu/ajax.texy delete mode 100644 application/hu/bootstrapping.texy delete mode 100644 application/hu/components.texy delete mode 100644 application/hu/configuration.texy delete mode 100644 application/hu/creating-links.texy delete mode 100644 application/hu/directory-structure.texy delete mode 100644 application/hu/how-it-works.texy delete mode 100644 application/hu/multiplier.texy delete mode 100644 application/hu/presenters.texy delete mode 100644 application/hu/routing.texy delete mode 100644 application/hu/templates.texy delete mode 100644 application/pt/@home.texy delete mode 100644 application/pt/@left-menu.texy delete mode 100644 application/pt/@meta.texy delete mode 100644 application/pt/ajax.texy delete mode 100644 application/pt/bootstrapping.texy delete mode 100644 application/pt/components.texy delete mode 100644 application/pt/configuration.texy delete mode 100644 application/pt/creating-links.texy delete mode 100644 application/pt/directory-structure.texy delete mode 100644 application/pt/how-it-works.texy delete mode 100644 application/pt/multiplier.texy delete mode 100644 application/pt/presenters.texy delete mode 100644 application/pt/routing.texy delete mode 100644 application/pt/templates.texy delete mode 100644 application/ro/@home.texy delete mode 100644 application/ro/@left-menu.texy delete mode 100644 application/ro/@meta.texy delete mode 100644 application/ro/ajax.texy delete mode 100644 application/ro/bootstrapping.texy delete mode 100644 application/ro/components.texy delete mode 100644 application/ro/configuration.texy delete mode 100644 application/ro/creating-links.texy delete mode 100644 application/ro/directory-structure.texy delete mode 100644 application/ro/how-it-works.texy delete mode 100644 application/ro/multiplier.texy delete mode 100644 application/ro/presenters.texy delete mode 100644 application/ro/routing.texy delete mode 100644 application/ro/templates.texy delete mode 100644 application/sl/@home.texy delete mode 100644 application/sl/@left-menu.texy delete mode 100644 application/sl/@meta.texy delete mode 100644 application/sl/ajax.texy delete mode 100644 application/sl/bootstrapping.texy delete mode 100644 application/sl/components.texy delete mode 100644 application/sl/configuration.texy delete mode 100644 application/sl/creating-links.texy delete mode 100644 application/sl/directory-structure.texy delete mode 100644 application/sl/how-it-works.texy delete mode 100644 application/sl/multiplier.texy delete mode 100644 application/sl/presenters.texy delete mode 100644 application/sl/routing.texy delete mode 100644 application/sl/templates.texy delete mode 100644 application/uk/@home.texy delete mode 100644 application/uk/@left-menu.texy delete mode 100644 application/uk/@meta.texy delete mode 100644 application/uk/ajax.texy delete mode 100644 application/uk/bootstrapping.texy delete mode 100644 application/uk/components.texy delete mode 100644 application/uk/configuration.texy delete mode 100644 application/uk/creating-links.texy delete mode 100644 application/uk/directory-structure.texy delete mode 100644 application/uk/how-it-works.texy delete mode 100644 application/uk/multiplier.texy delete mode 100644 application/uk/presenters.texy delete mode 100644 application/uk/routing.texy delete mode 100644 application/uk/templates.texy delete mode 100644 assets/bg/@home.texy delete mode 100644 assets/bg/@left-menu.texy delete mode 100644 assets/bg/@meta.texy delete mode 100644 assets/bg/configuration.texy delete mode 100644 assets/bg/vite.texy delete mode 100644 assets/el/@home.texy delete mode 100644 assets/el/@left-menu.texy delete mode 100644 assets/el/@meta.texy delete mode 100644 assets/el/configuration.texy delete mode 100644 assets/el/vite.texy delete mode 100644 assets/hu/@home.texy delete mode 100644 assets/hu/@left-menu.texy delete mode 100644 assets/hu/@meta.texy delete mode 100644 assets/hu/configuration.texy delete mode 100644 assets/hu/vite.texy delete mode 100644 assets/pt/@home.texy delete mode 100644 assets/pt/@left-menu.texy delete mode 100644 assets/pt/@meta.texy delete mode 100644 assets/pt/configuration.texy delete mode 100644 assets/pt/vite.texy delete mode 100644 assets/ro/@home.texy delete mode 100644 assets/ro/@left-menu.texy delete mode 100644 assets/ro/@meta.texy delete mode 100644 assets/ro/configuration.texy delete mode 100644 assets/ro/vite.texy delete mode 100644 assets/sl/@home.texy delete mode 100644 assets/sl/@left-menu.texy delete mode 100644 assets/sl/@meta.texy delete mode 100644 assets/sl/configuration.texy delete mode 100644 assets/sl/vite.texy delete mode 100644 assets/uk/@home.texy delete mode 100644 assets/uk/@left-menu.texy delete mode 100644 assets/uk/@meta.texy delete mode 100644 assets/uk/configuration.texy delete mode 100644 assets/uk/vite.texy delete mode 100644 best-practices/bg/@home.texy delete mode 100644 best-practices/bg/@meta.texy delete mode 100644 best-practices/bg/attribute-requires.texy delete mode 100644 best-practices/bg/composer.texy delete mode 100644 best-practices/bg/creating-editing-form.texy delete mode 100644 best-practices/bg/dynamic-snippets.texy delete mode 100644 best-practices/bg/editors-and-tools.texy delete mode 100644 best-practices/bg/form-reuse.texy delete mode 100644 best-practices/bg/inject-method-attribute.texy delete mode 100644 best-practices/bg/lets-create-contact-form.texy delete mode 100644 best-practices/bg/microsites.texy delete mode 100644 best-practices/bg/pagination.texy delete mode 100644 best-practices/bg/passing-settings-to-presenters.texy delete mode 100644 best-practices/bg/post-links.texy delete mode 100644 best-practices/bg/presenter-traits.texy delete mode 100644 best-practices/bg/restore-request.texy delete mode 100644 best-practices/el/@home.texy delete mode 100644 best-practices/el/@meta.texy delete mode 100644 best-practices/el/attribute-requires.texy delete mode 100644 best-practices/el/composer.texy delete mode 100644 best-practices/el/creating-editing-form.texy delete mode 100644 best-practices/el/dynamic-snippets.texy delete mode 100644 best-practices/el/editors-and-tools.texy delete mode 100644 best-practices/el/form-reuse.texy delete mode 100644 best-practices/el/inject-method-attribute.texy delete mode 100644 best-practices/el/lets-create-contact-form.texy delete mode 100644 best-practices/el/microsites.texy delete mode 100644 best-practices/el/pagination.texy delete mode 100644 best-practices/el/passing-settings-to-presenters.texy delete mode 100644 best-practices/el/post-links.texy delete mode 100644 best-practices/el/presenter-traits.texy delete mode 100644 best-practices/el/restore-request.texy delete mode 100644 best-practices/hu/@home.texy delete mode 100644 best-practices/hu/@meta.texy delete mode 100644 best-practices/hu/attribute-requires.texy delete mode 100644 best-practices/hu/composer.texy delete mode 100644 best-practices/hu/creating-editing-form.texy delete mode 100644 best-practices/hu/dynamic-snippets.texy delete mode 100644 best-practices/hu/editors-and-tools.texy delete mode 100644 best-practices/hu/form-reuse.texy delete mode 100644 best-practices/hu/inject-method-attribute.texy delete mode 100644 best-practices/hu/lets-create-contact-form.texy delete mode 100644 best-practices/hu/microsites.texy delete mode 100644 best-practices/hu/pagination.texy delete mode 100644 best-practices/hu/passing-settings-to-presenters.texy delete mode 100644 best-practices/hu/post-links.texy delete mode 100644 best-practices/hu/presenter-traits.texy delete mode 100644 best-practices/hu/restore-request.texy delete mode 100644 best-practices/pt/@home.texy delete mode 100644 best-practices/pt/@meta.texy delete mode 100644 best-practices/pt/attribute-requires.texy delete mode 100644 best-practices/pt/composer.texy delete mode 100644 best-practices/pt/creating-editing-form.texy delete mode 100644 best-practices/pt/dynamic-snippets.texy delete mode 100644 best-practices/pt/editors-and-tools.texy delete mode 100644 best-practices/pt/form-reuse.texy delete mode 100644 best-practices/pt/inject-method-attribute.texy delete mode 100644 best-practices/pt/lets-create-contact-form.texy delete mode 100644 best-practices/pt/microsites.texy delete mode 100644 best-practices/pt/pagination.texy delete mode 100644 best-practices/pt/passing-settings-to-presenters.texy delete mode 100644 best-practices/pt/post-links.texy delete mode 100644 best-practices/pt/presenter-traits.texy delete mode 100644 best-practices/pt/restore-request.texy delete mode 100644 best-practices/ro/@home.texy delete mode 100644 best-practices/ro/@meta.texy delete mode 100644 best-practices/ro/attribute-requires.texy delete mode 100644 best-practices/ro/composer.texy delete mode 100644 best-practices/ro/creating-editing-form.texy delete mode 100644 best-practices/ro/dynamic-snippets.texy delete mode 100644 best-practices/ro/editors-and-tools.texy delete mode 100644 best-practices/ro/form-reuse.texy delete mode 100644 best-practices/ro/inject-method-attribute.texy delete mode 100644 best-practices/ro/lets-create-contact-form.texy delete mode 100644 best-practices/ro/microsites.texy delete mode 100644 best-practices/ro/pagination.texy delete mode 100644 best-practices/ro/passing-settings-to-presenters.texy delete mode 100644 best-practices/ro/post-links.texy delete mode 100644 best-practices/ro/presenter-traits.texy delete mode 100644 best-practices/ro/restore-request.texy delete mode 100644 best-practices/sl/@home.texy delete mode 100644 best-practices/sl/@meta.texy delete mode 100644 best-practices/sl/attribute-requires.texy delete mode 100644 best-practices/sl/composer.texy delete mode 100644 best-practices/sl/creating-editing-form.texy delete mode 100644 best-practices/sl/dynamic-snippets.texy delete mode 100644 best-practices/sl/editors-and-tools.texy delete mode 100644 best-practices/sl/form-reuse.texy delete mode 100644 best-practices/sl/inject-method-attribute.texy delete mode 100644 best-practices/sl/lets-create-contact-form.texy delete mode 100644 best-practices/sl/microsites.texy delete mode 100644 best-practices/sl/pagination.texy delete mode 100644 best-practices/sl/passing-settings-to-presenters.texy delete mode 100644 best-practices/sl/post-links.texy delete mode 100644 best-practices/sl/presenter-traits.texy delete mode 100644 best-practices/sl/restore-request.texy delete mode 100644 best-practices/uk/@home.texy delete mode 100644 best-practices/uk/@meta.texy delete mode 100644 best-practices/uk/attribute-requires.texy delete mode 100644 best-practices/uk/composer.texy delete mode 100644 best-practices/uk/creating-editing-form.texy delete mode 100644 best-practices/uk/dynamic-snippets.texy delete mode 100644 best-practices/uk/editors-and-tools.texy delete mode 100644 best-practices/uk/form-reuse.texy delete mode 100644 best-practices/uk/inject-method-attribute.texy delete mode 100644 best-practices/uk/lets-create-contact-form.texy delete mode 100644 best-practices/uk/microsites.texy delete mode 100644 best-practices/uk/pagination.texy delete mode 100644 best-practices/uk/passing-settings-to-presenters.texy delete mode 100644 best-practices/uk/post-links.texy delete mode 100644 best-practices/uk/presenter-traits.texy delete mode 100644 best-practices/uk/restore-request.texy delete mode 100644 bootstrap/bg/@home.texy delete mode 100644 bootstrap/bg/@meta.texy delete mode 100644 bootstrap/el/@home.texy delete mode 100644 bootstrap/el/@meta.texy delete mode 100644 bootstrap/hu/@home.texy delete mode 100644 bootstrap/hu/@meta.texy delete mode 100644 bootstrap/pt/@home.texy delete mode 100644 bootstrap/pt/@meta.texy delete mode 100644 bootstrap/ro/@home.texy delete mode 100644 bootstrap/ro/@meta.texy delete mode 100644 bootstrap/sl/@home.texy delete mode 100644 bootstrap/sl/@meta.texy delete mode 100644 bootstrap/uk/@home.texy delete mode 100644 bootstrap/uk/@meta.texy delete mode 100644 caching/bg/@home.texy delete mode 100644 caching/bg/@meta.texy delete mode 100644 caching/el/@home.texy delete mode 100644 caching/el/@meta.texy delete mode 100644 caching/hu/@home.texy delete mode 100644 caching/hu/@meta.texy delete mode 100644 caching/pt/@home.texy delete mode 100644 caching/pt/@meta.texy delete mode 100644 caching/ro/@home.texy delete mode 100644 caching/ro/@meta.texy delete mode 100644 caching/sl/@home.texy delete mode 100644 caching/sl/@meta.texy delete mode 100644 caching/uk/@home.texy delete mode 100644 caching/uk/@meta.texy delete mode 100644 code-checker/bg/@home.texy delete mode 100644 code-checker/el/@home.texy delete mode 100644 code-checker/hu/@home.texy delete mode 100644 code-checker/pt/@home.texy delete mode 100644 code-checker/ro/@home.texy delete mode 100644 code-checker/sl/@home.texy delete mode 100644 code-checker/uk/@home.texy delete mode 100644 component-model/bg/@home.texy delete mode 100644 component-model/bg/@meta.texy delete mode 100644 component-model/el/@home.texy delete mode 100644 component-model/el/@meta.texy delete mode 100644 component-model/hu/@home.texy delete mode 100644 component-model/hu/@meta.texy delete mode 100644 component-model/pt/@home.texy delete mode 100644 component-model/pt/@meta.texy delete mode 100644 component-model/ro/@home.texy delete mode 100644 component-model/ro/@meta.texy delete mode 100644 component-model/sl/@home.texy delete mode 100644 component-model/sl/@meta.texy delete mode 100644 component-model/uk/@home.texy delete mode 100644 component-model/uk/@meta.texy delete mode 100644 contributing/bg/@home.texy delete mode 100644 contributing/bg/@left-menu.texy delete mode 100644 contributing/bg/code.texy delete mode 100644 contributing/bg/coding-standard.texy delete mode 100644 contributing/bg/documentation.texy delete mode 100644 contributing/bg/syntax.texy delete mode 100644 contributing/el/@home.texy delete mode 100644 contributing/el/@left-menu.texy delete mode 100644 contributing/el/code.texy delete mode 100644 contributing/el/coding-standard.texy delete mode 100644 contributing/el/documentation.texy delete mode 100644 contributing/el/syntax.texy delete mode 100644 contributing/hu/@home.texy delete mode 100644 contributing/hu/@left-menu.texy delete mode 100644 contributing/hu/code.texy delete mode 100644 contributing/hu/coding-standard.texy delete mode 100644 contributing/hu/documentation.texy delete mode 100644 contributing/hu/syntax.texy delete mode 100644 contributing/pt/@home.texy delete mode 100644 contributing/pt/@left-menu.texy delete mode 100644 contributing/pt/code.texy delete mode 100644 contributing/pt/coding-standard.texy delete mode 100644 contributing/pt/documentation.texy delete mode 100644 contributing/pt/syntax.texy delete mode 100644 contributing/ro/@home.texy delete mode 100644 contributing/ro/@left-menu.texy delete mode 100644 contributing/ro/code.texy delete mode 100644 contributing/ro/coding-standard.texy delete mode 100644 contributing/ro/documentation.texy delete mode 100644 contributing/ro/syntax.texy delete mode 100644 contributing/sl/@home.texy delete mode 100644 contributing/sl/@left-menu.texy delete mode 100644 contributing/sl/code.texy delete mode 100644 contributing/sl/coding-standard.texy delete mode 100644 contributing/sl/documentation.texy delete mode 100644 contributing/sl/syntax.texy delete mode 100644 contributing/uk/@home.texy delete mode 100644 contributing/uk/@left-menu.texy delete mode 100644 contributing/uk/code.texy delete mode 100644 contributing/uk/coding-standard.texy delete mode 100644 contributing/uk/documentation.texy delete mode 100644 contributing/uk/syntax.texy delete mode 100644 database/bg/@home.texy delete mode 100644 database/bg/@left-menu.texy delete mode 100644 database/bg/@meta.texy delete mode 100644 database/bg/configuration.texy delete mode 100644 database/bg/exceptions.texy delete mode 100644 database/bg/explorer.texy delete mode 100644 database/bg/guide.texy delete mode 100644 database/bg/mapping.texy delete mode 100644 database/bg/reflection.texy delete mode 100644 database/bg/security.texy delete mode 100644 database/bg/sql-way.texy delete mode 100644 database/bg/transactions.texy delete mode 100644 database/el/@home.texy delete mode 100644 database/el/@left-menu.texy delete mode 100644 database/el/@meta.texy delete mode 100644 database/el/configuration.texy delete mode 100644 database/el/exceptions.texy delete mode 100644 database/el/explorer.texy delete mode 100644 database/el/guide.texy delete mode 100644 database/el/mapping.texy delete mode 100644 database/el/reflection.texy delete mode 100644 database/el/security.texy delete mode 100644 database/el/sql-way.texy delete mode 100644 database/el/transactions.texy delete mode 100644 database/hu/@home.texy delete mode 100644 database/hu/@left-menu.texy delete mode 100644 database/hu/@meta.texy delete mode 100644 database/hu/configuration.texy delete mode 100644 database/hu/exceptions.texy delete mode 100644 database/hu/explorer.texy delete mode 100644 database/hu/guide.texy delete mode 100644 database/hu/mapping.texy delete mode 100644 database/hu/reflection.texy delete mode 100644 database/hu/security.texy delete mode 100644 database/hu/sql-way.texy delete mode 100644 database/hu/transactions.texy delete mode 100644 database/pt/@home.texy delete mode 100644 database/pt/@left-menu.texy delete mode 100644 database/pt/@meta.texy delete mode 100644 database/pt/configuration.texy delete mode 100644 database/pt/exceptions.texy delete mode 100644 database/pt/explorer.texy delete mode 100644 database/pt/guide.texy delete mode 100644 database/pt/mapping.texy delete mode 100644 database/pt/reflection.texy delete mode 100644 database/pt/security.texy delete mode 100644 database/pt/sql-way.texy delete mode 100644 database/pt/transactions.texy delete mode 100644 database/ro/@home.texy delete mode 100644 database/ro/@left-menu.texy delete mode 100644 database/ro/@meta.texy delete mode 100644 database/ro/configuration.texy delete mode 100644 database/ro/exceptions.texy delete mode 100644 database/ro/explorer.texy delete mode 100644 database/ro/guide.texy delete mode 100644 database/ro/mapping.texy delete mode 100644 database/ro/reflection.texy delete mode 100644 database/ro/security.texy delete mode 100644 database/ro/sql-way.texy delete mode 100644 database/ro/transactions.texy delete mode 100644 database/sl/@home.texy delete mode 100644 database/sl/@left-menu.texy delete mode 100644 database/sl/@meta.texy delete mode 100644 database/sl/configuration.texy delete mode 100644 database/sl/exceptions.texy delete mode 100644 database/sl/explorer.texy delete mode 100644 database/sl/guide.texy delete mode 100644 database/sl/mapping.texy delete mode 100644 database/sl/reflection.texy delete mode 100644 database/sl/security.texy delete mode 100644 database/sl/sql-way.texy delete mode 100644 database/sl/transactions.texy delete mode 100644 database/uk/@home.texy delete mode 100644 database/uk/@left-menu.texy delete mode 100644 database/uk/@meta.texy delete mode 100644 database/uk/configuration.texy delete mode 100644 database/uk/exceptions.texy delete mode 100644 database/uk/explorer.texy delete mode 100644 database/uk/guide.texy delete mode 100644 database/uk/mapping.texy delete mode 100644 database/uk/reflection.texy delete mode 100644 database/uk/security.texy delete mode 100644 database/uk/sql-way.texy delete mode 100644 database/uk/transactions.texy delete mode 100644 dependency-injection/bg/@home.texy delete mode 100644 dependency-injection/bg/@left-menu.texy delete mode 100644 dependency-injection/bg/@meta.texy delete mode 100644 dependency-injection/bg/autowiring.texy delete mode 100644 dependency-injection/bg/configuration.texy delete mode 100644 dependency-injection/bg/container.texy delete mode 100644 dependency-injection/bg/extensions.texy delete mode 100644 dependency-injection/bg/factory.texy delete mode 100644 dependency-injection/bg/faq.texy delete mode 100644 dependency-injection/bg/global-state.texy delete mode 100644 dependency-injection/bg/introduction.texy delete mode 100644 dependency-injection/bg/nette-container.texy delete mode 100644 dependency-injection/bg/passing-dependencies.texy delete mode 100644 dependency-injection/bg/services.texy delete mode 100644 dependency-injection/el/@home.texy delete mode 100644 dependency-injection/el/@left-menu.texy delete mode 100644 dependency-injection/el/@meta.texy delete mode 100644 dependency-injection/el/autowiring.texy delete mode 100644 dependency-injection/el/configuration.texy delete mode 100644 dependency-injection/el/container.texy delete mode 100644 dependency-injection/el/extensions.texy delete mode 100644 dependency-injection/el/factory.texy delete mode 100644 dependency-injection/el/faq.texy delete mode 100644 dependency-injection/el/global-state.texy delete mode 100644 dependency-injection/el/introduction.texy delete mode 100644 dependency-injection/el/nette-container.texy delete mode 100644 dependency-injection/el/passing-dependencies.texy delete mode 100644 dependency-injection/el/services.texy delete mode 100644 dependency-injection/hu/@home.texy delete mode 100644 dependency-injection/hu/@left-menu.texy delete mode 100644 dependency-injection/hu/@meta.texy delete mode 100644 dependency-injection/hu/autowiring.texy delete mode 100644 dependency-injection/hu/configuration.texy delete mode 100644 dependency-injection/hu/container.texy delete mode 100644 dependency-injection/hu/extensions.texy delete mode 100644 dependency-injection/hu/factory.texy delete mode 100644 dependency-injection/hu/faq.texy delete mode 100644 dependency-injection/hu/global-state.texy delete mode 100644 dependency-injection/hu/introduction.texy delete mode 100644 dependency-injection/hu/nette-container.texy delete mode 100644 dependency-injection/hu/passing-dependencies.texy delete mode 100644 dependency-injection/hu/services.texy delete mode 100644 dependency-injection/pt/@home.texy delete mode 100644 dependency-injection/pt/@left-menu.texy delete mode 100644 dependency-injection/pt/@meta.texy delete mode 100644 dependency-injection/pt/autowiring.texy delete mode 100644 dependency-injection/pt/configuration.texy delete mode 100644 dependency-injection/pt/container.texy delete mode 100644 dependency-injection/pt/extensions.texy delete mode 100644 dependency-injection/pt/factory.texy delete mode 100644 dependency-injection/pt/faq.texy delete mode 100644 dependency-injection/pt/global-state.texy delete mode 100644 dependency-injection/pt/introduction.texy delete mode 100644 dependency-injection/pt/nette-container.texy delete mode 100644 dependency-injection/pt/passing-dependencies.texy delete mode 100644 dependency-injection/pt/services.texy delete mode 100644 dependency-injection/ro/@home.texy delete mode 100644 dependency-injection/ro/@left-menu.texy delete mode 100644 dependency-injection/ro/@meta.texy delete mode 100644 dependency-injection/ro/autowiring.texy delete mode 100644 dependency-injection/ro/configuration.texy delete mode 100644 dependency-injection/ro/container.texy delete mode 100644 dependency-injection/ro/extensions.texy delete mode 100644 dependency-injection/ro/factory.texy delete mode 100644 dependency-injection/ro/faq.texy delete mode 100644 dependency-injection/ro/global-state.texy delete mode 100644 dependency-injection/ro/introduction.texy delete mode 100644 dependency-injection/ro/nette-container.texy delete mode 100644 dependency-injection/ro/passing-dependencies.texy delete mode 100644 dependency-injection/ro/services.texy delete mode 100644 dependency-injection/sl/@home.texy delete mode 100644 dependency-injection/sl/@left-menu.texy delete mode 100644 dependency-injection/sl/@meta.texy delete mode 100644 dependency-injection/sl/autowiring.texy delete mode 100644 dependency-injection/sl/configuration.texy delete mode 100644 dependency-injection/sl/container.texy delete mode 100644 dependency-injection/sl/extensions.texy delete mode 100644 dependency-injection/sl/factory.texy delete mode 100644 dependency-injection/sl/faq.texy delete mode 100644 dependency-injection/sl/global-state.texy delete mode 100644 dependency-injection/sl/introduction.texy delete mode 100644 dependency-injection/sl/nette-container.texy delete mode 100644 dependency-injection/sl/passing-dependencies.texy delete mode 100644 dependency-injection/sl/services.texy delete mode 100644 dependency-injection/uk/@home.texy delete mode 100644 dependency-injection/uk/@left-menu.texy delete mode 100644 dependency-injection/uk/@meta.texy delete mode 100644 dependency-injection/uk/autowiring.texy delete mode 100644 dependency-injection/uk/configuration.texy delete mode 100644 dependency-injection/uk/container.texy delete mode 100644 dependency-injection/uk/extensions.texy delete mode 100644 dependency-injection/uk/factory.texy delete mode 100644 dependency-injection/uk/faq.texy delete mode 100644 dependency-injection/uk/global-state.texy delete mode 100644 dependency-injection/uk/introduction.texy delete mode 100644 dependency-injection/uk/nette-container.texy delete mode 100644 dependency-injection/uk/passing-dependencies.texy delete mode 100644 dependency-injection/uk/services.texy delete mode 100644 forms/bg/@home.texy delete mode 100644 forms/bg/@left-menu.texy delete mode 100644 forms/bg/@meta.texy delete mode 100644 forms/bg/configuration.texy delete mode 100644 forms/bg/controls.texy delete mode 100644 forms/bg/in-presenter.texy delete mode 100644 forms/bg/rendering.texy delete mode 100644 forms/bg/standalone.texy delete mode 100644 forms/bg/validation.texy delete mode 100644 forms/el/@home.texy delete mode 100644 forms/el/@left-menu.texy delete mode 100644 forms/el/@meta.texy delete mode 100644 forms/el/configuration.texy delete mode 100644 forms/el/controls.texy delete mode 100644 forms/el/in-presenter.texy delete mode 100644 forms/el/rendering.texy delete mode 100644 forms/el/standalone.texy delete mode 100644 forms/el/validation.texy delete mode 100644 forms/hu/@home.texy delete mode 100644 forms/hu/@left-menu.texy delete mode 100644 forms/hu/@meta.texy delete mode 100644 forms/hu/configuration.texy delete mode 100644 forms/hu/controls.texy delete mode 100644 forms/hu/in-presenter.texy delete mode 100644 forms/hu/rendering.texy delete mode 100644 forms/hu/standalone.texy delete mode 100644 forms/hu/validation.texy delete mode 100644 forms/pt/@home.texy delete mode 100644 forms/pt/@left-menu.texy delete mode 100644 forms/pt/@meta.texy delete mode 100644 forms/pt/configuration.texy delete mode 100644 forms/pt/controls.texy delete mode 100644 forms/pt/in-presenter.texy delete mode 100644 forms/pt/rendering.texy delete mode 100644 forms/pt/standalone.texy delete mode 100644 forms/pt/validation.texy delete mode 100644 forms/ro/@home.texy delete mode 100644 forms/ro/@left-menu.texy delete mode 100644 forms/ro/@meta.texy delete mode 100644 forms/ro/configuration.texy delete mode 100644 forms/ro/controls.texy delete mode 100644 forms/ro/in-presenter.texy delete mode 100644 forms/ro/rendering.texy delete mode 100644 forms/ro/standalone.texy delete mode 100644 forms/ro/validation.texy delete mode 100644 forms/sl/@home.texy delete mode 100644 forms/sl/@left-menu.texy delete mode 100644 forms/sl/@meta.texy delete mode 100644 forms/sl/configuration.texy delete mode 100644 forms/sl/controls.texy delete mode 100644 forms/sl/in-presenter.texy delete mode 100644 forms/sl/rendering.texy delete mode 100644 forms/sl/standalone.texy delete mode 100644 forms/sl/validation.texy delete mode 100644 forms/uk/@home.texy delete mode 100644 forms/uk/@left-menu.texy delete mode 100644 forms/uk/@meta.texy delete mode 100644 forms/uk/configuration.texy delete mode 100644 forms/uk/controls.texy delete mode 100644 forms/uk/in-presenter.texy delete mode 100644 forms/uk/rendering.texy delete mode 100644 forms/uk/standalone.texy delete mode 100644 forms/uk/validation.texy delete mode 100644 http/bg/@home.texy delete mode 100644 http/bg/@left-menu.texy delete mode 100644 http/bg/@meta.texy delete mode 100644 http/bg/configuration.texy delete mode 100644 http/bg/request.texy delete mode 100644 http/bg/response.texy delete mode 100644 http/bg/sessions.texy delete mode 100644 http/bg/urls.texy delete mode 100644 http/el/@home.texy delete mode 100644 http/el/@left-menu.texy delete mode 100644 http/el/@meta.texy delete mode 100644 http/el/configuration.texy delete mode 100644 http/el/request.texy delete mode 100644 http/el/response.texy delete mode 100644 http/el/sessions.texy delete mode 100644 http/el/urls.texy delete mode 100644 http/hu/@home.texy delete mode 100644 http/hu/@left-menu.texy delete mode 100644 http/hu/@meta.texy delete mode 100644 http/hu/configuration.texy delete mode 100644 http/hu/request.texy delete mode 100644 http/hu/response.texy delete mode 100644 http/hu/sessions.texy delete mode 100644 http/hu/urls.texy delete mode 100644 http/pt/@home.texy delete mode 100644 http/pt/@left-menu.texy delete mode 100644 http/pt/@meta.texy delete mode 100644 http/pt/configuration.texy delete mode 100644 http/pt/request.texy delete mode 100644 http/pt/response.texy delete mode 100644 http/pt/sessions.texy delete mode 100644 http/pt/urls.texy delete mode 100644 http/ro/@home.texy delete mode 100644 http/ro/@left-menu.texy delete mode 100644 http/ro/@meta.texy delete mode 100644 http/ro/configuration.texy delete mode 100644 http/ro/request.texy delete mode 100644 http/ro/response.texy delete mode 100644 http/ro/sessions.texy delete mode 100644 http/ro/urls.texy delete mode 100644 http/sl/@home.texy delete mode 100644 http/sl/@left-menu.texy delete mode 100644 http/sl/@meta.texy delete mode 100644 http/sl/configuration.texy delete mode 100644 http/sl/request.texy delete mode 100644 http/sl/response.texy delete mode 100644 http/sl/sessions.texy delete mode 100644 http/sl/urls.texy delete mode 100644 http/uk/@home.texy delete mode 100644 http/uk/@left-menu.texy delete mode 100644 http/uk/@meta.texy delete mode 100644 http/uk/configuration.texy delete mode 100644 http/uk/request.texy delete mode 100644 http/uk/response.texy delete mode 100644 http/uk/sessions.texy delete mode 100644 http/uk/urls.texy delete mode 100644 latte/bg/@home.texy delete mode 100644 latte/bg/@left-menu.texy delete mode 100644 latte/bg/@menu.texy delete mode 100644 latte/bg/@meta.texy delete mode 100644 latte/bg/compiler-passes.texy delete mode 100644 latte/bg/cookbook/@home.texy delete mode 100644 latte/bg/cookbook/@meta.texy delete mode 100644 latte/bg/cookbook/grouping.texy delete mode 100644 latte/bg/cookbook/how-to-write-sql-queries-in-latte.texy delete mode 100644 latte/bg/cookbook/migration-from-php.texy delete mode 100644 latte/bg/cookbook/migration-from-twig.texy delete mode 100644 latte/bg/cookbook/passing-variables.texy delete mode 100644 latte/bg/cookbook/slim-framework.texy delete mode 100644 latte/bg/custom-filters.texy delete mode 100644 latte/bg/custom-functions.texy delete mode 100644 latte/bg/custom-tags.texy delete mode 100644 latte/bg/develop.texy delete mode 100644 latte/bg/extending-latte.texy delete mode 100644 latte/bg/filters.texy delete mode 100644 latte/bg/functions.texy delete mode 100644 latte/bg/guide.texy delete mode 100644 latte/bg/loaders.texy delete mode 100644 latte/bg/recipes.texy delete mode 100644 latte/bg/safety-first.texy delete mode 100644 latte/bg/sandbox.texy delete mode 100644 latte/bg/syntax.texy delete mode 100644 latte/bg/tags.texy delete mode 100644 latte/bg/template-inheritance.texy delete mode 100644 latte/bg/type-system.texy delete mode 100644 latte/bg/why-use.texy delete mode 100644 latte/el/@home.texy delete mode 100644 latte/el/@left-menu.texy delete mode 100644 latte/el/@menu.texy delete mode 100644 latte/el/@meta.texy delete mode 100644 latte/el/compiler-passes.texy delete mode 100644 latte/el/cookbook/@home.texy delete mode 100644 latte/el/cookbook/@meta.texy delete mode 100644 latte/el/cookbook/grouping.texy delete mode 100644 latte/el/cookbook/how-to-write-sql-queries-in-latte.texy delete mode 100644 latte/el/cookbook/migration-from-php.texy delete mode 100644 latte/el/cookbook/migration-from-twig.texy delete mode 100644 latte/el/cookbook/passing-variables.texy delete mode 100644 latte/el/cookbook/slim-framework.texy delete mode 100644 latte/el/custom-filters.texy delete mode 100644 latte/el/custom-functions.texy delete mode 100644 latte/el/custom-tags.texy delete mode 100644 latte/el/develop.texy delete mode 100644 latte/el/extending-latte.texy delete mode 100644 latte/el/filters.texy delete mode 100644 latte/el/functions.texy delete mode 100644 latte/el/guide.texy delete mode 100644 latte/el/loaders.texy delete mode 100644 latte/el/recipes.texy delete mode 100644 latte/el/safety-first.texy delete mode 100644 latte/el/sandbox.texy delete mode 100644 latte/el/syntax.texy delete mode 100644 latte/el/tags.texy delete mode 100644 latte/el/template-inheritance.texy delete mode 100644 latte/el/type-system.texy delete mode 100644 latte/el/why-use.texy delete mode 100644 latte/hu/@home.texy delete mode 100644 latte/hu/@left-menu.texy delete mode 100644 latte/hu/@menu.texy delete mode 100644 latte/hu/@meta.texy delete mode 100644 latte/hu/compiler-passes.texy delete mode 100644 latte/hu/cookbook/@home.texy delete mode 100644 latte/hu/cookbook/@meta.texy delete mode 100644 latte/hu/cookbook/grouping.texy delete mode 100644 latte/hu/cookbook/how-to-write-sql-queries-in-latte.texy delete mode 100644 latte/hu/cookbook/migration-from-php.texy delete mode 100644 latte/hu/cookbook/migration-from-twig.texy delete mode 100644 latte/hu/cookbook/passing-variables.texy delete mode 100644 latte/hu/cookbook/slim-framework.texy delete mode 100644 latte/hu/custom-filters.texy delete mode 100644 latte/hu/custom-functions.texy delete mode 100644 latte/hu/custom-tags.texy delete mode 100644 latte/hu/develop.texy delete mode 100644 latte/hu/extending-latte.texy delete mode 100644 latte/hu/filters.texy delete mode 100644 latte/hu/functions.texy delete mode 100644 latte/hu/guide.texy delete mode 100644 latte/hu/loaders.texy delete mode 100644 latte/hu/recipes.texy delete mode 100644 latte/hu/safety-first.texy delete mode 100644 latte/hu/sandbox.texy delete mode 100644 latte/hu/syntax.texy delete mode 100644 latte/hu/tags.texy delete mode 100644 latte/hu/template-inheritance.texy delete mode 100644 latte/hu/type-system.texy delete mode 100644 latte/hu/why-use.texy delete mode 100644 latte/pt/@home.texy delete mode 100644 latte/pt/@left-menu.texy delete mode 100644 latte/pt/@menu.texy delete mode 100644 latte/pt/@meta.texy delete mode 100644 latte/pt/compiler-passes.texy delete mode 100644 latte/pt/cookbook/@home.texy delete mode 100644 latte/pt/cookbook/@meta.texy delete mode 100644 latte/pt/cookbook/grouping.texy delete mode 100644 latte/pt/cookbook/how-to-write-sql-queries-in-latte.texy delete mode 100644 latte/pt/cookbook/migration-from-php.texy delete mode 100644 latte/pt/cookbook/migration-from-twig.texy delete mode 100644 latte/pt/cookbook/passing-variables.texy delete mode 100644 latte/pt/cookbook/slim-framework.texy delete mode 100644 latte/pt/custom-filters.texy delete mode 100644 latte/pt/custom-functions.texy delete mode 100644 latte/pt/custom-tags.texy delete mode 100644 latte/pt/develop.texy delete mode 100644 latte/pt/extending-latte.texy delete mode 100644 latte/pt/filters.texy delete mode 100644 latte/pt/functions.texy delete mode 100644 latte/pt/guide.texy delete mode 100644 latte/pt/loaders.texy delete mode 100644 latte/pt/recipes.texy delete mode 100644 latte/pt/safety-first.texy delete mode 100644 latte/pt/sandbox.texy delete mode 100644 latte/pt/syntax.texy delete mode 100644 latte/pt/tags.texy delete mode 100644 latte/pt/template-inheritance.texy delete mode 100644 latte/pt/type-system.texy delete mode 100644 latte/pt/why-use.texy delete mode 100644 latte/ro/@home.texy delete mode 100644 latte/ro/@left-menu.texy delete mode 100644 latte/ro/@menu.texy delete mode 100644 latte/ro/@meta.texy delete mode 100644 latte/ro/compiler-passes.texy delete mode 100644 latte/ro/cookbook/@home.texy delete mode 100644 latte/ro/cookbook/@meta.texy delete mode 100644 latte/ro/cookbook/grouping.texy delete mode 100644 latte/ro/cookbook/how-to-write-sql-queries-in-latte.texy delete mode 100644 latte/ro/cookbook/migration-from-php.texy delete mode 100644 latte/ro/cookbook/migration-from-twig.texy delete mode 100644 latte/ro/cookbook/passing-variables.texy delete mode 100644 latte/ro/cookbook/slim-framework.texy delete mode 100644 latte/ro/custom-filters.texy delete mode 100644 latte/ro/custom-functions.texy delete mode 100644 latte/ro/custom-tags.texy delete mode 100644 latte/ro/develop.texy delete mode 100644 latte/ro/extending-latte.texy delete mode 100644 latte/ro/filters.texy delete mode 100644 latte/ro/functions.texy delete mode 100644 latte/ro/guide.texy delete mode 100644 latte/ro/loaders.texy delete mode 100644 latte/ro/recipes.texy delete mode 100644 latte/ro/safety-first.texy delete mode 100644 latte/ro/sandbox.texy delete mode 100644 latte/ro/syntax.texy delete mode 100644 latte/ro/tags.texy delete mode 100644 latte/ro/template-inheritance.texy delete mode 100644 latte/ro/type-system.texy delete mode 100644 latte/ro/why-use.texy delete mode 100644 latte/sl/@home.texy delete mode 100644 latte/sl/@left-menu.texy delete mode 100644 latte/sl/@menu.texy delete mode 100644 latte/sl/@meta.texy delete mode 100644 latte/sl/compiler-passes.texy delete mode 100644 latte/sl/cookbook/@home.texy delete mode 100644 latte/sl/cookbook/@meta.texy delete mode 100644 latte/sl/cookbook/grouping.texy delete mode 100644 latte/sl/cookbook/how-to-write-sql-queries-in-latte.texy delete mode 100644 latte/sl/cookbook/migration-from-php.texy delete mode 100644 latte/sl/cookbook/migration-from-twig.texy delete mode 100644 latte/sl/cookbook/passing-variables.texy delete mode 100644 latte/sl/cookbook/slim-framework.texy delete mode 100644 latte/sl/custom-filters.texy delete mode 100644 latte/sl/custom-functions.texy delete mode 100644 latte/sl/custom-tags.texy delete mode 100644 latte/sl/develop.texy delete mode 100644 latte/sl/extending-latte.texy delete mode 100644 latte/sl/filters.texy delete mode 100644 latte/sl/functions.texy delete mode 100644 latte/sl/guide.texy delete mode 100644 latte/sl/loaders.texy delete mode 100644 latte/sl/recipes.texy delete mode 100644 latte/sl/safety-first.texy delete mode 100644 latte/sl/sandbox.texy delete mode 100644 latte/sl/syntax.texy delete mode 100644 latte/sl/tags.texy delete mode 100644 latte/sl/template-inheritance.texy delete mode 100644 latte/sl/type-system.texy delete mode 100644 latte/sl/why-use.texy delete mode 100644 latte/uk/@home.texy delete mode 100644 latte/uk/@left-menu.texy delete mode 100644 latte/uk/@menu.texy delete mode 100644 latte/uk/@meta.texy delete mode 100644 latte/uk/compiler-passes.texy delete mode 100644 latte/uk/cookbook/@home.texy delete mode 100644 latte/uk/cookbook/@meta.texy delete mode 100644 latte/uk/cookbook/grouping.texy delete mode 100644 latte/uk/cookbook/how-to-write-sql-queries-in-latte.texy delete mode 100644 latte/uk/cookbook/migration-from-php.texy delete mode 100644 latte/uk/cookbook/migration-from-twig.texy delete mode 100644 latte/uk/cookbook/passing-variables.texy delete mode 100644 latte/uk/cookbook/slim-framework.texy delete mode 100644 latte/uk/custom-filters.texy delete mode 100644 latte/uk/custom-functions.texy delete mode 100644 latte/uk/custom-tags.texy delete mode 100644 latte/uk/develop.texy delete mode 100644 latte/uk/extending-latte.texy delete mode 100644 latte/uk/filters.texy delete mode 100644 latte/uk/functions.texy delete mode 100644 latte/uk/guide.texy delete mode 100644 latte/uk/loaders.texy delete mode 100644 latte/uk/recipes.texy delete mode 100644 latte/uk/safety-first.texy delete mode 100644 latte/uk/sandbox.texy delete mode 100644 latte/uk/syntax.texy delete mode 100644 latte/uk/tags.texy delete mode 100644 latte/uk/template-inheritance.texy delete mode 100644 latte/uk/type-system.texy delete mode 100644 latte/uk/why-use.texy delete mode 100644 mail/bg/@home.texy delete mode 100644 mail/bg/@meta.texy delete mode 100644 mail/el/@home.texy delete mode 100644 mail/el/@meta.texy delete mode 100644 mail/hu/@home.texy delete mode 100644 mail/hu/@meta.texy delete mode 100644 mail/pt/@home.texy delete mode 100644 mail/pt/@meta.texy delete mode 100644 mail/ro/@home.texy delete mode 100644 mail/ro/@meta.texy delete mode 100644 mail/sl/@home.texy delete mode 100644 mail/sl/@meta.texy delete mode 100644 mail/uk/@home.texy delete mode 100644 mail/uk/@meta.texy delete mode 100644 neon/bg/@home.texy delete mode 100644 neon/bg/@meta.texy delete mode 100644 neon/bg/format.texy delete mode 100644 neon/el/@home.texy delete mode 100644 neon/el/@meta.texy delete mode 100644 neon/el/format.texy delete mode 100644 neon/hu/@home.texy delete mode 100644 neon/hu/@meta.texy delete mode 100644 neon/hu/format.texy delete mode 100644 neon/pt/@home.texy delete mode 100644 neon/pt/@meta.texy delete mode 100644 neon/pt/format.texy delete mode 100644 neon/ro/@home.texy delete mode 100644 neon/ro/@meta.texy delete mode 100644 neon/ro/format.texy delete mode 100644 neon/sl/@home.texy delete mode 100644 neon/sl/@meta.texy delete mode 100644 neon/sl/format.texy delete mode 100644 neon/uk/@home.texy delete mode 100644 neon/uk/@meta.texy delete mode 100644 neon/uk/format.texy delete mode 100644 nette/bg/@home.texy delete mode 100644 nette/bg/@menu-topics.texy delete mode 100644 nette/bg/@meta.texy delete mode 100644 nette/bg/configuring.texy delete mode 100644 nette/bg/glossary.texy delete mode 100644 nette/bg/installation.texy delete mode 100644 nette/bg/introduction-to-object-oriented-programming.texy delete mode 100644 nette/bg/troubleshooting.texy delete mode 100644 nette/bg/vulnerability-protection.texy delete mode 100644 nette/el/@home.texy delete mode 100644 nette/el/@menu-topics.texy delete mode 100644 nette/el/@meta.texy delete mode 100644 nette/el/configuring.texy delete mode 100644 nette/el/glossary.texy delete mode 100644 nette/el/installation.texy delete mode 100644 nette/el/introduction-to-object-oriented-programming.texy delete mode 100644 nette/el/troubleshooting.texy delete mode 100644 nette/el/vulnerability-protection.texy delete mode 100644 nette/hu/@home.texy delete mode 100644 nette/hu/@menu-topics.texy delete mode 100644 nette/hu/@meta.texy delete mode 100644 nette/hu/configuring.texy delete mode 100644 nette/hu/glossary.texy delete mode 100644 nette/hu/installation.texy delete mode 100644 nette/hu/introduction-to-object-oriented-programming.texy delete mode 100644 nette/hu/troubleshooting.texy delete mode 100644 nette/hu/vulnerability-protection.texy delete mode 100644 nette/pt/@home.texy delete mode 100644 nette/pt/@menu-topics.texy delete mode 100644 nette/pt/@meta.texy delete mode 100644 nette/pt/configuring.texy delete mode 100644 nette/pt/glossary.texy delete mode 100644 nette/pt/installation.texy delete mode 100644 nette/pt/introduction-to-object-oriented-programming.texy delete mode 100644 nette/pt/troubleshooting.texy delete mode 100644 nette/pt/vulnerability-protection.texy delete mode 100644 nette/ro/@home.texy delete mode 100644 nette/ro/@menu-topics.texy delete mode 100644 nette/ro/@meta.texy delete mode 100644 nette/ro/configuring.texy delete mode 100644 nette/ro/glossary.texy delete mode 100644 nette/ro/installation.texy delete mode 100644 nette/ro/introduction-to-object-oriented-programming.texy delete mode 100644 nette/ro/troubleshooting.texy delete mode 100644 nette/ro/vulnerability-protection.texy delete mode 100644 nette/sl/@home.texy delete mode 100644 nette/sl/@menu-topics.texy delete mode 100644 nette/sl/@meta.texy delete mode 100644 nette/sl/configuring.texy delete mode 100644 nette/sl/glossary.texy delete mode 100644 nette/sl/installation.texy delete mode 100644 nette/sl/introduction-to-object-oriented-programming.texy delete mode 100644 nette/sl/troubleshooting.texy delete mode 100644 nette/sl/vulnerability-protection.texy delete mode 100644 nette/uk/@home.texy delete mode 100644 nette/uk/@menu-topics.texy delete mode 100644 nette/uk/@meta.texy delete mode 100644 nette/uk/configuring.texy delete mode 100644 nette/uk/glossary.texy delete mode 100644 nette/uk/installation.texy delete mode 100644 nette/uk/introduction-to-object-oriented-programming.texy delete mode 100644 nette/uk/troubleshooting.texy delete mode 100644 nette/uk/vulnerability-protection.texy delete mode 100644 php-generator/bg/@home.texy delete mode 100644 php-generator/bg/@meta.texy delete mode 100644 php-generator/el/@home.texy delete mode 100644 php-generator/el/@meta.texy delete mode 100644 php-generator/hu/@home.texy delete mode 100644 php-generator/hu/@meta.texy delete mode 100644 php-generator/pt/@home.texy delete mode 100644 php-generator/pt/@meta.texy delete mode 100644 php-generator/ro/@home.texy delete mode 100644 php-generator/ro/@meta.texy delete mode 100644 php-generator/sl/@home.texy delete mode 100644 php-generator/sl/@meta.texy delete mode 100644 php-generator/uk/@home.texy delete mode 100644 php-generator/uk/@meta.texy delete mode 100644 quickstart/bg/@home.texy delete mode 100644 quickstart/bg/@left-menu.texy delete mode 100644 quickstart/bg/@meta.texy delete mode 100644 quickstart/bg/authentication.texy delete mode 100644 quickstart/bg/comments.texy delete mode 100644 quickstart/bg/creating-posts.texy delete mode 100644 quickstart/bg/home-page.texy delete mode 100644 quickstart/bg/model.texy delete mode 100644 quickstart/bg/single-post.texy delete mode 100644 quickstart/el/@home.texy delete mode 100644 quickstart/el/@left-menu.texy delete mode 100644 quickstart/el/@meta.texy delete mode 100644 quickstart/el/authentication.texy delete mode 100644 quickstart/el/comments.texy delete mode 100644 quickstart/el/creating-posts.texy delete mode 100644 quickstart/el/home-page.texy delete mode 100644 quickstart/el/model.texy delete mode 100644 quickstart/el/single-post.texy delete mode 100644 quickstart/hu/@home.texy delete mode 100644 quickstart/hu/@left-menu.texy delete mode 100644 quickstart/hu/@meta.texy delete mode 100644 quickstart/hu/authentication.texy delete mode 100644 quickstart/hu/comments.texy delete mode 100644 quickstart/hu/creating-posts.texy delete mode 100644 quickstart/hu/home-page.texy delete mode 100644 quickstart/hu/model.texy delete mode 100644 quickstart/hu/single-post.texy delete mode 100644 quickstart/pt/@home.texy delete mode 100644 quickstart/pt/@left-menu.texy delete mode 100644 quickstart/pt/@meta.texy delete mode 100644 quickstart/pt/authentication.texy delete mode 100644 quickstart/pt/comments.texy delete mode 100644 quickstart/pt/creating-posts.texy delete mode 100644 quickstart/pt/home-page.texy delete mode 100644 quickstart/pt/model.texy delete mode 100644 quickstart/pt/single-post.texy delete mode 100644 quickstart/ro/@home.texy delete mode 100644 quickstart/ro/@left-menu.texy delete mode 100644 quickstart/ro/@meta.texy delete mode 100644 quickstart/ro/authentication.texy delete mode 100644 quickstart/ro/comments.texy delete mode 100644 quickstart/ro/creating-posts.texy delete mode 100644 quickstart/ro/home-page.texy delete mode 100644 quickstart/ro/model.texy delete mode 100644 quickstart/ro/single-post.texy delete mode 100644 quickstart/sl/@home.texy delete mode 100644 quickstart/sl/@left-menu.texy delete mode 100644 quickstart/sl/@meta.texy delete mode 100644 quickstart/sl/authentication.texy delete mode 100644 quickstart/sl/comments.texy delete mode 100644 quickstart/sl/creating-posts.texy delete mode 100644 quickstart/sl/home-page.texy delete mode 100644 quickstart/sl/model.texy delete mode 100644 quickstart/sl/single-post.texy delete mode 100644 quickstart/uk/@home.texy delete mode 100644 quickstart/uk/@left-menu.texy delete mode 100644 quickstart/uk/@meta.texy delete mode 100644 quickstart/uk/authentication.texy delete mode 100644 quickstart/uk/comments.texy delete mode 100644 quickstart/uk/creating-posts.texy delete mode 100644 quickstart/uk/home-page.texy delete mode 100644 quickstart/uk/model.texy delete mode 100644 quickstart/uk/single-post.texy delete mode 100644 robot-loader/bg/@home.texy delete mode 100644 robot-loader/el/@home.texy delete mode 100644 robot-loader/hu/@home.texy delete mode 100644 robot-loader/pt/@home.texy delete mode 100644 robot-loader/ro/@home.texy delete mode 100644 robot-loader/sl/@home.texy delete mode 100644 robot-loader/uk/@home.texy delete mode 100644 safe-stream/bg/@home.texy delete mode 100644 safe-stream/el/@home.texy delete mode 100644 safe-stream/hu/@home.texy delete mode 100644 safe-stream/pt/@home.texy delete mode 100644 safe-stream/ro/@home.texy delete mode 100644 safe-stream/sl/@home.texy delete mode 100644 safe-stream/uk/@home.texy delete mode 100644 schema/bg/@home.texy delete mode 100644 schema/bg/@meta.texy delete mode 100644 schema/el/@home.texy delete mode 100644 schema/el/@meta.texy delete mode 100644 schema/hu/@home.texy delete mode 100644 schema/hu/@meta.texy delete mode 100644 schema/pt/@home.texy delete mode 100644 schema/pt/@meta.texy delete mode 100644 schema/ro/@home.texy delete mode 100644 schema/ro/@meta.texy delete mode 100644 schema/sl/@home.texy delete mode 100644 schema/sl/@meta.texy delete mode 100644 schema/uk/@home.texy delete mode 100644 schema/uk/@meta.texy delete mode 100644 security/bg/@home.texy delete mode 100644 security/bg/@left-menu.texy delete mode 100644 security/bg/@meta.texy delete mode 100644 security/bg/authentication.texy delete mode 100644 security/bg/authorization.texy delete mode 100644 security/bg/configuration.texy delete mode 100644 security/bg/passwords.texy delete mode 100644 security/el/@home.texy delete mode 100644 security/el/@left-menu.texy delete mode 100644 security/el/@meta.texy delete mode 100644 security/el/authentication.texy delete mode 100644 security/el/authorization.texy delete mode 100644 security/el/configuration.texy delete mode 100644 security/el/passwords.texy delete mode 100644 security/hu/@home.texy delete mode 100644 security/hu/@left-menu.texy delete mode 100644 security/hu/@meta.texy delete mode 100644 security/hu/authentication.texy delete mode 100644 security/hu/authorization.texy delete mode 100644 security/hu/configuration.texy delete mode 100644 security/hu/passwords.texy delete mode 100644 security/pt/@home.texy delete mode 100644 security/pt/@left-menu.texy delete mode 100644 security/pt/@meta.texy delete mode 100644 security/pt/authentication.texy delete mode 100644 security/pt/authorization.texy delete mode 100644 security/pt/configuration.texy delete mode 100644 security/pt/passwords.texy delete mode 100644 security/ro/@home.texy delete mode 100644 security/ro/@left-menu.texy delete mode 100644 security/ro/@meta.texy delete mode 100644 security/ro/authentication.texy delete mode 100644 security/ro/authorization.texy delete mode 100644 security/ro/configuration.texy delete mode 100644 security/ro/passwords.texy delete mode 100644 security/sl/@home.texy delete mode 100644 security/sl/@left-menu.texy delete mode 100644 security/sl/@meta.texy delete mode 100644 security/sl/authentication.texy delete mode 100644 security/sl/authorization.texy delete mode 100644 security/sl/configuration.texy delete mode 100644 security/sl/passwords.texy delete mode 100644 security/uk/@home.texy delete mode 100644 security/uk/@left-menu.texy delete mode 100644 security/uk/@meta.texy delete mode 100644 security/uk/authentication.texy delete mode 100644 security/uk/authorization.texy delete mode 100644 security/uk/configuration.texy delete mode 100644 security/uk/passwords.texy delete mode 100644 tester/bg/@home.texy delete mode 100644 tester/bg/@left-menu.texy delete mode 100644 tester/bg/@menu.texy delete mode 100644 tester/bg/@meta.texy delete mode 100644 tester/bg/assertions.texy delete mode 100644 tester/bg/guide.texy delete mode 100644 tester/bg/helpers.texy delete mode 100644 tester/bg/running-tests.texy delete mode 100644 tester/bg/test-annotations.texy delete mode 100644 tester/bg/testcase.texy delete mode 100644 tester/bg/writing-tests.texy delete mode 100644 tester/el/@home.texy delete mode 100644 tester/el/@left-menu.texy delete mode 100644 tester/el/@menu.texy delete mode 100644 tester/el/@meta.texy delete mode 100644 tester/el/assertions.texy delete mode 100644 tester/el/guide.texy delete mode 100644 tester/el/helpers.texy delete mode 100644 tester/el/running-tests.texy delete mode 100644 tester/el/test-annotations.texy delete mode 100644 tester/el/testcase.texy delete mode 100644 tester/el/writing-tests.texy delete mode 100644 tester/hu/@home.texy delete mode 100644 tester/hu/@left-menu.texy delete mode 100644 tester/hu/@menu.texy delete mode 100644 tester/hu/@meta.texy delete mode 100644 tester/hu/assertions.texy delete mode 100644 tester/hu/guide.texy delete mode 100644 tester/hu/helpers.texy delete mode 100644 tester/hu/running-tests.texy delete mode 100644 tester/hu/test-annotations.texy delete mode 100644 tester/hu/testcase.texy delete mode 100644 tester/hu/writing-tests.texy delete mode 100644 tester/pt/@home.texy delete mode 100644 tester/pt/@left-menu.texy delete mode 100644 tester/pt/@menu.texy delete mode 100644 tester/pt/@meta.texy delete mode 100644 tester/pt/assertions.texy delete mode 100644 tester/pt/guide.texy delete mode 100644 tester/pt/helpers.texy delete mode 100644 tester/pt/running-tests.texy delete mode 100644 tester/pt/test-annotations.texy delete mode 100644 tester/pt/testcase.texy delete mode 100644 tester/pt/writing-tests.texy delete mode 100644 tester/ro/@home.texy delete mode 100644 tester/ro/@left-menu.texy delete mode 100644 tester/ro/@menu.texy delete mode 100644 tester/ro/@meta.texy delete mode 100644 tester/ro/assertions.texy delete mode 100644 tester/ro/guide.texy delete mode 100644 tester/ro/helpers.texy delete mode 100644 tester/ro/running-tests.texy delete mode 100644 tester/ro/test-annotations.texy delete mode 100644 tester/ro/testcase.texy delete mode 100644 tester/ro/writing-tests.texy delete mode 100644 tester/sl/@home.texy delete mode 100644 tester/sl/@left-menu.texy delete mode 100644 tester/sl/@menu.texy delete mode 100644 tester/sl/@meta.texy delete mode 100644 tester/sl/assertions.texy delete mode 100644 tester/sl/guide.texy delete mode 100644 tester/sl/helpers.texy delete mode 100644 tester/sl/running-tests.texy delete mode 100644 tester/sl/test-annotations.texy delete mode 100644 tester/sl/testcase.texy delete mode 100644 tester/sl/writing-tests.texy delete mode 100644 tester/uk/@home.texy delete mode 100644 tester/uk/@left-menu.texy delete mode 100644 tester/uk/@menu.texy delete mode 100644 tester/uk/@meta.texy delete mode 100644 tester/uk/assertions.texy delete mode 100644 tester/uk/guide.texy delete mode 100644 tester/uk/helpers.texy delete mode 100644 tester/uk/running-tests.texy delete mode 100644 tester/uk/test-annotations.texy delete mode 100644 tester/uk/testcase.texy delete mode 100644 tester/uk/writing-tests.texy delete mode 100644 tracy/bg/@home.texy delete mode 100644 tracy/bg/@left-menu.texy delete mode 100644 tracy/bg/@menu.texy delete mode 100644 tracy/bg/@meta.texy delete mode 100644 tracy/bg/configuring.texy delete mode 100644 tracy/bg/dumper.texy delete mode 100644 tracy/bg/extensions.texy delete mode 100644 tracy/bg/guide.texy delete mode 100644 tracy/bg/open-files-in-ide.texy delete mode 100644 tracy/bg/recipes.texy delete mode 100644 tracy/bg/stopwatch.texy delete mode 100644 tracy/el/@home.texy delete mode 100644 tracy/el/@left-menu.texy delete mode 100644 tracy/el/@menu.texy delete mode 100644 tracy/el/@meta.texy delete mode 100644 tracy/el/configuring.texy delete mode 100644 tracy/el/dumper.texy delete mode 100644 tracy/el/extensions.texy delete mode 100644 tracy/el/guide.texy delete mode 100644 tracy/el/open-files-in-ide.texy delete mode 100644 tracy/el/recipes.texy delete mode 100644 tracy/el/stopwatch.texy delete mode 100644 tracy/hu/@home.texy delete mode 100644 tracy/hu/@left-menu.texy delete mode 100644 tracy/hu/@menu.texy delete mode 100644 tracy/hu/@meta.texy delete mode 100644 tracy/hu/configuring.texy delete mode 100644 tracy/hu/dumper.texy delete mode 100644 tracy/hu/extensions.texy delete mode 100644 tracy/hu/guide.texy delete mode 100644 tracy/hu/open-files-in-ide.texy delete mode 100644 tracy/hu/recipes.texy delete mode 100644 tracy/hu/stopwatch.texy delete mode 100644 tracy/pt/@home.texy delete mode 100644 tracy/pt/@left-menu.texy delete mode 100644 tracy/pt/@menu.texy delete mode 100644 tracy/pt/@meta.texy delete mode 100644 tracy/pt/configuring.texy delete mode 100644 tracy/pt/dumper.texy delete mode 100644 tracy/pt/extensions.texy delete mode 100644 tracy/pt/guide.texy delete mode 100644 tracy/pt/open-files-in-ide.texy delete mode 100644 tracy/pt/recipes.texy delete mode 100644 tracy/pt/stopwatch.texy delete mode 100644 tracy/ro/@home.texy delete mode 100644 tracy/ro/@left-menu.texy delete mode 100644 tracy/ro/@menu.texy delete mode 100644 tracy/ro/@meta.texy delete mode 100644 tracy/ro/configuring.texy delete mode 100644 tracy/ro/dumper.texy delete mode 100644 tracy/ro/extensions.texy delete mode 100644 tracy/ro/guide.texy delete mode 100644 tracy/ro/open-files-in-ide.texy delete mode 100644 tracy/ro/recipes.texy delete mode 100644 tracy/ro/stopwatch.texy delete mode 100644 tracy/sl/@home.texy delete mode 100644 tracy/sl/@left-menu.texy delete mode 100644 tracy/sl/@menu.texy delete mode 100644 tracy/sl/@meta.texy delete mode 100644 tracy/sl/configuring.texy delete mode 100644 tracy/sl/dumper.texy delete mode 100644 tracy/sl/extensions.texy delete mode 100644 tracy/sl/guide.texy delete mode 100644 tracy/sl/open-files-in-ide.texy delete mode 100644 tracy/sl/recipes.texy delete mode 100644 tracy/sl/stopwatch.texy delete mode 100644 tracy/uk/@home.texy delete mode 100644 tracy/uk/@left-menu.texy delete mode 100644 tracy/uk/@menu.texy delete mode 100644 tracy/uk/@meta.texy delete mode 100644 tracy/uk/configuring.texy delete mode 100644 tracy/uk/dumper.texy delete mode 100644 tracy/uk/extensions.texy delete mode 100644 tracy/uk/guide.texy delete mode 100644 tracy/uk/open-files-in-ide.texy delete mode 100644 tracy/uk/recipes.texy delete mode 100644 tracy/uk/stopwatch.texy delete mode 100644 utils/bg/@home.texy delete mode 100644 utils/bg/@left-menu.texy delete mode 100644 utils/bg/@meta.texy delete mode 100644 utils/bg/arrays.texy delete mode 100644 utils/bg/callback.texy delete mode 100644 utils/bg/datetime.texy delete mode 100644 utils/bg/filesystem.texy delete mode 100644 utils/bg/finder.texy delete mode 100644 utils/bg/floats.texy delete mode 100644 utils/bg/helpers.texy delete mode 100644 utils/bg/html-elements.texy delete mode 100644 utils/bg/images.texy delete mode 100644 utils/bg/iterables.texy delete mode 100644 utils/bg/json.texy delete mode 100644 utils/bg/paginator.texy delete mode 100644 utils/bg/random.texy delete mode 100644 utils/bg/reflection.texy delete mode 100644 utils/bg/smartobject.texy delete mode 100644 utils/bg/staticclass.texy delete mode 100644 utils/bg/strings.texy delete mode 100644 utils/bg/type.texy delete mode 100644 utils/bg/validators.texy delete mode 100644 utils/el/@home.texy delete mode 100644 utils/el/@left-menu.texy delete mode 100644 utils/el/@meta.texy delete mode 100644 utils/el/arrays.texy delete mode 100644 utils/el/callback.texy delete mode 100644 utils/el/datetime.texy delete mode 100644 utils/el/filesystem.texy delete mode 100644 utils/el/finder.texy delete mode 100644 utils/el/floats.texy delete mode 100644 utils/el/helpers.texy delete mode 100644 utils/el/html-elements.texy delete mode 100644 utils/el/images.texy delete mode 100644 utils/el/iterables.texy delete mode 100644 utils/el/json.texy delete mode 100644 utils/el/paginator.texy delete mode 100644 utils/el/random.texy delete mode 100644 utils/el/reflection.texy delete mode 100644 utils/el/smartobject.texy delete mode 100644 utils/el/staticclass.texy delete mode 100644 utils/el/strings.texy delete mode 100644 utils/el/type.texy delete mode 100644 utils/el/validators.texy delete mode 100644 utils/hu/@home.texy delete mode 100644 utils/hu/@left-menu.texy delete mode 100644 utils/hu/@meta.texy delete mode 100644 utils/hu/arrays.texy delete mode 100644 utils/hu/callback.texy delete mode 100644 utils/hu/datetime.texy delete mode 100644 utils/hu/filesystem.texy delete mode 100644 utils/hu/finder.texy delete mode 100644 utils/hu/floats.texy delete mode 100644 utils/hu/helpers.texy delete mode 100644 utils/hu/html-elements.texy delete mode 100644 utils/hu/images.texy delete mode 100644 utils/hu/iterables.texy delete mode 100644 utils/hu/json.texy delete mode 100644 utils/hu/paginator.texy delete mode 100644 utils/hu/random.texy delete mode 100644 utils/hu/reflection.texy delete mode 100644 utils/hu/smartobject.texy delete mode 100644 utils/hu/staticclass.texy delete mode 100644 utils/hu/strings.texy delete mode 100644 utils/hu/type.texy delete mode 100644 utils/hu/validators.texy delete mode 100644 utils/pt/@home.texy delete mode 100644 utils/pt/@left-menu.texy delete mode 100644 utils/pt/@meta.texy delete mode 100644 utils/pt/arrays.texy delete mode 100644 utils/pt/callback.texy delete mode 100644 utils/pt/datetime.texy delete mode 100644 utils/pt/filesystem.texy delete mode 100644 utils/pt/finder.texy delete mode 100644 utils/pt/floats.texy delete mode 100644 utils/pt/helpers.texy delete mode 100644 utils/pt/html-elements.texy delete mode 100644 utils/pt/images.texy delete mode 100644 utils/pt/iterables.texy delete mode 100644 utils/pt/json.texy delete mode 100644 utils/pt/paginator.texy delete mode 100644 utils/pt/random.texy delete mode 100644 utils/pt/reflection.texy delete mode 100644 utils/pt/smartobject.texy delete mode 100644 utils/pt/staticclass.texy delete mode 100644 utils/pt/strings.texy delete mode 100644 utils/pt/type.texy delete mode 100644 utils/pt/validators.texy delete mode 100644 utils/ro/@home.texy delete mode 100644 utils/ro/@left-menu.texy delete mode 100644 utils/ro/@meta.texy delete mode 100644 utils/ro/arrays.texy delete mode 100644 utils/ro/callback.texy delete mode 100644 utils/ro/datetime.texy delete mode 100644 utils/ro/filesystem.texy delete mode 100644 utils/ro/finder.texy delete mode 100644 utils/ro/floats.texy delete mode 100644 utils/ro/helpers.texy delete mode 100644 utils/ro/html-elements.texy delete mode 100644 utils/ro/images.texy delete mode 100644 utils/ro/iterables.texy delete mode 100644 utils/ro/json.texy delete mode 100644 utils/ro/paginator.texy delete mode 100644 utils/ro/random.texy delete mode 100644 utils/ro/reflection.texy delete mode 100644 utils/ro/smartobject.texy delete mode 100644 utils/ro/staticclass.texy delete mode 100644 utils/ro/strings.texy delete mode 100644 utils/ro/type.texy delete mode 100644 utils/ro/validators.texy delete mode 100644 utils/sl/@home.texy delete mode 100644 utils/sl/@left-menu.texy delete mode 100644 utils/sl/@meta.texy delete mode 100644 utils/sl/arrays.texy delete mode 100644 utils/sl/callback.texy delete mode 100644 utils/sl/datetime.texy delete mode 100644 utils/sl/filesystem.texy delete mode 100644 utils/sl/finder.texy delete mode 100644 utils/sl/floats.texy delete mode 100644 utils/sl/helpers.texy delete mode 100644 utils/sl/html-elements.texy delete mode 100644 utils/sl/images.texy delete mode 100644 utils/sl/iterables.texy delete mode 100644 utils/sl/json.texy delete mode 100644 utils/sl/paginator.texy delete mode 100644 utils/sl/random.texy delete mode 100644 utils/sl/reflection.texy delete mode 100644 utils/sl/smartobject.texy delete mode 100644 utils/sl/staticclass.texy delete mode 100644 utils/sl/strings.texy delete mode 100644 utils/sl/type.texy delete mode 100644 utils/sl/validators.texy delete mode 100644 utils/uk/@home.texy delete mode 100644 utils/uk/@left-menu.texy delete mode 100644 utils/uk/@meta.texy delete mode 100644 utils/uk/arrays.texy delete mode 100644 utils/uk/callback.texy delete mode 100644 utils/uk/datetime.texy delete mode 100644 utils/uk/filesystem.texy delete mode 100644 utils/uk/finder.texy delete mode 100644 utils/uk/floats.texy delete mode 100644 utils/uk/helpers.texy delete mode 100644 utils/uk/html-elements.texy delete mode 100644 utils/uk/images.texy delete mode 100644 utils/uk/iterables.texy delete mode 100644 utils/uk/json.texy delete mode 100644 utils/uk/paginator.texy delete mode 100644 utils/uk/random.texy delete mode 100644 utils/uk/reflection.texy delete mode 100644 utils/uk/smartobject.texy delete mode 100644 utils/uk/staticclass.texy delete mode 100644 utils/uk/strings.texy delete mode 100644 utils/uk/type.texy delete mode 100644 utils/uk/validators.texy delete mode 100644 www/bg/10-reasons-why-nette.texy delete mode 100644 www/bg/@home.texy delete mode 100644 www/bg/@menu-common.texy delete mode 100644 www/bg/donate.texy delete mode 100644 www/bg/history.texy delete mode 100644 www/bg/license.texy delete mode 100644 www/bg/maintenance.texy delete mode 100644 www/bg/packages.texy delete mode 100644 www/el/10-reasons-why-nette.texy delete mode 100644 www/el/@home.texy delete mode 100644 www/el/@menu-common.texy delete mode 100644 www/el/donate.texy delete mode 100644 www/el/history.texy delete mode 100644 www/el/license.texy delete mode 100644 www/el/maintenance.texy delete mode 100644 www/el/packages.texy delete mode 100644 www/hu/10-reasons-why-nette.texy delete mode 100644 www/hu/@home.texy delete mode 100644 www/hu/@menu-common.texy delete mode 100644 www/hu/donate.texy delete mode 100644 www/hu/history.texy delete mode 100644 www/hu/license.texy delete mode 100644 www/hu/maintenance.texy delete mode 100644 www/hu/packages.texy delete mode 100644 www/pt/10-reasons-why-nette.texy delete mode 100644 www/pt/@home.texy delete mode 100644 www/pt/@menu-common.texy delete mode 100644 www/pt/donate.texy delete mode 100644 www/pt/history.texy delete mode 100644 www/pt/license.texy delete mode 100644 www/pt/maintenance.texy delete mode 100644 www/pt/packages.texy delete mode 100644 www/ro/10-reasons-why-nette.texy delete mode 100644 www/ro/@home.texy delete mode 100644 www/ro/@menu-common.texy delete mode 100644 www/ro/donate.texy delete mode 100644 www/ro/history.texy delete mode 100644 www/ro/license.texy delete mode 100644 www/ro/maintenance.texy delete mode 100644 www/ro/packages.texy delete mode 100644 www/sl/10-reasons-why-nette.texy delete mode 100644 www/sl/@home.texy delete mode 100644 www/sl/@menu-common.texy delete mode 100644 www/sl/donate.texy delete mode 100644 www/sl/history.texy delete mode 100644 www/sl/license.texy delete mode 100644 www/sl/maintenance.texy delete mode 100644 www/sl/packages.texy delete mode 100644 www/uk/10-reasons-why-nette.texy delete mode 100644 www/uk/@home.texy delete mode 100644 www/uk/@menu-common.texy delete mode 100644 www/uk/donate.texy delete mode 100644 www/uk/history.texy delete mode 100644 www/uk/license.texy delete mode 100644 www/uk/maintenance.texy delete mode 100644 www/uk/packages.texy diff --git a/application/bg/@home.texy b/application/bg/@home.texy deleted file mode 100644 index 4c10c2af32..0000000000 --- a/application/bg/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application е ядрото на Nette framework, което предоставя мощни инструменти за създаване на модерни уеб приложения. Предлага редица изключителни характеристики, които значително улесняват разработката и подобряват сигурността и поддръжката на кода. - - -Инсталация ----------- - -Изтеглете и инсталирайте библиотеката с помощта на [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Защо да изберете Nette Application? ------------------------------------ - -Nette винаги е бил пионер в областта на уеб технологиите. - -**Двупосочен рутер:** Nette разполага с усъвършенствана система за маршрутизация, която е уникална със своята двупосочност - не само преобразува URL адреси в действия на приложението, но също така може да генерира обратно URL адреси. Това означава, че: -- Можете по всяко време да промените структурата на URL адресите на цялото приложение, без да е необходимо да редактирате шаблоните -- URL адресите се канонизират автоматично, което подобрява SEO -- Маршрутизацията се дефинира на едно място, а не е разпръсната в анотации - -**Компоненти и сигнали:** Вградената компонентна система, вдъхновена от Delphi и React.js, е напълно изключителна сред PHP framework-ците: -- Позволява създаването на повторно използваеми UI елементи -- Поддържа йерархично композиране на компоненти -- Предлага елегантна обработка на AJAX заявки с помощта на сигнали -- Богата библиотека от готови компоненти на [Componette](https://componette.org) - -**AJAX и снипети:** Nette представи революционен начин за работа с AJAX още през 2009 г., много преди подобни решения като Hotwire за Ruby on Rails или Symfony UX Turbo: -- Снипетите позволяват актуализиране само на части от страницата, без да е необходимо да се пише JavaScript -- Автоматична интеграция с компонентната система -- Интелигентна инвалидация на части от страници -- Минимално количество предавани данни - -**Интуитивни шаблони [Latte|latte:]:** Най-сигурната система за шаблони за PHP с разширени функции: -- Автоматична защита срещу XSS с контекстно чувствително екраниране -- Разширяемост с помощта на персонализирани филтри, функции и тагове -- Наследяване на шаблони и снипети за AJAX -- Отлична поддръжка на PHP 8.x с типова система - -**Dependency Injection:** Nette напълно използва Dependency Injection: -- Автоматично предаване на зависимости (autowiring) -- Конфигурация чрез ясен NEON формат -- Поддръжка на фабрики за компоненти - - -Основни предимства ------------------- - -- **Сигурност**: Автоматична защита срещу [уязвимости|nette:vulnerability-protection] като XSS, CSRF и др. -- **Продуктивност**: По-малко писане, повече функции благодарение на интелигентния дизайн -- **Дебъгване**: [Tracy debugger|tracy:] с панел за маршрутизация -- **Производителност**: Интелигентен кеш, lazy loading на компоненти -- **Гъвкавост**: Лесно модифициране на URL адреси дори след завършване на приложението -- **Компоненти**: Уникална система от повторно използваеми UI елементи -- **Модерност**: Пълна поддръжка на PHP 8.4+ и типова система - - -Да започваме ------------- - -1. [Как работят приложенията? |how-it-works] - Разбиране на основната архитектура -2. [Presenters |presenters] - Работа с презентери и действия -3. [Шаблони |templates] - Създаване на шаблони в Latte -4. [Маршрутизация |routing] - Конфигуриране на URL адреси -5. [Интерактивни компоненти |components] - Използване на компонентната система - - -Съвместимост с PHP ------------------- - -| версия | съвместим с 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 - -Важи за последната пач версия. diff --git a/application/bg/@left-menu.texy b/application/bg/@left-menu.texy deleted file mode 100644 index 89db4642b8..0000000000 --- a/application/bg/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Как работят приложенията? |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Шаблони |templates] -- [Директорийна структура |directory-structure] -- [Маршрутизация |routing] -- [Създаване на URL връзки |creating-links] -- [Интерактивни компоненти |components] -- [AJAX & снипети |ajax] -- [Multiplier |multiplier] -- [Конфигурация |configuration] - - -Допълнително четене -******************* -- [Защо да използвате Nette? |www:10-reasons-why-nette] -- [Инсталация |nette:installation] -- [Пишем първото си приложение! |quickstart:] -- [Ръководства и процедури |best-practices:] -- [Решаване на проблеми |nette:troubleshooting] diff --git a/application/bg/@meta.texy b/application/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/application/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/application/bg/ajax.texy b/application/bg/ajax.texy deleted file mode 100644 index 1cfe2fdaf2..0000000000 --- a/application/bg/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & снипети -************** - -<div class=perex> - -В ерата на съвременните уеб приложения, където функционалността често се разпределя между сървъра и браузъра, AJAX е незаменим свързващ елемент. Какви възможности ни предлага Nette Framework в тази област? -- изпращане на части от шаблона, т.нар. снипети -- предаване на променливи между PHP и JavaScript -- инструменти за дебъгване на AJAX заявки - -</div> - - -AJAX заявка -=========== - -AJAX заявката по същество не се различава от класическата HTTP заявка. Извиква се презентер с определени параметри. И от презентера зависи как ще реагира на заявката - може да върне данни във формат JSON, да изпрати част от HTML код, XML документ и т.н. - -От страна на браузъра инициализираме AJAX заявката с помощта на функцията `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // обработка на отговора -}); -``` - -От страна на сървъра разпознаваме AJAX заявка с метода `$httpRequest->isAjax()` на сървиса [капсулиращ HTTP заявка |http:request]. За откриване се използва HTTP хедърът `X-Requested-With`, затова е важно да го изпращате. В рамките на презентера може да се използва методът `$this->isAjax()`. - -Ако искате да изпратите данни във формат JSON, използвайте метода [`sendJson()` |presenters#Изпращане на отговор]. Методът също така прекратява дейността на презентера. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Ако планирате да отговорите със специален шаблон, предназначен за AJAX, можете да го направите по следния начин: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Снипети -======= - -Най-мощният инструмент, който Nette предлага за свързване на сървъра с клиента, са снипетите. Благодарение на тях можете да превърнете обикновено приложение в AJAX приложение с минимални усилия и няколко реда код. Как работи всичко това, демонстрира примерът Fifteen, чийто код можете да намерите на [GitHub |https://github.com/nette-examples/fifteen]. - -Снипетите, или изрезките, позволяват да се актуализират само части от страницата, вместо да се презарежда цялата страница. Това е не само по-бързо и по-ефективно, но и осигурява по-комфортно потребителско изживяване. Снипетите могат да ви напомнят за Hotwire за Ruby on Rails или Symfony UX Turbo. Интересно е, че Nette представи снипетите 14 години по-рано. - -Как работят снипетите? При първото зареждане на страницата (не-AJAX заявка) се зарежда цялата страница, включително всички снипети. Когато потребителят взаимодейства със страницата (напр. кликне върху бутон, изпрати формуляр и т.н.), вместо да се зарежда цялата страница, се извиква AJAX заявка. Кодът в презентера извършва действието и решава кои снипети трябва да бъдат актуализирани. Nette рендира тези снипети и ги изпраща под формата на масив във формат JSON. Обслужващият код в браузъра вмъква получените снипети обратно в страницата. Така се пренася само кодът на променените снипети, което спестява трафик и ускорява зареждането в сравнение с пренасянето на съдържанието на цялата страница. - - -Naja ----- - -За обслужване на снипети от страна на браузъра се използва [библиотеката Naja |https://naja.js.org]. [Инсталирайте я |https://naja.js.org/#/guide/01-install-setup-naja] като node.js пакет (за използване с приложения Webpack, Rollup, Vite, Parcel и други): - -```shell -npm install naja -``` - -…или директно я вмъкнете в шаблона на страницата: - -```latte -<script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script> -``` - -Първо е необходимо библиотеката да бъде [инициализирана |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: - -```js -naja.initialize(); -``` - -За да превърнете обикновена връзка (сигнал) или изпращане на формуляр в AJAX заявка, е достатъчно да маркирате съответната връзка, формуляр или бутон с клас `ajax`: - -```latte -<a n:href="go!" class="ajax">Go</a> - -<form n:name="form" class="ajax"> - <input n:name="submit"> -</form> - -или - -<form n:name="form"> - <input n:name="submit" class="ajax"> -</form> -``` - - -Прерисуване на снипети ----------------------- - -Всеки обект от клас [Control |components] (включително самият Presenter) следи дали са настъпили промени, изискващи неговото прерисуване. За това служи методът `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // след влизане е необходимо да се прерисува съответната част - $this->redrawControl(); - // ... -} -``` - -Nette позволява още по-фин контрол върху това, което трябва да се прерисува. Споменатият метод може да приема името на снипета като аргумент. Така може да се инвалидира (разбирай: да се наложи прерисуване) на ниво части от шаблона. Ако се инвалидира целият компонент, тогава се прерисува и всеки негов снипет: - -```php -// инвалидира снипета 'header' -$this->redrawControl('header'); -``` - - -Снипети в Latte ---------------- - -Използването на снипети в Latte е изключително лесно. Ако искате да дефинирате част от шаблона като снипет, просто я обвийте с таговете `{snippet}` и `{/snippet}`: - -```latte -{snippet header} - <h1>Hello ... </h1> -{/snippet} -``` - -Снипетът създава в HTML страницата елемент `<div>` със специално генериран `id`. При прерисуване на снипета се актуализира съдържанието на този елемент. Затова е необходимо при първоначалното рендиране на страницата да се рендират и всички снипети, дори и ако в началото са празни. - -Можете да създадете и снипет с друг елемент освен `<div>` с помощта на n:атрибут: - -```latte -<article n:snippet="header" class="foo bar"> - <h1>Hello ... </h1> -</article> -``` - - -Области на снипети ------------------- - -Имената на снипетите могат да бъдат и изрази: - -```latte -{foreach $items as $id => $item} - <li n:snippet="item-{$id}">{$item}</li> -{/foreach} -``` - -Така ще ни се създадат няколко снипета `item-0`, `item-1` и т.н. Ако директно инвалидираме динамичен снипет (например `item-1`), нищо няма да се прерисува. Причината е, че снипетите наистина работят като изрезки и се рендират само те самите. Но в шаблона всъщност няма снипет с име `item-1`. Той се създава едва при изпълнението на кода около снипета, т.е. цикъла foreach. Затова ще маркираме частта от шаблона, която трябва да се изпълни, с помощта на тага `{snippetArea}`: - -```latte -<ul n:snippetArea="itemsContainer"> - {foreach $items as $id => $item} - <li n:snippet="item-{$id}">{$item}</li> - {/foreach} -</ul> -``` - -И ще накараме да се прерисува както самият снипет, така и цялата родителска област: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Същевременно е добре да се уверим, че масивът `$items` съдържа само тези елементи, които трябва да се прерисуват. - -Ако в шаблона вмъкваме с помощта на тага `{include}` друг шаблон, който съдържа снипети, е необходимо вмъкването на шаблона отново да се включи в `snippetArea` и тя да се инвалидира заедно със снипета: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Снипети в компоненти --------------------- - -Можете да създавате снипети и в [компоненти|components] и Nette ще ги прерисува автоматично. Но тук има определено ограничение: за прерисуване на снипети се извиква методът `render()` без параметри. Следователно предаването на параметри в шаблона няма да работи: - -```latte -OK -{control productGrid} - -няма да работи: -{control productGrid $arg, $arg} -{control productGrid:paginator} -``` - - -Изпращане на потребителски данни --------------------------------- - -Заедно със снипетите можете да изпратите на клиента всякакви други данни. Достатъчно е да ги запишете в обекта `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } -} -``` - - -Предаване на параметри -====================== - -Ако изпращаме параметри на компонент чрез AJAX заявка, било то параметри на сигнал или персистентни параметри, трябва да посочим тяхното глобално име в заявката, което включва и името на компонента. Цялото име на параметъра се връща от метода `getParameterId()`. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -И handle метод със съответните параметри в компонента: - -```php -public function handleFoo(int $bar): void -{ -} -``` diff --git a/application/bg/bootstrapping.texy b/application/bg/bootstrapping.texy deleted file mode 100644 index 12decd318d..0000000000 --- a/application/bg/bootstrapping.texy +++ /dev/null @@ -1,297 +0,0 @@ -Зареждане -********* - -<div class=perex> - -Зареждането е процесът на инициализиране на средата на приложението, създаване на контейнер за инжектиране на зависимости (DI) и стартиране на приложението. Ще обсъдим: - -- как класът Bootstrap инициализира средата -- как приложенията се конфигурират чрез NEON файлове -- как да разграничаваме между производствен и разработчически режим -- как да създаваме и конфигурираме DI контейнера - -</div> - - -Приложенията, независимо дали са уеб или скриптове, стартирани от командния ред, започват своята работа с някаква форма на инициализация на средата. В миналото за това отговаряше файл с име например `include.inc.php`, който първоначалният файл включваше. В съвременните Nette приложения той е заменен от клас `Bootstrap`, който като част от приложението ще намерите във файла `app/Bootstrap.php`. Може да изглежда например така: - -```php -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Конфигураторът е отговорен за настройката на средата на приложението и сървисите. - $this->configurator = new Configurator; - // Задава директорията за временни файлове, генерирани от Nette (напр. компилирани шаблони) - $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 е умно и режимът за разработка се включва автоматично, - // или можете да го разрешите за конкретен IP адрес, като разкоментирате следния ред: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Активира Tracy: ултимативният "швейцарски нож" за дебъгване. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: автоматично зарежда всички класове в избраната директория - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Зарежда конфигурационните файлове - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -Първоначалният файл в случай на уеб приложения е `index.php`, който се намира в [публичната директория |directory-structure#Публична директория www] `www/`. Той изисква от клас Bootstrap да инициализира средата и да създаде DI контейнер. След това от него получава сървиса `Application`, който стартира уеб приложението: - -```php -$bootstrap = new App\Bootstrap; -// Инициализация на средата + създаване на DI контейнер -$container = $bootstrap->bootWebApplication(); -// DI контейнерът създава обект Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Стартиране на приложението Nette и обработка на входящата заявка -$application->run(); -``` - -Както се вижда, с настройката на средата и създаването на dependency injection (DI) контейнер помага класът [api:Nette\Bootstrap\Configurator], който сега ще разгледаме по-подробно. - - -Режим за разработка срещу продукционен режим -============================================ - -Nette се държи различно в зависимост от това дали работи на сървър за разработка или на продукционен сървър: - -🛠️ Режим за разработка (Development): - - Показва Tracy debugbar с полезна информация (SQL заявки, време за изпълнение, използвана памет) - - При грешка показва подробна страница за грешка с извиквания на функции и съдържание на променливи - - Автоматично обновява кеша при промяна на Latte шаблони, редактиране на конфигурационни файлове и т.н. - - -🚀 Продукционен режим (Production): - - Не показва никаква информация за дебъгване, всички грешки се записват в лога - - При грешка показва ErrorPresenter или обща страница "Server Error" - - Кешът никога не се обновява автоматично! - - Оптимизиран за скорост и сигурност - - -Изборът на режим се извършва чрез автодетекция, така че обикновено не е необходимо нищо да се конфигурира или ръчно да се превключва: - -- режим за разработка: на localhost (IP адрес `127.0.0.1` или `::1`), ако няма прокси (т.е. неговия HTTP хедър) -- продукционен режим: навсякъде другаде - -Ако искаме да разрешим режима за разработка и в други случаи, например за програмисти, достъпващи от конкретен IP адрес, използваме `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // може да се посочи и масив от IP адреси -``` - -Определено препоръчваме да комбинирате IP адрес с бисквитка. В бисквитката `nette-debug` ще запазим таен токен, напр. `secret1234`, и по този начин ще активираме режима за разработка за програмисти, достъпващи от конкретен IP адрес и същевременно имащи споменатия токен в бисквитката: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Можем също така да изключим напълно режима за разработка, дори и за localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Внимание, стойността `true` включва режима за разработка принудително, което никога не трябва да се случва на продукционен сървър. - - -Инструмент за дебъгване Tracy -============================= - -За лесно дебъгване ще включим и страхотния инструмент [Tracy |tracy:]. В режим за разработка той визуализира грешките, а в продукционен режим ги записва в лога в посочената директория: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Временни файлове -================ - -Nette използва кеш за DI контейнер, RobotLoader, шаблони и т.н. Затова е необходимо да се зададе път до директорията, където ще се съхранява кешът: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -На Linux или macOS задайте на директориите `log/` и `temp/` [права за запис |nette:troubleshooting#Настройка на правата на директориите]. - - -RobotLoader -=========== - -Обикновено ще искаме автоматично да зареждаме класове с помощта на [RobotLoader |robot-loader:], затова трябва да го стартираме и да го накараме да зарежда класове от директорията, където се намира `Bootstrap.php` (т.е. `__DIR__`), и всички поддиректории: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Алтернативен подход е да оставите класовете да се зареждат само чрез [Composer |best-practices:composer] при спазване на PSR-4. - - -Часова зона -=========== - -Чрез конфигуратора можете да зададете подразбиращата се часова зона. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Конфигурация на DI контейнера -============================= - -Част от процеса на зареждане е създаването на DI контейнер или фабрика за обекти, което е сърцето на цялото приложение. Всъщност това е PHP клас, който Nette генерира и съхранява в директорията с кеша. Фабриката произвежда ключови обекти на приложението и с помощта на конфигурационни файлове я инструктираме как да ги създава и настройва, като по този начин влияем на поведението на цялото приложение. - -Конфигурационните файлове обикновено се записват във формат [NEON |neon:format]. В отделна глава ще научите [какво всичко може да се конфигурира |nette:configuring]. - -.[tip] -В режим за разработка контейнерът се актуализира автоматично при всяка промяна на кода или конфигурационните файлове. В продукционен режим той се генерира само веднъж и промените не се проверяват заради максимална производителност. - -Конфигурационните файлове зареждаме с помощта на `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Ако искаме да добавим повече конфигурационни файлове, можем да извикаме функцията `addConfig()` няколко пъти. - -```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'); -} -``` - -Името `cli.php` не е грешка, конфигурацията може да бъде записана и в PHP файл, който я връща като масив. - -Също така можем да добавим други конфигурационни файлове в [секцията `includes` |dependency-injection:configuration#Включване на файлове]. - -Ако в конфигурационните файлове се появят елементи със същите ключове, те ще бъдат презаписани или в случай на [масиви слети |dependency-injection:configuration#Сливане]. По-късно включеният файл има по-висок приоритет от предходния. Файлът, в който е посочена секцията `includes`, има по-висок приоритет от включените в него файлове. - - -Статични параметри ------------------- - -Параметрите, използвани в конфигурационните файлове, можем да дефинираме [в секцията `parameters` |dependency-injection:configuration#Параметри] и също така да ги предаваме (или презаписваме) с метода `addStaticParameters()` (има псевдоним `addParameters()`). Важно е, че различните стойности на параметрите ще доведат до генериране на допълнителни DI контейнери, т.е. допълнителни класове. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -Към параметъра `projectId` може да се обърнем в конфигурацията с обичайния запис `%projectId%`. - - -Динамични параметри -------------------- - -В контейнера можем да добавим и динамични параметри, чиито различни стойности, за разлика от статичните параметри, не предизвикват генериране на нови DI контейнери. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Лесно можем да добавим напр. променливи на средата, към които след това можем да се обърнем в конфигурацията със записа `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Параметри по подразбиране -------------------------- - -В конфигурационните файлове можете да използвате тези статични параметри: - -- `%appDir%` е абсолютният път до директорията с файла `Bootstrap.php` -- `%wwwDir%` е абсолютният път до директорията с входния файл `index.php` -- `%tempDir%` е абсолютният път до директорията за временни файлове -- `%vendorDir%` е абсолютният път до директорията, където Composer инсталира библиотеките -- `%rootDir%` е абсолютният път до коренната директория на проекта -- `%debugMode%` указва дали приложението е в режим на дебъгване -- `%consoleMode%` указва дали заявката е дошла през командния ред - - -Импортирани сървиси -------------------- - -Сега вече навлизаме по-дълбоко. Въпреки че смисълът на DI контейнера е да произвежда обекти, по изключение може да възникне нужда да се вмъкне съществуващ обект в контейнера. Правим това, като дефинираме сървиса с флаг `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -И в bootstrap вмъкваме обекта в контейнера: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Различна среда -============== - -Не се страхувайте да промените клас Bootstrap според вашите нужди. Към метода `bootWebApplication()` можете да добавите параметри за разграничаване на уеб проекти. Или можем да добавим други методи, например `bootTestEnvironment()`, който инициализира средата за единични тестове, `bootConsoleApplication()` за скриптове, извиквани от командния ред и т.н. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // инициализация на Nette Tester - $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/bg/components.texy b/application/bg/components.texy deleted file mode 100644 index 42049c0017..0000000000 --- a/application/bg/components.texy +++ /dev/null @@ -1,485 +0,0 @@ -Интерактивни компоненти -*********************** - -<div class=perex> - -Компонентите са самостоятелни обекти за многократна употреба, които вмъкваме в страниците. Това могат да бъдат формуляри, datagrid-ове, анкети, всъщност всичко, което има смисъл да се използва многократно. Ще покажем: - -- как да използваме компоненти? -- как да ги пишем? -- какво са сигналите? - -</div> - -Nette има вградена компонентна система. Нещо подобно може да е познато на ветераните от Delphi или ASP.NET Web Forms, на нещо отдалечено подобно са базирани React или Vue.js. Въпреки това, в света на PHP фреймуърците това е уникално явление. - -При това компонентите фундаментално влияят на подхода към създаването на приложения. Можете да сглобявате страници от предварително подготвени единици. Нуждаете се от datagrid в администрацията? Намерете го на [Componette |https://componette.org/search/component], хранилище на open-source добавки (т.е. не само компоненти) за Nette и просто го вмъкнете в презентера. - -В презентера можете да включите произволен брой компоненти. А в някои компоненти можете да вмъквате други компоненти. Така се създава компонентно дърво, чийто корен е презентерът. - - -Фабрични методи -=============== - -Как се вмъкват компоненти в презентера и след това се използват? Обикновено с помощта на фабрични методи. - -Фабриката за компоненти представлява елегантен начин за създаване на компоненти едва в момента, когато те са наистина необходими (lazy / on demand). Цялата магия се състои в имплементирането на метод с име `createComponent<Name>()`, където `<Name>` е името на създавания компонент, и който създава и връща компонента. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->item; - return $poll; - } -} -``` - -Благодарение на това, че всички компоненти се създават в отделни методи, кодът става по-прегледен. - -.[note] -Имената на компонентите винаги започват с малка буква, въпреки че в името на метода се пишат с главна. - -Фабриките никога не се извикват директно, те се извикват сами в момента, когато използваме компонента за първи път. Благодарение на това компонентът се създава в правилния момент и само в случай, че е наистина необходим. Ако не използваме компонента (например при AJAX заявка, когато се пренася само част от страницата, или при кеширане на шаблона), той изобщо не се създава и спестяваме производителност на сървъра. - -```php .{file:DefaultPresenter.php} -// достъпваме компонента и ако това е за първи път, -// се извиква createComponentPoll(), която го създава -$poll = $this->getComponent('poll'); -// алтернативен синтаксис: $poll = $this['poll']; -``` - -В шаблона е възможно да се рендира компонент с помощта на тага [{control} |#Рендиране]. Затова не е необходимо ръчно да се предават компоненти в шаблона. - -```latte -<h2>Гласувайте</h2> - -{control poll} -``` - - -Hollywood style -=============== - -Компонентите обикновено използват една свежа техника, която обичаме да наричаме Hollywood style. Със сигурност познавате крилатата фраза, която толкова често чуват участниците във филмови кастинги: „Не ни звънете, ние ще ви се обадим“. И точно за това става въпрос. - -В Nette, вместо постоянно да се налага да питате нещо („беше ли изпратен формулярът?“, „беше ли валиден?“ или „натисна ли потребителят този бутон?“), казвате на фреймуърка „когато това се случи, извикай този метод“ и оставяте по-нататъшната работа на него. Ако програмирате на JavaScript, този стил на програмиране ви е добре познат. Пишете функции, които се извикват, когато настъпи определено събитие. И езикът им предава съответните параметри. - -Това напълно променя гледната точка към писането на приложения. Колкото повече задачи можете да оставите на фреймуърка, толкова по-малко работа имате вие. И толкова по-малко неща можете да пропуснете. - - -Пишем компонент -=============== - -Под понятието компонент обикновено разбираме наследник на клас [api:Nette\Application\UI\Control]. (По-точно би било да се използва терминът „controls“, но „контроли“ на български има съвсем различно значение и по-скоро се е наложило „компоненти“.) Самият презентер [api:Nette\Application\UI\Presenter] между другото също е наследник на клас `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Рендиране -========= - -Вече знаем, че за рендиране на компонент се използва тагът `{control componentName}`. Той всъщност извиква метода `render()` на компонента, в който се грижим за рендирането. На разположение имаме, точно както в презентера, [Latte шаблон|templates] в променливата `$this->template`, на която предаваме параметри. За разлика от презентера, трябва да посочим файла с шаблона и да го накараме да се рендира: - -```php .{file:PollControl.php} -public function render(): void -{ - // вмъкваме в шаблона някакви параметри - $this->template->param = $value; - // и го рендираме - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -Тагът `{control}` позволява да се предадат параметри на метода `render()`: - -```latte -{control poll $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Понякога компонентът може да се състои от няколко части, които искаме да рендираме отделно. За всяка от тях създаваме собствен метод за рендиране, тук в примера например `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -А в шаблона след това го извикваме с помощта на: - -```latte -{control poll:paginator} -``` - -За по-добро разбиране е добре да знаете как този таг се превежда на PHP. - -```latte -{control poll} -{control poll:paginator 123, 'hello'} -``` - -се превежда като: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -Методът `getComponent()` връща компонента `poll` и върху този компонент извиква метода `render()`, респ. `renderPaginator()`, ако в тага след двоеточието е посочен друг начин на рендиране. - -.[caution] -Внимание, ако някъде в параметрите се появи **`=>`**, всички параметри ще бъдат опаковани в масив и предадени като първи аргумент: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -се превежда като: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Рендиране на подкомпонент: - -```latte -{control cartControl-someForm} -``` - -се превежда като: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Компонентите, както и презентерите, предават на шаблоните няколко полезни променливи автоматично: - -- `$basePath` е абсолютният URL път до коренната директория (напр. `/eshop`) -- `$baseUrl` е абсолютният URL до коренната директория (напр. `http://localhost/eshop`) -- `$user` е обект [представляващ потребителя |security:authentication] -- `$presenter` е текущият презентер -- `$control` е текущият компонент -- `$flashes` масив от [съобщения |#Flash съобщения], изпратени с функцията `flashMessage()` - - -Сигнал -====== - -Вече знаем, че навигацията в Nette приложение се състои в свързване или пренасочване към двойки `Presenter:action`. Но какво, ако просто искаме да извършим действие на **текущата страница**? Например да променим сортирането на колони в таблица; да изтрием елемент; да превключим светъл/тъмен режим; да изпратим формуляр; да гласуваме в анкета; и т.н. - -Този вид заявки се наричат сигнали. И подобно на действията, които извикват методи `action<Action>()` или `render<Action>()`, сигналите извикват методи `handle<Signal>()`. Докато понятието действие (или view) е свързано чисто само с презентерите, сигналите се отнасят до всички компоненти. И следователно и до презентерите, защото `UI\Presenter` е наследник на `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... обработка на сигнала ... -} -``` - -Връзка, която извиква сигнал, създаваме по обичайния начин, т.е. в шаблона с атрибут `n:href` или таг `{link}`, в кода с метод `link()`. Повече в главата [Създаване на URL връзки |creating-links#Връзки към сигнал]. - -```latte -<a n:href="click! $x, $y">кликнете тук</a> -``` - -Сигналът винаги се извиква на текущия презентер и действие, не е възможно да се извика на друг презентер или друго действие. - -Сигналът следователно предизвиква презареждане на страницата точно както при първоначалната заявка, само че допълнително извиква обслужващия метод на сигнала със съответните параметри. Ако методът не съществува, се хвърля изключение [api:Nette\Application\UI\BadSignalException], което се показва на потребителя като страница за грешка 403 Forbidden. - - -Снипети и AJAX -============== - -Сигналите може би малко ви напомнят на AJAX: хендлъри, които се извикват на текущата страница. И сте прави, сигналите наистина често се извикват с помощта на AJAX и след това предаваме на браузъра само променените части от страницата. Или т.нар. снипети. Повече информация ще намерите на [страницата, посветена на AJAX |ajax]. - - -Flash съобщения -=============== - -Компонентът има собствено хранилище за flash съобщения, независимо от презентера. Това са съобщения, които например информират за резултата от операция. Важна характеристика на flash съобщенията е, че те са достъпни в шаблона и след пренасочване. Дори след показване остават активни още 30 секунди – например в случай, че поради грешка при прехвърлянето потребителят обнови страницата - съобщението няма да изчезне веднага. - -Изпращането се извършва от метода [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Първият параметър е текстът на съобщението или обект `stdClass`, представляващ съобщението. Незадължителният втори параметър е неговият тип (error, warning, info и др.). Методът `flashMessage()` връща инстанция на flash съобщението като обект `stdClass`, към който могат да се добавят допълнителни информации. - -```php -$this->flashMessage('Елементът беше изтрит.'); -$this->redirect(/* ... */); // и пренасочваме -``` - -В шаблона тези съобщения са достъпни в променливата `$flashes` като обекти `stdClass`, които съдържат свойства `message` (текст на съобщението), `type` (тип на съобщението) и могат да съдържат вече споменатите потребителски информации. Рендираме ги например така: - -```latte -{foreach $flashes as $flash} - <div class="flash {$flash->type}">{$flash->message}</div> -{/foreach} -``` - - -Пренасочване след сигнал -======================== - -След обработка на сигнала на компонента често следва пренасочване. Това е подобна ситуация като при формулярите - след тяхното изпращане също пренасочваме, за да не се изпратят данните отново при обновяване на страницата в браузъра. - -```php -$this->redirect('this') // пренасочва към текущия презентер и действие -``` - -Тъй като компонентът е елемент за многократна употреба и обикновено не трябва да има пряка връзка с конкретни презентери, методите `redirect()` и `link()` автоматично интерпретират параметъра като сигнал на компонента: - -```php -$this->redirect('click') // пренасочва към сигнала 'click' на същия компонент -``` - -Ако трябва да пренасочите към друг презентер или действие, можете да го направите чрез презентера: - -```php -$this->getPresenter()->redirect('Product:show'); // пренасочва към друг презентер/действие -``` - - -Персистентни параметри -====================== - -Персистентните параметри служат за поддържане на състоянието в компонентите между различни заявки. Тяхната стойност остава същата и след кликване върху връзка. За разлика от данните в сесията, те се пренасят в URL. И това става напълно автоматично, включително за връзки, създадени в други компоненти на същата страница. - -Имате например компонент за пагиниране на съдържание. Такива компоненти могат да бъдат няколко на страницата. И искаме след кликване върху връзка всички компоненти да останат на своята текуща страница. Затова от номера на страницата (`page`) ще направим персистентен параметър. - -Създаването на персистентен параметър в Nette е изключително лесно. Достатъчно е да създадете публично свойство и да го маркирате с атрибут: (преди се използваше `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // този ред е важен - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // трябва да е public -} -``` - -При свойството препоръчваме да посочите и типа данни (напр. `int`) и можете да посочите и стойност по подразбиране. Стойностите на параметрите могат да бъдат [валидирани |#Валидация на персистентни параметри]. - -При създаване на връзка може да се промени стойността на персистентния параметър: - -```latte -<a n:href="this page: $page + 1">next</a> -``` - -Или може да бъде *ресетнат*, т.е. премахнат от URL. Тогава ще приеме своята стойност по подразбиране: - -```latte -<a n:href="this page: null">reset</a> -``` - - -Персистентни компоненти -======================= - -Не само параметрите, но и компонентите могат да бъдат персистентни. При такъв компонент неговите персистентни параметри се пренасят и между различни действия на презентера или между няколко презентера. Персистентните компоненти маркираме с анотация при класа на презентера. Например така маркираме компонентите `calendar` и `poll`: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Подкомпонентите вътре в тези компоненти не е необходимо да се маркират, те също стават персистентни. - -В PHP 8 можете да използвате и атрибути за маркиране на персистентни компоненти: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Компоненти със зависимости -========================== - -Как да създаваме компоненти със зависимости, без да „замърсяваме“ презентерите, които ще ги използват? Благодарение на умните свойства на DI контейнера в Nette, както при използването на класически сървиси, можем да оставим по-голямата част от работата на фреймуърка. - -Да вземем за пример компонент, който има зависимост от сървиса `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // Id на анкетата, за която създаваме компонента - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($id, $voteId); - // ... - } -} -``` - -Ако пишехме класически сървис, нямаше да има какво да се решава. За предаването на всички зависимости невидимо щеше да се погрижи DI контейнерът. Но с компонентите обикновено постъпваме така, че създаваме нова инстанция директно в презентера в [фабричните методи |#Фабрични методи] `createComponent…()`. Но да предаваме всички зависимости на всички компоненти в презентера, за да ги предадем след това на компонентите, е тромаво. И колко написан код… - -Логичният въпрос е защо просто не регистрираме компонента като класически сървис, не го предадем на презентера и след това в метода `createComponent…()` не го връщаме? Такъв подход обаче е неподходящ, защото искаме да имаме възможност да създаваме компонента дори няколко пъти. - -Правилното решение е да напишем за компонента фабрика, т.е. клас, който ще ни създаде компонента: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Така регистрираме фабриката в нашия контейнер в конфигурацията: - -```neon -services: - - PollControlFactory -``` - -и накрая я използваме в нашия презентер: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // можем да си предадем нашия параметър - return $this->pollControlFactory->create($pollId); - } -} -``` - -Страхотно е, че Nette DI може да [генерира |dependency-injection:factory] такива прости фабрики, така че вместо целия й код е достатъчно да напишем само нейния интерфейс: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -И това е всичко. Nette вътрешно ще имплементира този интерфейс и ще го предаде на презентера, където вече можем да го използваме. Магически ще добави към нашия компонент и параметъра `$id` и инстанция на класа `PollFacade`. - - -Компоненти в дълбочина -====================== - -Компонентите в Nette Application представляват части от уеб приложението за многократна употреба, които вмъкваме в страниците и на които всъщност е посветена цялата тази глава. Какви точно възможности има такъв компонент? - -1) може да се рендира в шаблон -2) знае [коя своя част |ajax#Снипети] трябва да рендира при AJAX заявка (снипети) -3) има способността да съхранява своето състояние в URL (персистентни параметри) -4) има способността да реагира на потребителски действия (сигнали) -5) създава йерархична структура (където коренът е презентерът) - -Всяка от тези функции се обслужва от някой от класовете на наследствената линия. За рендирането (1 + 2) отговаря [api:Nette\Application\UI\Control], за включването в [жизнения цикъл |presenters#Жизнен цикъл на презентера] (3, 4) класът [api:Nette\Application\UI\Component], а за създаването на йерархична структура (5) класовете [Container и Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Жизнен цикъл на компонента --------------------------- - -[* lifecycle-component.svg *] *** *Жизнен цикъл на компонента* .<> - - -Валидация на персистентни параметри ------------------------------------ - -Стойностите на [персистентните параметри |#Персистентни параметри], получени от URL, се записват в свойствата от метода `loadState()`. Той също така проверява дали съответства типът данни, посочен при свойството, в противен случай отговаря с грешка 404 и страницата не се показва. - -Никога не вярвайте сляпо на персистентните параметри, защото те могат лесно да бъдат презаписани от потребителя в URL. Така например ще проверим дали номерът на страницата `$this->page` е по-голям от 0. Подходящ начин е да презапишем споменатия метод `loadState()`: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // тук се задава $this->page - // следва собствена проверка на стойността: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Обратният процес, т.е. събирането на стойности от персистентните свойства, се извършва от метода `saveState()`. - - -Сигнали в дълбочина -------------------- - -Сигналът предизвиква презареждане на страницата точно както при първоначалната заявка (освен в случай, че е извикан с AJAX) и извиква метода `signalReceived($signal)`, чиято имплементация по подразбиране в класа `Nette\Application\UI\Component` се опитва да извика метод, съставен от думите `handle{signal}`. По-нататъшната обработка зависи от дадения обект. Обектите, които наследяват `Component` (т.е. `Control` и `Presenter`), реагират така, че се опитват да извикат метода `handle{signal}` със съответните параметри. - -С други думи: взема се дефиницията на функцията `handle{signal}` и всички параметри, които са дошли със заявката, и към аргументите се присвояват параметри от URL по име и се опитва да се извика даденият метод. Напр. като параметър `$id` се предава стойността от параметъра `id` в URL, като `$something` се предава `something` от URL и т.н. И ако методът не съществува, методът `signalReceived` хвърля [изключение |api:Nette\Application\UI\BadSignalException]. - -Сигнал може да приема всякакъв компонент, презентер или обект, който имплементира интерфейса `SignalReceiver` и е свързан към дървото на компонентите. - -Сред основните получатели на сигнали ще бъдат `Presenters` и визуалните компоненти, наследяващи `Control`. Сигналът трябва да служи като знак за обекта, че трябва да направи нещо – анкетата трябва да преброи гласа от потребителя, блокът с новини трябва да се разгъне и да покаже два пъти повече новини, формулярът е изпратен и трябва да обработи данните и т.н. - -URL за сигнал създаваме с помощта на метода [Component::link() |api:Nette\Application\UI\Component::link()]. Като параметър `$destination` предаваме низ `{signal}!` и като `$args` масив от аргументи, които искаме да предадем на сигнала. Сигналът винаги се извиква на текущия презентер и действие с текущите параметри, параметрите на сигнала само се добавят. Освен това в началото се добавя **параметър `?do`, който определя сигнала**. - -Неговият формат е или `{signal}`, или `{signalReceiver}-{signal}`. `{signalReceiver}` е името на компонента в презентера. Затова в името на компонента не може да има тире – използва се за разделяне на името на компонента и сигнала, но е възможно така да се вложат няколко компонента. - -Методът [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] проверява дали компонентът (първи аргумент) е получател на сигнала (втори аргумент). Вторият аргумент можем да пропуснем – тогава се проверява дали компонентът е получател на какъвто и да е сигнал. Като втори параметър може да се посочи `true` и така да се провери дали получател е не само посоченият компонент, но и който и да е негов наследник. - -Във всяка фаза, предхождаща `handle{signal}`, можем да изпълним сигнала ръчно, като извикаме метода [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], който поема отговорността за обработката на сигнала – взема компонента, който е определен като получател на сигнала (ако не е определен получател на сигнала, това е самият презентер) и му изпраща сигнала. - -Пример: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Така сигналът е изпълнен преждевременно и вече няма да се извиква отново. diff --git a/application/bg/configuration.texy b/application/bg/configuration.texy deleted file mode 100644 index f1e952fd3a..0000000000 --- a/application/bg/configuration.texy +++ /dev/null @@ -1,191 +0,0 @@ -Конфигурация на приложения -************************** - -.[perex] -Преглед на конфигурационните опции за Nette приложения. - - -Application -=========== - -```neon -application: - # показва ли се панелът "Nette Application" в Tracy BlueScreen? - debugger: ... # (bool) по подразбиране е true - - # ще се извиква ли error-presenter при грешка? - # има ефект само в режим на разработка - catchExceptions: ... # (bool) по подразбиране е true - - # име на error-presenter - errorPresenter: Error # (string|array) по подразбиране е 'Nette:Error' - - # дефинира псевдоними за презентери и действия - aliases: ... - - # дефинира правила за превод на името на презентера в клас - mapping: ... - - # грешните връзки не генерират ли предупреждения? - # има ефект само в режим на разработка - silentLinks: ... # (bool) по подразбиране е false -``` - -От `nette/application` версия 3.2 може да се дефинира двойка error-presenter-и: - -```neon -application: - errorPresenter: - 4xx: Error4xx # за изключение Nette\Application\BadRequestException - 5xx: Error5xx # за останалите изключения -``` - -Опцията `silentLinks` определя как Nette ще се държи в режим на разработка, когато генерирането на връзка се провали (например защото не съществува презентер и т.н.). Стойността по подразбиране `false` означава, че Nette ще хвърли грешка `E_USER_WARNING`. Задаването на `true` ще потисне това съобщение за грешка. В продукционна среда `E_USER_WARNING` винаги се извиква. Това поведение можем да контролираме и чрез задаване на променливата на презентера [$invalidLinkMode |creating-links#Невалидни връзки]. - -[Псевдонимите опростяват свързването |creating-links#Псевдоними] към често използвани презентери. - -[Мапингът дефинира правила |directory-structure#Мапиране на презентери], според които от името на презентера се извежда името на класа. - - -Автоматична регистрация на презентери -------------------------------------- - -Nette автоматично добавя презентерите като сървиси в DI контейнера, което значително ускорява тяхното създаване. Как Nette намира презентерите може да се конфигурира: - -```neon -application: - # търси ли презентери в Composer class map? - scanComposer: ... # (bool) по подразбиране е true - - # маска, на която трябва да отговарят името на класа и файла - scanFilter: ... # (string) по подразбиране е '*Presenter' - - # в кои директории да се търсят презентери? - scanDirs: # (string[]|false) по подразбиране е '%appDir%' - - %vendorDir%/mymodule -``` - -Директориите, посочени в `scanDirs`, не презаписват стойността по подразбиране `%appDir%`, а я допълват, така че `scanDirs` ще съдържа и двата пътя `%appDir%` и `%vendorDir%/mymodule`. Ако искаме да пропуснем директорията по подразбиране, използваме [удивителен знак |dependency-injection:configuration#Сливане], който презаписва стойността: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Сканирането на директории може да се изключи, като се посочи стойност false. Не препоръчваме напълно да се потиска автоматичното добавяне на презентери, защото в противен случай ще се намали производителността на приложението. - - -Шаблони Latte -============= - -С тази настройка може глобално да се повлияе на поведението на Latte в компоненти и презентери. - -```neon -latte: - # показва ли се панелът Latte в Tracy Bar за основния шаблон (true) или за всички компоненти (all)? - debugger: ... # (true|false|'all') по подразбиране е true - - # генерира шаблони с хедър declare(strict_types=1) - strictTypes: ... # (bool) по подразбиране е false - - # включва режим на [стриктен парсер |latte:develop#strict-mode] - strictParsing: ... # (bool) по подразбиране е false - - # активира [проверка на генерирания код |latte:develop#Checking Generated Code] - phpLinter: ... # (string) по подразбиране е null - - # задава locale - locale: cs_CZ # (string) по подразбиране е null - - # клас на обекта $this->template - templateClass: App\MyTemplateClass # по подразбиране е Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Ако използвате Latte версия 3, можете да добавяте нови [разширения |latte:extending-latte#Latte Extension] с помощта на: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Ако използвате Latte версия 2, можете да регистрирате нови тагове (макроси) или като посочите името на класа, или като референция към сървис. По подразбиране се извиква методът `install()`, но това може да се промени, като се посочи името на друг метод: - -```neon -latte: - # регистрация на потребителски Latte тагове - macros: - - App\MyLatteMacros::register # статичен метод, classname или callable - - @App\MyLatteMacrosFactory # сървис с метод install() - - @App\MyLatteMacrosFactory::register # сървис с метод register() - -services: - - App\MyLatteMacrosFactory -``` - - -Маршрутизация -============= - -Основни настройки: - -```neon -routing: - # показва ли се панелът за маршрутизация в Tracy Bar? - debugger: ... # (bool) по подразбиране е true - - # сериализира рутера в DI контейнера - cache: ... # (bool) по подразбиране е false -``` - -Маршрутизацията обикновено дефинираме в клас [RouterFactory |routing#Колекция от маршрути]. Алтернативно, маршрутите могат да се дефинират и в конфигурацията с помощта на двойки `маска: действие`, но този начин не предлага толкова широка вариативност в настройките: - -```neon -routing: - routes: - 'detail/<id>': Admin:Home:default - '<presenter>/<action>': Front:Home:default -``` - - -Константи -========= - -Създаване на PHP константи. - -```neon -constants: - Foobar: 'baz' -``` - -След стартиране на приложението ще бъде създадена константата `Foobar`. - -.[note] -Константите не трябва да служат като някакви глобално достъпни променливи. За предаване на стойности към обекти използвайте [dependency injection |dependency-injection:passing-dependencies]. - - -PHP -=== - -Настройка на PHP директиви. Преглед на всички директиви ще намерите на [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -DI сървиси -========== - -Тези сървиси се добавят към DI контейнера: - -| Име | Тип | Описание -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [стартер на цялото приложение |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | фабрика за презентери -| `application.###` | [api:Nette\Application\UI\Presenter] | отделни презентери -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | фабрика за обект `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | фабрика за [`$this->template` |templates] diff --git a/application/bg/creating-links.texy b/application/bg/creating-links.texy deleted file mode 100644 index 0c0bb9c665..0000000000 --- a/application/bg/creating-links.texy +++ /dev/null @@ -1,286 +0,0 @@ -Създаване на URL връзки -*********************** - -<div class=perex> - -Създаването на връзки в Nette е лесно като посочване с пръст. Достатъчно е само да насочите и фреймуъркът вече ще свърши цялата работа вместо вас. Ще покажем: - -- как да създаваме връзки в шаблони и другаде -- как да различим връзка към текущата страница -- какво да правим с невалидни връзки - -</div> - - -Благодарение на [двупосочното маршрутизиране |routing] никога няма да се налага да записвате твърдо URL адреси на вашето приложение в шаблони или код, които могат по-късно да се променят, или сложно да ги сглобявате. Във връзката е достатъчно да посочите презентера и действието, да предадете евентуални параметри и фреймуъркът вече ще генерира URL сам. Всъщност е много подобно на извикването на функция. Това ще ви хареса. - - -В шаблона на презентера -======================= - -Най-често създаваме връзки в шаблони и страхотен помощник е атрибутът `n:href`: - -```latte -<a n:href="Product:show">детайл</a> -``` - -Забележете, че вместо HTML атрибута `href` използвахме [n:атрибут |latte:syntax#n:атрибути] `n:href`. Неговата стойност тогава не е URL, както би било в случая с атрибута `href`, а името на презентера и действието. - -Кликването върху връзка е, опростено казано, нещо като извикване на метода `ProductPresenter::renderShow()`. И ако той има параметри в своята сигнатура, можем да го извикаме с аргументи: - -```latte -<a n:href="Product:show $product->id, $product->slug">детайл на продукта</a> -``` - -Възможно е да се предават и именувани параметри. Следващата връзка предава параметъра `lang` със стойност `cs`: - -```latte -<a n:href="Product:show $product->id, lang: cs">детайл на продукта</a> -``` - -Ако методът `ProductPresenter::renderShow()` няма `$lang` в своята сигнатура, може да разбере стойността на параметъра с помощта на `$lang = $this->getParameter('lang')` или от [свойство |presenters#Параметри на заявката]. - -Ако параметрите са съхранени в масив, могат да се разгърнат с оператора `...` (в Latte 2.x с оператора `(expand)`): - -```latte -{var $args = [$product->id, lang => cs]} -<a n:href="Product:show ...$args">детайл на продукта</a> -``` - -Във връзките автоматично се предават и т.нар. [персистентни параметри |presenters#Персистентни параметри]. - -Атрибутът `n:href` е много удобен за HTML тагове `<a>`. Ако искаме да изпишем връзка другаде, например в текст, използваме `{link}`: - -```latte -Адресът е: {link Home:default} -``` - - -В кода -====== - -За създаване на връзка в презентера служи методът `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Параметрите могат да се предадат и с помощта на масив, където могат да се посочат и именувани параметри: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); -``` - -Връзки могат да се създават и без презентер, за това е тук [#LinkGenerator] и неговият метод `link()`. - - -Връзки към презентер -==================== - -Ако целта на връзката е презентер и действие, тя има следния синтаксис: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Форматът се поддържа от всички тагове на Latte и всички методи на презентера, които работят с връзки, т.е. `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` и също [#LinkGenerator]. Така че, дори ако в примерите е използван `n:href`, там може да бъде която и да е от функциите. - -Основната форма е следователно `Presenter:action`: - -```latte -<a n:href="Home:default">начална страница</a> -``` - -Ако свързваме към действие на текущия презентер, можем да пропуснем неговото име: - -```latte -<a n:href="default">начална страница</a> -``` - -Ако целта е действието `default`, можем да го пропуснем, но двоеточието трябва да остане: - -```latte -<a n:href="Home:">начална страница</a> -``` - -Връзките могат също да сочат към други [модули |directory-structure#Презентери и шаблони]. Тук връзките се разграничават на относителни към вложен подмодул или абсолютни. Принципът е аналогичен на пътищата на диска, само че вместо наклонени черти има двоеточия. Да предположим, че текущият презентер е част от модула `Front`, тогава ще запишем: - -```latte -<a n:href="Shop:Product:show">връзка към Front:Shop:Product:show</a> -<a n:href=":Admin:Product:show">връзка към Admin:Product:show</a> -``` - -Специален случай е връзка [към себе си |#Връзка към текущата страница], когато като цел посочим `this`. - -```latte -<a n:href="this">обнови</a> -``` - -Можем да свързваме към определена част от страницата чрез т.нар. фрагмент след знака диез `#`: - -```latte -<a n:href="Home:#main">връзка към Home:default и фрагмент #main</a> -``` - - -Абсолютни пътища -================ - -Връзките, генерирани с помощта на `link()` или `n:href`, са винаги абсолютни пътища (т.е. започват със знак `/`), но не и абсолютни URL с протокол и домейн като `https://domain`. - -За да генерирате абсолютен URL, добавете в началото две наклонени черти (напр. `n:href="//Home:"`). Или може да превключите презентера да генерира само абсолютни връзки, като зададете `$this->absoluteUrls = true`. - - -Връзка към текущата страница -============================ - -Целта `this` създава връзка към текущата страница: - -```latte -<a n:href="this">обнови</a> -``` - -Същевременно се пренасят и всички параметри, посочени в сигнатурата на метода `action<Action>()` или `render<View>()`, ако `action<Action>()` не е дефинирана. Така че, ако сме на страницата `Product:show` и `id: 123`, връзката към `this` ще предаде и този параметър. - -Разбира се, възможно е параметрите да се специфицират директно: - -```latte -<a n:href="this refresh: 1">обнови</a> -``` - -Функцията `isLinkCurrent()` проверява дали целта на връзката е същата като текущата страница. Това може да се използва например в шаблон за разграничаване на връзки и др. - -Параметрите са същите като при метода `link()`, но освен това е възможно вместо конкретно действие да се посочи заместващ знак `*`, който означава всяко действие на дадения презентер. - -```latte -{if !isLinkCurrent('Admin:login')} - <a n:href="Admin:login">Влезте</a> -{/if} - -<li n:class="isLinkCurrent('Product:*') ? active"> - <a n:href="Product:">...</a> -</li> -``` - -В комбинация с `n:href` в един елемент може да се използва съкратена форма: - -```latte -<a n:class="isLinkCurrent() ? active" n:href="Home:">...</a> -``` - -Заместващият знак `*` може да се използва само вместо действие, а не презентер. - -За да проверим дали сме в определен модул или негов подмодул, използваме метода `isModuleCurrent(moduleName)`. - -```latte -<li n:class="isModuleCurrent('Forum:Users') ? active"> - <a n:href="Product:">...</a> -</li> -``` - - -Връзки към сигнал -================= - -Целта на връзката не трябва да бъде само презентер и действие, но и [сигнал |components#Сигнал] (извикват метода `handle<Signal>()`). Тогава синтаксисът е следният: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -Сигналът следователно се отличава с удивителен знак: - -```latte -<a n:href="click!">сигнал</a> -``` - -Може да се създаде и връзка към сигнал на подкомпонент (или под-подкомпонент): - -```latte -<a n:href="componentName:click!">сигнал</a> -``` - - -Връзки в компонент -================== - -Тъй като [компонентите|components] са самостоятелни цялости за многократна употреба, които не трябва да имат никакви връзки с околните презентери, връзките тук работят малко по-различно. Атрибутът на Latte `n:href` и тагът `{link}` и методите на компонента като `link()` и други считат целта на връзката **винаги за име на сигнал**. Затова не е необходимо дори да се посочва удивителен знак: - -```latte -<a n:href="click">сигнал, а не действие</a> -``` - -Ако искаме в шаблона на компонента да свързваме към презентери, ще използваме за това тага `{plink}`: - -```latte -<a href={plink Home:default}>начало</a> -``` - -или в кода - -```php -$this->getPresenter()->link('Home:default') -``` - - -Псевдоними .{data-version:v3.2.2} -================================= - -Понякога може да е полезно да се присвои на двойката Presenter:действие лесно запомнящ се псевдоним. Например началната страница `Front:Home:default` да се нарече просто `home` или `Admin:Dashboard:default` като `admin`. - -Псевдонимите се дефинират в [конфигурацията|configuration] под ключа `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -Във връзките след това се записват с помощта на знак @, например: - -```latte -<a n:href="@admin">администрация</a> -``` - -Поддържат се и във всички методи, работещи с връзки, като `redirect()` и подобни. - - -Невалидни връзки -================ - -Може да се случи да създадем невалидна връзка - или защото води към несъществуващ презентер, или защото предава повече параметри, отколкото целевият метод приема в своята сигнатура, или когато за целевото действие не може да се генерира URL. Как да се постъпи с невалидните връзки определя статичната променлива `Presenter::$invalidLinkMode`. Тя може да приема комбинация от тези стойности (константи): - -- `Presenter::InvalidLinkSilent` - тих режим, като URL се връща знак # -- `Presenter::InvalidLinkWarning` - хвърля се предупреждение E_USER_WARNING, което в продукционен режим ще бъде записано в лога, но няма да предизвика прекъсване на изпълнението на скрипта -- `Presenter::InvalidLinkTextual` - визуално предупреждение, изписва грешката директно във връзката -- `Presenter::InvalidLinkException` - хвърля се изключение InvalidLinkException - -Настройката по подразбиране е `InvalidLinkWarning` в продукционен режим и `InvalidLinkWarning | InvalidLinkTextual` в режим на разработка. `InvalidLinkWarning` в продукционна среда не предизвиква прекъсване на скрипта, но предупреждението ще бъде записано в лога. В режим на разработка то се улавя от [Tracy |tracy:] и показва bluescreen. `InvalidLinkTextual` работи така, че като URL връща съобщение за грешка, което започва със знаците `#error:`. За да бъдат такива връзки забележими на пръв поглед, ще добавим към CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Ако не искаме в режим на разработка да се генерират предупреждения, можем да настроим тих режим директно в [конфигурацията|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Как да създаваме връзки с подобен комфорт като метода `link()`, но без присъствието на презентер? За това е тук [api:Nette\Application\LinkGenerator]. - -LinkGenerator е сървис, който можете да си поискате чрез конструктор и след това да създавате връзки с неговия метод `link()`. - -В сравнение с презентерите тук има разлика. LinkGenerator създава всички връзки директно като абсолютни URL. И освен това не съществува "текущ презентер", така че не може като цел да се посочи само името на действието `link('default')` или да се посочват относителни пътища към модули. - -Невалидните връзки винаги хвърлят `Nette\Application\UI\InvalidLinkException`. diff --git a/application/bg/directory-structure.texy b/application/bg/directory-structure.texy deleted file mode 100644 index 1d782640dc..0000000000 --- a/application/bg/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Директорийна структура на приложението -************************************** - -<div class=perex> - -Как да проектираме ясна и мащабируема директорийна структура за проекти в Nette Framework? Ще покажем доказани практики, които ще ви помогнат с организацията на кода. Ще научите: - -- как **логически да разделим** приложението на директории -- как да проектираме структурата така, че **добре да се мащабира** с растежа на проекта -- какви са **възможните алтернативи** и техните предимства или недостатъци - -</div> - - -Важно е да се спомене, че самият Nette Framework не налага никаква конкретна структура. Той е проектиран така, че да може лесно да се адаптира към всякакви нужди и предпочитания. - - -Основна структура на проекта -============================ - -Въпреки че Nette Framework не диктува никаква твърда директорийна структура, съществува доказано подразбиращо се подреждане под формата на [Web Project|https://github.com/nette/web-project]: - -/--pre -<b>web-project/</b> -├── <b>app/</b> ← директория с приложението -├── <b>assets/</b> ← файлове SCSS, JS, изображения..., алтернативно resources/ -├── <b>bin/</b> ← скриптове за командния ред -├── <b>config/</b> ← конфигурация -├── <b>log/</b> ← логвани грешки -├── <b>temp/</b> ← временни файлове, кеш -├── <b>tests/</b> ← тестове -├── <b>vendor/</b> ← библиотеки, инсталирани от Composer -└── <b>www/</b> ← публична директория (document-root) -\-- - -Тази структура можете свободно да променяте според вашите нужди - да преименувате или премествате папки. След това е достатъчно само да промените относителните пътища до директориите във файла `Bootstrap.php` и евентуално `composer.json`. Нищо повече не е необходимо, никаква сложна реконфигурация, никакви промени на константи. Nette разполага с умна автодетекция и автоматично разпознава местоположението на приложението, включително неговата URL основа. - - -Принципи на организация на кода -=============================== - -Когато за първи път разглеждате нов проект, трябва бързо да се ориентирате в него. Представете си, че разгръщате директорията `app/Model/` и виждате тази структура: - -/--pre -<b>app/Model/</b> -├── <b>Services/</b> -├── <b>Repositories/</b> -└── <b>Entities/</b> -\-- - -От нея разбирате само, че проектът използва някакви сървиси, репозиторита и ентитита. За истинската цел на приложението не научавате абсолютно нищо. - -Да разгледаме друг подход - **организация по домейни**: - -/--pre -<b>app/Model/</b> -├── <b>Cart/</b> -├── <b>Payment/</b> -├── <b>Order/</b> -└── <b>Product/</b> -\-- - -Тук е различно - на пръв поглед е ясно, че става въпрос за електронен магазин. Самите имена на директориите разкриват какво може приложението - работи с плащания, поръчки и продукти. - -Първият подход (организация по тип класове) носи на практика редица проблеми: код, който логически е свързан, е разпръснат в различни папки и трябва да прескачате между тях. Затова ще организираме по домейни. - - -Именни пространства -------------------- - -Прието е директорийната структура да съответства на именните пространства в приложението. Това означава, че физическото местоположение на файловете отговаря на техния namespace. Например клас, разположен в `app/Model/Product/ProductRepository.php`, трябва да има namespace `App\Model\Product`. Този принцип помага за ориентацията в кода и опростява autoloading-а. - - -Единствено срещу множествено число в имената --------------------------------------------- - -Забележете, че при основните директории на приложението използваме единствено число: `app`, `config`, `log`, `temp`, `www`. Също така и вътре в приложението: `Model`, `Core`, `Presentation`. Това е така, защото всяка от тях представлява една цялостна концепция. - -Подобно, например `app/Model/Product` представлява всичко около продуктите. Няма да го наречем `Products`, защото не става въпрос за папка, пълна с продукти (тогава там биха били файлове `nokia.php`, `samsung.php`). Това е namespace, съдържащ класове за работа с продукти - `ProductRepository.php`, `ProductService.php`. - -Папката `app/Tasks` е в множествено число, защото съдържа набор от самостоятелни изпълними скриптове - `CleanupTask.php`, `ImportTask.php`. Всеки от тях е самостоятелна единица. - -За консистентност препоръчваме да използвате: -- Единствено число за namespace, представляващ функционална цялост (макар и работещ с множество ентитита) -- Множествено число за колекции от самостоятелни единици -- В случай на несигурност или ако не искате да мислите за това, изберете единствено число - - -Публична директория `www/` -========================== - -Тази директория е единствената достъпна от уеб (т.нар. document-root). Често можете да срещнете и името `public/` вместо `www/` - това е само въпрос на конвенция и няма влияние върху функционалността на приложението. Директорията съдържа: -- [Входна точка |bootstrapping#index.php] на приложението `index.php` -- Файл `.htaccess` с правила за mod_rewrite (при Apache) -- Статични файлове (CSS, JavaScript, изображения) -- Качени файлове - -За правилното осигуряване на сигурността на приложението е от съществено значение да имате правилно [конфигуриран document-root |nette:troubleshooting#Как да промените или премахнете директорията www от URL адреса]. - -.[note] -Никога не поставяйте в тази директория папката `node_modules/` - тя съдържа хиляди файлове, които могат да бъдат изпълними и не трябва да бъдат публично достъпни. - - -Апликационна директория `app/` -============================== - -Това е основната директория с кода на приложението. Основна структура: - -/--pre -<b>app/</b> -├── <b>Core/</b> ← инфраструктурни въпроси -├── <b>Model/</b> ← бизнес логика -├── <b>Presentation/</b> ← презентери и шаблони -├── <b>Tasks/</b> ← командни скриптове -└── <b>Bootstrap.php</b> ← зареждащ клас на приложението -\-- - -`Bootstrap.php` е [стартовият клас на приложението|bootstrapping], който инициализира средата, зарежда конфигурацията и създава DI контейнер. - -Нека сега разгледаме отделните поддиректории по-подробно. - - -Презентери и шаблони -==================== - -Презентационната част на приложението имаме в директорията `app/Presentation`. Алтернатива е краткото `app/UI`. Това е мястото за всички презентери, техните шаблони и евентуални помощни класове. - -Този слой организираме по домейни. В сложен проект, който комбинира електронен магазин, блог и API, структурата би изглеждала така: - -/--pre -<b>app/Presentation/</b> -├── <b>Shop/</b> ← електронен магазин frontend -│ ├── <b>Product/</b> -│ ├── <b>Cart/</b> -│ └── <b>Order/</b> -├── <b>Blog/</b> ← блог -│ ├── <b>Home/</b> -│ └── <b>Post/</b> -├── <b>Admin/</b> ← администрация -│ ├── <b>Dashboard/</b> -│ └── <b>Products/</b> -└── <b>Api/</b> ← API endpoints - └── <b>V1/</b> -\-- - -Напротив, при прост блог бихме използвали разделяне: - -/--pre -<b>app/Presentation/</b> -├── <b>Front/</b> ← frontend на уебсайта -│ ├── <b>Home/</b> -│ └── <b>Post/</b> -├── <b>Admin/</b> ← администрация -│ ├── <b>Dashboard/</b> -│ └── <b>Posts/</b> -├── <b>Error/</b> -└── <b>Export/</b> ← RSS, sitemaps и т.н. -\-- - -Папки като `Home/` или `Dashboard/` съдържат презентери и шаблони. Папки като `Front/`, `Admin/` или `Api/` наричаме **модули**. Технически това са обикновени директории, които служат за логическо разделяне на приложението. - -Всяка папка с презентер съдържа едноименен презентер и неговите шаблони. Например папка `Dashboard/` съдържа: - -/--pre -<b>Dashboard/</b> -├── <b>DashboardPresenter.php</b> ← презентер -└── <b>default.latte</b> ← шаблон -\-- - -Тази директорийна структура се отразява в именните пространства на класовете. Например `DashboardPresenter` се намира в именното пространство `App\Presentation\Admin\Dashboard` (виж [#Мапиране на презентери]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Към презентера `Dashboard` вътре в модула `Admin` се обръщаме в приложението с помощта на нотация с двоеточие като към `Admin:Dashboard`. Към неговото действие `default` след това като към `Admin:Dashboard:default`. В случай на вложени модули използваме повече двоеточия, например `Shop:Order:Detail:default`. - - -Гъвкаво развитие на структурата -------------------------------- - -Едно от големите предимства на тази структура е колко елегантно се адаптира към растящите нужди на проекта. Като пример да вземем частта, генерираща XML фийдове. В началото имаме проста форма: - -/--pre -<b>Export/</b> -├── <b>ExportPresenter.php</b> ← един презентер за всички експорти -├── <b>sitemap.latte</b> ← шаблон за sitemap -└── <b>feed.latte</b> ← шаблон за RSS feed -\-- - -С времето се добавят други типове фийдове и се нуждаем от повече логика за тях... Няма проблем! Папката `Export/` просто става модул: - -/--pre -<b>Export/</b> -├── <b>Sitemap/</b> -│ ├── <b>SitemapPresenter.php</b> -│ └── <b>sitemap.latte</b> -└── <b>Feed/</b> - ├── <b>FeedPresenter.php</b> - ├── <b>zbozi.latte</b> ← фийд за Zboží.cz - └── <b>heureka.latte</b> ← фийд за Heureka.cz -\-- - -Тази трансформация е напълно плавна - достатъчно е да се създадат нови подпапки, да се раздели кодът в тях и да се актуализират връзките (напр. от `Export:feed` на `Export:Feed:zbozi`). Благодарение на това можем постепенно да разширяваме структурата според нуждите, нивото на влагане не е никак ограничено. - -Ако например в администрацията имате много презентери, свързани с управлението на поръчки, като `OrderDetail`, `OrderEdit`, `OrderDispatch` и т.н., можете за по-добра организираност на това място да създадете модул (папка) `Order`, в който ще бъдат (папки за) презентерите `Detail`, `Edit`, `Dispatch` и други. - - -Местоположение на шаблоните ---------------------------- - -В предишните примери видяхме, че шаблоните са разположени директно в папката с презентера: - -/--pre -<b>Dashboard/</b> -├── <b>DashboardPresenter.php</b> ← презентер -├── <b>DashboardTemplate.php</b> ← незадължителен клас за шаблона -└── <b>default.latte</b> ← шаблон -\-- - -Това местоположение на практика се оказва най-удобно - всички свързани файлове са ви веднага под ръка. - -Алтернативно можете да поставите шаблоните в подпапка `templates/`. Nette поддържа и двата варианта. Дори можете да поставите шаблоните изцяло извън папката `Presentation/`. Всичко за възможностите за разполагане на шаблони ще намерите в главата [Търсене на шаблони |templates#Търсене на шаблони]. - - -Помощни класове и компоненти ----------------------------- - -Към презентерите и шаблоните често принадлежат и други помощни файлове. Разполагаме ги логично според тяхната област на действие: - -1. **Директно при презентера** в случай на специфични компоненти за дадения презентер: - -/--pre -<b>Product/</b> -├── <b>ProductPresenter.php</b> -├── <b>ProductGrid.php</b> ← компонент за извеждане на продукти -└── <b>FilterForm.php</b> ← формуляр за филтриране -\-- - -2. **За модула** - препоръчваме да използвате папка `Accessory`, която се поставя прегледно веднага в началото на азбуката: - -/--pre -<b>Front/</b> -├── <b>Accessory/</b> -│ ├── <b>NavbarControl.php</b> ← компоненти за frontend -│ └── <b>TemplateFilters.php</b> -├── <b>Product/</b> -└── <b>Cart/</b> -\-- - -3. **За цялото приложение** - в `Presentation/Accessory/`: -/--pre -<b>app/Presentation/</b> -├── <b>Accessory/</b> -│ ├── <b>LatteExtension.php</b> -│ └── <b>TemplateFilters.php</b> -├── <b>Front/</b> -└── <b>Admin/</b> -\-- - -Или можете да поставите помощни класове като `LatteExtension.php` или `TemplateFilters.php` в инфраструктурната папка `app/Core/Latte/`. А компонентите в `app/Components`. Изборът зависи от навиците на екипа. - - -Модел - сърцето на приложението -=============================== - -Моделът съдържа цялата бизнес логика на приложението. За неговата организация важи отново правилото - структурираме по домейни: - -/--pre -<b>app/Model/</b> -├── <b>Payment/</b> ← всичко около плащанията -│ ├── <b>PaymentFacade.php</b> ← основна входна точка -│ ├── <b>PaymentRepository.php</b> -│ ├── <b>Payment.php</b> ← ентитит -├── <b>Order/</b> ← всичко около поръчките -│ ├── <b>OrderFacade.php</b> -│ ├── <b>OrderRepository.php</b> -│ ├── <b>Order.php</b> -└── <b>Shipping/</b> ← всичко около доставката -\-- - -В модела типично ще срещнете тези типове класове: - -**Фасади**: представляват основната входна точка към конкретен домейн в приложението. Действат като оркестратор, който координира сътрудничеството между различни сървиси с цел имплементиране на пълни use-cases (като "създай поръчка" или "обработи плащане"). Под своя оркестрационен слой фасадата скрива имплементационните детайли от останалата част на приложението, като по този начин предоставя чист интерфейс за работа с дадения домейн. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // валидация - // създаване на поръчка - // изпращане на имейл - // записване в статистики - } -} -``` - -**Сървиси**: фокусират се върху специфична бизнес операция в рамките на домейна. За разлика от фасадата, която оркестрира цели use-cases, сървисът имплементира конкретна бизнес логика (като изчисления на цени или обработка на плащания). Сървисите са типично безсъстоянийни и могат да бъдат използвани или от фасади като строителни блокове за по-сложни операции, или директно от други части на приложението за по-прости задачи. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // изчисление на цена - } -} -``` - -**Репозиторита**: осигуряват цялата комуникация с хранилището на данни, типично база данни. Неговата задача е зареждане и съхраняване на ентитита и имплементиране на методи за тяхното търсене. Репозиторият изолира останалата част от приложението от имплементационните детайли на базата данни и предоставя обектно-ориентиран интерфейс за работа с данни. - -```php -class OrderRepository -{ - public function find(int $id): ?Order - { - } - - public function findByCustomer(int $customerId): array - { - } -} -``` - -**Ентитита**: обекти, представляващи основните бизнес концепции в приложението, които имат своя идентичност и се променят във времето. Типично става въпрос за класове, мапнати към таблици в базата данни с помощта на ORM (като Nette Database Explorer или Doctrine). Ентититата могат да съдържат бизнес правила, свързани с техните данни и валидационна логика. - -```php -// Ентитит, мапнат към таблицата 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 обекти**: неизменни обекти, представляващи стойности без собствена идентичност - например парична сума или имейл адрес. Две инстанции на value обект със същите стойности се считат за идентични. - - -Инфраструктурен код -=================== - -Папката `Core/` (или също `Infrastructure/`) е домът на техническата основа на приложението. Инфраструктурният код типично включва: - -/--pre -<b>app/Core/</b> -├── <b>Router/</b> ← маршрутизация и управление на URL -│ └── <b>RouterFactory.php</b> -├── <b>Security/</b> ← автентикация и авторизация -│ ├── <b>Authenticator.php</b> -│ └── <b>Authorizator.php</b> -├── <b>Logging/</b> ← логване и мониторинг -│ ├── <b>SentryLogger.php</b> -│ └── <b>FileLogger.php</b> -├── <b>Cache/</b> ← кеширащ слой -│ └── <b>FullPageCache.php</b> -└── <b>Integration/</b> ← интеграция с външни сървиси - ├── <b>Slack/</b> - └── <b>Stripe/</b> -\-- - -При по-малки проекти, разбира се, е достатъчно плоско разделяне: - -/--pre -<b>Core/</b> -├── <b>RouterFactory.php</b> -├── <b>Authenticator.php</b> -└── <b>QueueMailer.php</b> -\-- - -Става въпрос за код, който: - -- Решава техническата инфраструктура (маршрутизация, логване, кеширане) -- Интегрира външни сървиси (Sentry, Elasticsearch, Redis) -- Предоставя основни сървиси за цялото приложение (поща, база данни) -- Е предимно независим от конкретния домейн - кешът или логерът работи еднакво за електронен магазин или блог. - -Чудите се дали определен клас принадлежи тук, или към модела? Ключовата разлика е в това, че кодът в `Core/`: - -- Не знае нищо за домейна (продукти, поръчки, статии) -- Е предимно възможно да се пренесе в друг проект -- Решава "как работи" (как да се изпрати имейл), а не "какво прави" (какъв имейл да се изпрати) - -Пример за по-добро разбиране: - -- `App\Core\MailerFactory` - създава инстанции на клас за изпращане на имейли, решава SMTP настройките -- `App\Model\OrderMailer` - използва `MailerFactory` за изпращане на имейли за поръчки, знае техните шаблони и кога трябва да се изпратят - - -Командни скриптове -================== - -Приложенията често трябва да извършват дейности извън обичайните HTTP заявки - било то обработка на данни във фонов режим, поддръжка или периодични задачи. За стартиране служат прости скриптове в директорията `bin/`, самата имплементационна логика след това поставяме в `app/Tasks/` (евентуално `app/Commands/`). - -Пример: - -/--pre -<b>app/Tasks/</b> -├── <b>Maintenance/</b> ← скриптове за поддръжка -│ ├── <b>CleanupCommand.php</b> ← изтриване на стари данни -│ └── <b>DbOptimizeCommand.php</b> ← оптимизация на базата данни -├── <b>Integration/</b> ← интеграция с външни системи -│ ├── <b>ImportProducts.php</b> ← импорт от доставчикова система -│ └── <b>SyncOrders.php</b> ← синхронизация на поръчки -└── <b>Scheduled/</b> ← редовни задачи - ├── <b>NewsletterCommand.php</b> ← разпращане на бюлетини - └── <b>ReminderCommand.php</b> ← нотификации към клиенти -\-- - -Какво принадлежи към модела и какво към командните скриптове? Например логиката за изпращане на един имейл е част от модела, масовото разпращане на хиляди имейли вече принадлежи към `Tasks/`. - -Задачите обикновено [стартираме от командния ред |https://blog.nette.org/en/cli-scripts-in-nette-application] или чрез cron. Могат да се стартират и чрез HTTP заявка, но е необходимо да се мисли за сигурността. Презентерът, който стартира задачата, трябва да бъде защитен, например само за влезли потребители или със силен токен и достъп от разрешени IP адреси. При дълги задачи е необходимо да се увеличи времевият лимит на скрипта и да се използва `session_write_close()`, за да не се заключва сесията. - - -Други възможни директории -========================= - -Освен споменатите основни директории, можете според нуждите на проекта да добавите други специализирани папки. Да разгледаме най-често срещаните от тях и тяхното използване: - -/--pre -<b>app/</b> -├── <b>Api/</b> ← логика за API, независима от презентационния слой -├── <b>Database/</b> ← миграционни скриптове и seeders за тестови данни -├── <b>Components/</b> ← споделени визуални компоненти в цялото приложение -├── <b>Event/</b> ← полезно, ако използвате event-driven архитектура -├── <b>Mail/</b> ← имейл шаблони и свързана логика -└── <b>Utils/</b> ← помощни класове -\-- - -За споделени визуални компоненти, използвани в презентерите в цялото приложение, може да се използва папка `app/Components` или `app/Controls`: - -/--pre -<b>app/Components/</b> -├── <b>Form/</b> ← споделени формулярни компоненти -│ ├── <b>SignInForm.php</b> -│ └── <b>UserForm.php</b> -├── <b>Grid/</b> ← компоненти за извеждане на данни -│ └── <b>DataGrid.php</b> -└── <b>Navigation/</b> ← навигационни елементи - ├── <b>Breadcrumbs.php</b> - └── <b>Menu.php</b> -\-- - -Тук принадлежат компоненти, които имат по-сложна логика. Ако искате да споделяте компоненти между няколко проекта, е препоръчително да ги изнесете в отделен composer пакет. - -В директорията `app/Mail` можете да поставите управлението на имейл комуникацията: - -/--pre -<b>app/Mail/</b> -├── <b>templates/</b> ← имейл шаблони -│ ├── <b>order-confirmation.latte</b> -│ └── <b>welcome.latte</b> -└── <b>OrderMailer.php</b> -\-- - - -Мапиране на презентери -====================== - -Мапирането дефинира правила за извеждане на името на класа от името на презентера. Специфицираме ги в [конфигурацията|configuration] под ключа `application › mapping`. - -На тази страница показахме, че поставяме презентерите в папка `app/Presentation` (евентуално `app/UI`). Тази конвенция трябва да съобщим на Nette в конфигурационния файл. Достатъчен е един ред: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Как работи мапирането? За по-добро разбиране първо си представете приложение без модули. Искаме класовете на презентерите да попадат в именното пространство `App\Presentation`, така че презентерът `Home` да се мапира към класа `App\Presentation\HomePresenter`. Което постигаме с тази конфигурация: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Мапирането работи така, че името на презентера `Home` замества звездичката в маската `App\Presentation\*Presenter`, с което получаваме крайния име на класа `App\Presentation\HomePresenter`. Просто! - -Както обаче виждате в примерите в тази и други глави, класовете на презентерите поставяме в едноименни поддиректории, например презентерът `Home` се мапира към класа `App\Presentation\Home\HomePresenter`. Това постигаме с удвояване на двоеточието (изисква Nette Application 3.2): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Сега ще пристъпим към мапиране на презентери в модули. За всеки модул можем да дефинираме специфично мапиране: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -Според тази конфигурация презентерът `Front:Home` се мапира към класа `App\Presentation\Front\Home\HomePresenter`, докато презентерът `Api:OAuth` към класа `App\Api\OAuthPresenter`. - -Тъй като модулите `Front` и `Admin` имат подобен начин на мапиране и такива модули най-вероятно ще бъдат повече, е възможно да се създаде общо правило, което да ги замени. В маската на класа така ще се добави нова звездичка за модула: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Това работи и за по-дълбоко вложени директорийни структури, като например презентер `Admin:User:Edit`, сегментът със звездичка се повтаря за всяко ниво и резултатът е клас `App\Presentation\Admin\User\Edit\EditPresenter`. - -Алтернативен запис е вместо низ да се използва масив, състоящ се от три сегмента. Този запис е еквивалентен на предходния: - -```neon -application: - mapping: - *: [App\Presentation, *, **Presenter] - Api: [App\Api, '', *Presenter] -``` diff --git a/application/bg/how-it-works.texy b/application/bg/how-it-works.texy deleted file mode 100644 index 2015bc5145..0000000000 --- a/application/bg/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Как работят приложенията? -************************* - -<div class=perex> - -Току-що прочетохте основния документ на документацията на Nette. Ще научите целия принцип на работа на уеб приложенията. От А до Я, от момента на създаването до последния дъх на PHP скрипта. След като прочетете, ще знаете: - -- как работи всичко -- какво е Bootstrap, Presenter и DI контейнер -- как изглежда директорийната структура - -</div> - - -Директорийна структура -====================== - -Отворете примера за скелет на уеб приложение, наречен [WebProject|https://github.com/nette/web-project], и докато четете, можете да разглеждате файловете, за които става въпрос. - -Директорийната структура изглежда приблизително така: - -/--pre -<b>web-project/</b> -├── <b>app/</b> ← директория с приложението -│ ├── <b>Core/</b> ← основни класове, необходими за работа -│ │ └── <b>RouterFactory.php</b> ← конфигурация на URL адреси -│ ├── <b>Presentation/</b> ← презентери, шаблони и др. -│ │ ├── <b>@layout.latte</b> ← шаблон на лейаута -│ │ └── <b>Home/</b> ← директория на презентера Home -│ │ ├── <b>HomePresenter.php</b> ← клас на презентера Home -│ │ └── <b>default.latte</b> ← шаблон на действието default -│ └── <b>Bootstrap.php</b> ← зареждащ клас Bootstrap -├── <b>assets/</b> ← ресурси (SCSS, TypeScript, изходни изображения) -├── <b>bin/</b> ← скриптове, стартирани от командния ред -├── <b>config/</b> ← конфигурационни файлове -│ ├── <b>common.neon</b> -│ └── <b>services.neon</b> -├── <b>log/</b> ← логвани грешки -├── <b>temp/</b> ← временни файлове, кеш, … -├── <b>vendor/</b> ← библиотеки, инсталирани от Composer -│ ├── ... -│ └── <b>autoload.php</b> ← autoloading на всички инсталирани пакети -├── <b>www/</b> ← публична директория или document-root на проекта -│ ├── <b>assets/</b> ← компилирани статични файлове (CSS, JS, изображения, ...) -│ ├── <b>.htaccess</b> ← правила mod_rewrite -│ └── <b>index.php</b> ← първоначален файл, с който се стартира приложението -└── <b>.htaccess</b> ← забранява достъпа до всички директории освен www -\-- - -Директорийната структура можете да променяте както искате, да преименувате или премествате папки, тя е напълно гъвкава. Nette освен това разполага с умна автодетекция и автоматично разпознава местоположението на приложението, включително неговата URL основа. - -При малко по-големи приложения можем [да разделим папките с презентери и шаблони на поддиректории |directory-structure#Презентери и шаблони] и класовете на именни пространства, които наричаме модули. - -Директорията `www/` представлява т.нар. публична директория или document-root на проекта. Можете да я преименувате без нужда от каквото и да било друго настройване от страна на приложението. Само е необходимо [да конфигурирате хостинга |nette:troubleshooting#Как да промените или премахнете директорията www от URL адреса] така, че document-root да сочи към тази директория. - -WebProject можете също така директно да изтеглите, включително Nette, с помощта на [Composer |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -На Linux или macOS задайте на директориите `log/` и `temp/` [права за запис |nette:troubleshooting#Настройка на правата на директориите]. - -Приложението WebProject е готово за стартиране, не е необходимо изобщо нищо да се конфигурира и можете директно да го покажете в браузъра, като достъпите папката `www/`. - - -HTTP заявка -=========== - -Всичко започва в момента, когато потребителят отвори страница в браузъра. Тоест, когато браузърът почука на сървъра с HTTP заявка. Заявката сочи към единствен PHP файл, който се намира в публичната директория `www/`, и това е `index.php`. Да кажем, че става въпрос за заявка към адреса `https://example.com/product/123`. Благодарение на подходящо [настройване на сървъра |nette:troubleshooting#Как да настроите сървъра за красиви URL адреси], и този URL се мапва към файла `index.php` и той се изпълнява. - -Неговата задача е: - -1) да инициализира средата -2) да получи фабриката -3) да стартира Nette приложението, което ще обработи заявката - -Каква фабрика? Не произвеждаме трактори, а уеб страници! Изчакайте, веднага ще се изясни. - -С думите „инициализация на средата“ имаме предвид например това, че се активира [Tracy|tracy:], което е страхотен инструмент за логване или визуализация на грешки. На продукционен сървър той логва грешки, на сървър за разработка ги показва директно. Следователно към инициализацията принадлежи и решението дали уебсайтът работи в продукционна или развойна среда. За това Nette използва [умна автодетекция |bootstrapping#Режим за разработка срещу продукционен режим]: ако стартирате уебсайта на localhost, той работи в развойна среда. Не е необходимо нищо да конфигурирате и приложението е веднага готово както за разработка, така и за реално внедряване. Тези стъпки се извършват и са подробно описани в главата за [клас Bootstrap|bootstrapping]. - -Третата точка (да, прескочихме втората, но ще се върнем към нея) е стартирането на приложението. Обработката на HTTP заявки в Nette се извършва от класа `Nette\Application\Application` (наричан по-нататък `Application`), така че когато казваме стартиране на приложението, имаме предвид конкретно извикване на метода със знаковото име `run()` върху обекта на този клас. - -Nette е ментор, който ви води към писането на чисти приложения според доказани методики. И една от тези абсолютно най-доказани се нарича **dependency injection**, съкратено DI. В този момент не искаме да ви натоварваме с обяснение на DI, за това има [отделна глава|dependency-injection:introduction], същественото последствие е, че ключовите обекти обикновено ще ни ги създава фабрика за обекти, която се нарича **DI контейнер** (съкратено DIC). Да, това е фабриката, за която стана дума преди малко. И тя ще ни произведе и обекта `Application`, затова първо се нуждаем от контейнера. Получаваме го с помощта на класа `Configurator` и го караме да произведе обекта `Application`, извикваме върху него метода `run()` и така се стартира Nette приложението. Точно това се случва във файла [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -Класът Application има една-единствена задача: да отговори на HTTP заявка. - -Приложенията, написани на Nette, се разделят на много т.нар. презентери (в други фреймуърци може да срещнете термина controller, става въпрос за същото), които са класове, всеки от които представлява някаква конкретна страница на уебсайта: напр. начална страница; продукт в електронен магазин; формуляр за вход; sitemap feed и т.н. Приложението може да има от един до хиляди презентери. - -Application започва с това, че моли т.нар. рутер да реши на кой от презентерите да предаде текущата заявка за обработка. Рутерът решава чия е отговорността. Поглежда входния URL `https://example.com/product/123` и въз основа на това как е настроен, решава, че това е работа напр. за **презентера** `Product`, от който ще иска като **действие** показване (`show`) на продукта с `id: 123`. Двойката презентер + действие е добър навик да се записва, разделена с двоеточие, като `Product:show`. - -Следователно рутерът трансформира URL в двойка `Presenter:action` + параметри, в нашия случай `Product:show` + `id: 123`. Как изглежда такъв рутер можете да видите във файла `app/Core/RouterFactory.php` и го описваме подробно в главата [Маршрутизация |Routing]. - -Да продължим нататък. Application вече знае името на презентера и може да продължи напред. Като произведе обект от класа `ProductPresenter`, което е кодът на презентера `Product`. По-точно казано, моли DI контейнера да произведе презентера, защото производството е негова работа. - -Презентерът може да изглежда например така: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // получаваме данни от модела и ги предаваме на шаблона - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Обработката на заявката се поема от презентера. И задачата е ясна: извърши действието `show` с `id: 123`. Което на езика на презентерите означава, че се извиква методът `renderShow()` и в параметъра `$id` получава `123`. - -Презентерът може да обслужва повече действия, т.е. да има повече методи `render<Action>()`. Но препоръчваме да проектирате презентери с едно или възможно най-малко действия. - -Така че, извика се методът `renderShow(123)`, чийто код е измислен пример, но можете да видите на него как се предават данни към шаблона, т.е. със запис в `$this->template`. - -Впоследствие презентерът връща отговор. Той може да бъде HTML страница, изображение, XML документ, изпращане на файл от диска, JSON или например пренасочване към друга страница. Важно е, че ако изрично не кажем как трябва да отговори (което е случаят с `ProductPresenter`), отговорът ще бъде рендиране на шаблон с HTML страница. Защо? Защото в 99% от случаите искаме да рендираме шаблон, следователно презентерът приема това поведение като подразбиращо се и иска да ни улесни работата. Това е смисълът на Nette. - -Не е необходимо дори да посочваме кой шаблон да се рендира, пътят до него се извежда сам. В случай на действие `show` просто се опитва да зареди шаблона `show.latte` в директорията с класа `ProductPresenter`. Също така се опитва да намери лейаут във файла `@layout.latte` (по-подробно за [намиране на шаблони |templates#Търсене на шаблони]). - -И впоследствие рендира шаблоните. С това задачата на презентера и на цялото приложение е изпълнена и делото е завършено. Ако шаблонът не съществува, се връща страница с грешка 404. Повече за презентерите ще прочетете на страницата [Презентери|presenters]. - -[* request-flow.svg *] - -За всеки случай, нека опитаме да рекапитулираме целия процес с малко по-различен URL: - -1) URL ще бъде `https://example.com` -2) зареждаме приложението, създава се контейнер и се стартира `Application::run()` -3) рутерът декодира URL като двойка `Home:default` -4) създава се обект от класа `HomePresenter` -5) извиква се методът `renderDefault()` (ако съществува) -6) рендира се шаблон напр. `default.latte` с лейаут напр. `@layout.latte` - - -Може би сега сте се сблъскали с голям брой нови понятия, но вярваме, че те имат смисъл. Създаването на приложения в Nette е огромно удоволствие. - - -Шаблони -======= - -Когато вече стана дума за шаблони, в Nette се използва шаблониращата система [Latte |latte:]. Затова и тези разширения `.latte` при шаблоните. Latte се използва от една страна, защото е най-добре защитената шаблонираща система за PHP, а същевременно и най-интуитивната система. Не е необходимо да учите много нови неща, достатъчно е да познавате PHP и няколко тага. Всичко ще научите [в документацията |templates]. - -В шаблона се [създават връзки |creating-links] към други презентери и действия по следния начин: - -```latte -<a n:href="Product:show $productId">детайл на продукта</a> -``` - -Просто вместо реален URL напишете познатата двойка `Presenter:action` и посочете евентуални параметри. Трикът е в `n:href`, което казва, че този атрибут ще бъде обработен от Nette. И ще генерира: - -```latte -<a href="/product/456">детайл на продукта</a> -``` - -Генерирането на URL се извършва от вече споменатия рутер. Всъщност рутерите в Nette са изключителни с това, че могат да извършват не само трансформации от URL към двойка presenter:action, но и обратно, т.е. от името на презентера + действието + параметрите да генерират URL. Благодарение на това в Nette можете напълно да промените формите на URL в цялото готово приложение, без да променяте нито един знак в шаблона или презентера. Само като промените рутера. Също така благодарение на това работи т.нар. канонизация, което е друга уникална характеристика на Nette, която допринася за по-добро SEO (оптимизация за намиране в интернет), като автоматично предотвратява съществуването на дублирано съдържание на различни URL адреси. Много програмисти смятат това за изумително. - - -Интерактивни компоненти -======================= - -За презентерите трябва да ви разкрием още нещо: те имат вградена компонентна система. Нещо подобно може да е познато на ветераните от Delphi или ASP.NET Web Forms, на нещо отдалечено подобно са базирани React или Vue.js. В света на PHP фреймуърците това е абсолютно уникално явление. - -Компонентите са самостоятелни цялости за многократна употреба, които вмъкваме в страниците (т.е. презентерите). Могат да бъдат [формуляри |forms:in-presenter], [datagrid-ове |https://componette.org/contributte/datagrid/], менюта, анкети за гласуване, всъщност всичко, което има смисъл да се използва многократно. Можем да създаваме собствени компоненти или да използваме някои от [огромното предлагане |https://componette.org] на open source компоненти. - -Компонентите фундаментално влияят на подхода към създаването на приложения. Ще ви отворят нови възможности за сглобяване на страници от предварително подготвени единици. И освен това имат нещо общо с [Холивуд |components#Hollywood style]. - - -DI контейнер и конфигурация -=========================== - -DI контейнерът, или фабриката за обекти, е сърцето на цялото приложение. - -Не се притеснявайте, това не е никаква магическа черна кутия, както може би изглежда от предишните редове. Всъщност това е един доста скучен PHP клас, който Nette генерира и съхранява в директорията с кеша. Има много методи, наречени като `createServiceAbcd()`, и всеки от тях може да произведе и върне някакъв обект. Да, там има и метод `createServiceApplication()`, който произвежда `Nette\Application\Application`, който ни беше необходим във файла `index.php` за стартиране на приложението. И има методи, произвеждащи отделните презентери. И така нататък. - -Обектите, които DI контейнерът създава, по някаква причина се наричат сървиси. - -Това, което е наистина специално в този клас, е, че не го програмирате вие, а фреймуъркът. Той наистина генерира PHP код и го съхранява на диска. Вие само давате инструкции какви обекти трябва да може да произвежда контейнерът и как точно. И тези инструкции са записани в [конфигурационни файлове |bootstrapping#Конфигурация на DI контейнера], за които се използва форматът [NEON|neon:format] и следователно имат и разширение `.neon`. - -Конфигурационните файлове служат чисто за инструктиране на DI контейнера. Така че, когато например посоча в секцията [session |http:configuration#Сесия] опцията `expiration: 14 days`, DI контейнерът при създаването на обекта `Nette\Http\Session`, представляващ сесията, ще извика неговия метод `setExpiration('14 days')` и така конфигурацията ще стане реалност. - -Има подготвена за вас цяла глава, описваща какво всичко може да се [конфигурира |nette:configuring] и как да се [дефинират собствени сървиси |dependency-injection:services]. - -Щом малко навлезете в създаването на сървиси, ще се сблъскате с думата [autowiring |dependency-injection:autowiring]. Това е хитринка, която по невероятен начин ще ви улесни живота. Може автоматично да предава обекти там, където ги имате нужда (например в конструкторите на вашите класове), без да е необходимо да правите каквото и да било. Ще откриете, че DI контейнерът в Nette е малко чудо. - - -Накъде да продължим? -==================== - -Преминахме през основните принципи на приложенията в Nette. Засега много повърхностно, но скоро ще навлезете в дълбочина и с времето ще създадете прекрасни уеб приложения. Накъде да продължим? Опитахте ли вече урока [Пишем първото приложение|quickstart:]? - -Освен описаното по-горе, Nette разполага с цял арсенал от [полезни класове|utils:], [слой за работа с бази данни|database:] и т.н. Опитайте просто да прегледате документацията. Или [блога|https://blog.nette.org]. Ще откриете много интересно. - -Нека фреймуъркът ви носи много радост 💙 diff --git a/application/bg/multiplier.texy b/application/bg/multiplier.texy deleted file mode 100644 index 0ca2717c6f..0000000000 --- a/application/bg/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: динамични компоненти -******************************** - -.[perex] -Инструмент за динамично създаване на интерактивни компоненти - -Да започнем с типичен пример: имаме списък със стоки в електронен магазин, като за всяка искаме да покажем формуляр за добавяне на стоката в количката. Един от възможните варианти е да обвием целия списък в един формуляр. Много по-удобен начин обаче ни предлага [api:Nette\Application\UI\Multiplier]. - -Multiplier позволява удобно да се дефинира фабрика за множество компоненти. Работи на принципа на вложените компоненти - всеки компонент, наследяващ [api:Nette\ComponentModel\Container], може да съдържа други компоненти. - -.[tip] -Вижте главата за [компонентния модел |components#Компоненти в дълбочина] в документацията или [лекцията от Honza Tvrdík|https://www.youtube.com/watch?v=8y3LLexWu-I]. - -Същността на Multiplier е, че той действа като родител, който може да създава своите потомци динамично с помощта на callback, предаден в конструктора. Вижте примера: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Брой стоки:') - ->setRequired(); - $form->addSubmit('send', 'Добави в количката'); - return $form; - }); -} -``` - -Сега можем в шаблона лесно при всяка стока да накараме да се рендира формуляр - и всеки ще бъде наистина уникален компонент. - -```latte -{foreach $items as $item} - <h2>{$item->title}</h2> - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -Аргументът, предаден в тага `{control}`, е във формат, който казва: - -1. вземи компонента `shopForm` -2. и от него вземи потомъка `$item->id` - -При първото извикване на точка **1.** `shopForm` все още не съществува, така че се извиква неговата фабрика `createComponentShopForm`. Върху получения компонент (инстанция на Multiplier) след това се извиква фабриката на конкретния формуляр - което е анонимната функция, която предадохме на Multiplier в конструктора. - -В следващата итерация на foreach методът `createComponentShopForm` вече няма да бъде извикван (компонентът съществува), но тъй като търсим друг негов потомък (`$item->id` ще бъде различно във всяка итерация), отново ще бъде извикана анонимната функция и ще ни върне нов формуляр. - -Единственото, което остава, е да осигурим, че формулярът ще добави в количката наистина тази стока, която трябва - в момента формулярът при всяка стока е напълно идентичен. Ще ни помогне свойството на Multiplier (и общо на всяка фабрика за компонент в Nette Framework), а именно това, че всяка фабрика като свой първи аргумент получава името на създавания компонент. В нашия случай това ще бъде `$item->id`, което е точно информацията, от която се нуждаем. Достатъчно е леко да променим създаването на формуляра: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function ($itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Брой стоки:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Добави в количката'); - return $form; - }); -} -``` diff --git a/application/bg/presenters.texy b/application/bg/presenters.texy deleted file mode 100644 index 77de773f85..0000000000 --- a/application/bg/presenters.texy +++ /dev/null @@ -1,500 +0,0 @@ -Презентери -********** - -<div class=perex> - -Ще се запознаем с това как се пишат презентери и шаблони в Nette. След като прочетете, ще знаете: - -- как работи презентерът -- какво са персистентните параметри -- как се рендират шаблони - -</div> - -[Вече знаем |how-it-works#Nette Application], че презентерът е клас, който представлява някаква конкретна страница на уеб приложение, напр. начална страница; продукт в електронен магазин; формуляр за вход; sitemap feed и т.н. Приложението може да има от един до хиляди презентери. В други фреймуърци те се наричат и контролери. - -Обикновено под понятието презентер се разбира наследник на клас [api:Nette\Application\UI\Presenter], който е подходящ за генериране на уеб интерфейси и на който ще се посветим в останалата част от тази глава. В общ смисъл презентерът е всеки обект, имплементиращ интерфейса [api:Nette\Application\IPresenter]. - - -Жизнен цикъл на презентера -========================== - -Задачата на презентера е да обработи заявка и да върне отговор (който може да бъде HTML страница, изображение, пренасочване и т.н.). - -Следователно в началото му се предава заявка. Това не е директно HTTP заявка, а обект [api:Nette\Application\Request], в който HTTP заявката е била трансформирана с помощта на рутера. С този обект обикновено не влизаме в контакт, тъй като презентерът умно делегира обработката на заявката на други методи, които сега ще покажем. - -[* lifecycle.svg *] *** *Жизнен цикъл на презентера* .<> - -Изображението представлява списък с методи, които се извикват последователно отгоре надолу, ако съществуват. Никой от тях не е задължителен, можем да имаме напълно празен презентер без нито един метод и да изградим върху него прост статичен уебсайт. - - -`__construct()` ---------------- - -Конструкторът не принадлежи съвсем към жизнения цикъл на презентера, защото се извиква в момента на създаване на обекта. Но го споменаваме поради важността му. Конструкторът (заедно с [метода inject|best-practices:inject-method-attribute]) служи за предаване на зависимости. - -Презентерът не трябва да се занимава с бизнес логиката на приложението, да записва и чете от база данни, да извършва изчисления и т.н. За това са класовете от слоя, който наричаме модел. Например класът `ArticleRepository` може да отговаря за зареждането и съхраняването на статии. За да може презентерът да работи с него, той си го [изисква чрез dependency injection |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Веднага след получаване на заявката се извиква методът `startup()`. Можете да го използвате за инициализация на свойства, проверка на потребителски права и т.н. Изисква се методът винаги да извиква родителя `parent::startup()`. - - -`action<Action>(args...)` .{toc: action<Action>()} --------------------------------------------------- - -Аналог на метода `render<View>()`. Докато `render<View>()` е предназначен да подготви данни за конкретен шаблон, който след това се рендира, то в `action<Action>()` се обработва заявка без връзка с рендирането на шаблон. Например се обработват данни, потребителят се вписва или изписва, и така нататък, и след това [се пренасочва другаде |#Пренасочване]. - -Важно е, че `action<Action>()` се извиква преди `render<View>()`, така че в него можем евентуално да променим по-нататъшния ход на събитията, т.е. да променим шаблона, който ще се рендира, както и метода `render<View>()`, който ще се извика. И това става с помощта на `setView('jineView')`. - -На метода се предават параметри от заявката. Възможно е и се препоръчва да се посочат типове на параметрите, напр. `actionShow(int $id, ?string $slug = null)` - ако параметърът `id` липсва или ако не е integer, презентерът ще върне [грешка 404 |#Грешка 404 и др] и ще прекрати дейността си. - - -`handle<Signal>(args...)` .{toc: handle<Signal>()} --------------------------------------------------- - -Методът обработва т.нар. сигнали, с които ще се запознаем в главата, посветена на [компонентите |components#Сигнал]. Той е предназначен основно за компоненти и обработка на AJAX заявки. - -На метода се предават параметри от заявката, както в случая с `action<Action>()`, включително проверка на типа. - - -`beforeRender()` ----------------- - -Методът `beforeRender`, както подсказва името, се извиква преди всеки метод `render<View>()`. Използва се за обща конфигурация на шаблона, предаване на променливи за лейаута и подобни. - - -`render<View>(args...)` .{toc: render<View>()} ----------------------------------------------- - -Мястото, където подготвяме шаблона за последващо рендиране, предаваме му данни и т.н. - -На метода се предават параметри от заявката, както в случая с `action<Action>()`, включително проверка на типа. - -```php -public function renderShow(int $id): void -{ - // получаваме данни от модела и ги предаваме на шаблона - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -Методът `afterRender`, както отново подсказва името, се извиква след всеки метод `render<View>()`. Използва се по-скоро рядко. - - -`shutdown()` ------------- - -Извиква се в края на жизнения цикъл на презентера. - - -**Добър съвет, преди да продължим**. Презентерът, както се вижда, може да обслужва повече действия/view, т.е. да има повече методи `render<View>()`. Но препоръчваме да проектирате презентери с едно или възможно най-малко действия. - - -Изпращане на отговор -==================== - -Отговорът на презентера обикновено е [рендиране на шаблон с HTML страница|templates], но може да бъде и изпращане на файл, JSON или например пренасочване към друга страница. - -По всяко време на жизнения цикъл можем с някой от следните методи да изпратим отговор и същевременно да прекратим презентера: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` и `forward()` [пренасочват |#Пренасочване] -- `error()` прекратява презентера [поради грешка |#Грешка 404 и др] -- `sendJson($data)` прекратява презентера и [изпраща данни |#Изпращане на JSON] във формат JSON -- `sendTemplate()` прекратява презентера и веднага [рендира шаблон |templates] -- `sendResponse($response)` прекратява презентера и изпраща [собствен отговор |#Отговори] -- `terminate()` прекратява презентера без отговор - -Ако не извикате никой от тези методи, презентерът автоматично ще пристъпи към рендиране на шаблона. Защо? Защото в 99% от случаите искаме да рендираме шаблон, следователно презентерът приема това поведение като подразбиращо се и иска да ни улесни работата. - - -Създаване на връзки -=================== - -Презентерът разполага с метод `link()`, с помощта на който могат да се създават URL връзки към други презентери. Първият параметър е целевият презентер и действие, следват предаваните аргументи, които могат да бъдат посочени като масив: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'cs']); -``` - -В шаблона се създават връзки към други презентери и действия по следния начин: - -```latte -<a n:href="Product:show $id">детайл на продукта</a> -``` - -Просто вместо реален URL напишете познатата двойка `Presenter:action` и посочете евентуални параметри. Трикът е в `n:href`, което казва, че този атрибут ще бъде обработен от Latte и ще генерира реален URL. В Nette така изобщо не е необходимо да мислите за URL, само за презентери и действия. - -Повече информация ще намерите в главата [Създаване на URL връзки|creating-links]. - - -Пренасочване -============ - -За преход към друг презентер служат методите `redirect()` и `forward()`, които имат много подобен синтаксис на метода [link() |#Създаване на връзки]. - -Методът `forward()` преминава към новия презентер веднага без HTTP пренасочване: - -```php -$this->forward('Product:show'); -``` - -Пример за т.нар. временно пренасочване с HTTP код 302 (или 303, ако методът на текущата заявка е POST): - -```php -$this->redirect('Product:show', $id); -``` - -Постоянно пренасочване с HTTP код 301 постигате така: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Към друг URL извън приложението може да се пренасочи с метода `redirectUrl()`. Като втори параметър може да се посочи HTTP код, по подразбиране е 302 (или 303, ако методът на текущата заявка е POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Пренасочването веднага прекратява дейността на презентера, като хвърля т.нар. тихо прекратяващо изключение `Nette\Application\AbortException`. - -Преди пренасочване може да се изпрати [flash съобщение |#Flash съобщения], т.е. съобщения, които ще бъдат показани в шаблона след пренасочването. - - -Flash съобщения -=============== - -Това са съобщения, обикновено информиращи за резултата от някаква операция. Важна характеристика на flash съобщенията е, че те са достъпни в шаблона и след пренасочване. Дори след показване остават активни още 30 секунди – например в случай, че поради грешка при прехвърлянето потребителят обнови страницата - съобщението няма да изчезне веднага. - -Достатъчно е да извикате метода [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] и за предаването в шаблона ще се погрижи презентерът. Първият параметър е текстът на съобщението, а незадължителният втори параметър е неговият тип (error, warning, info и др.). Методът `flashMessage()` връща инстанция на flash съобщението, към което могат да се добавят допълнителни информации. - -```php -$this->flashMessage('Елементът беше изтрит.'); -$this->redirect(/* ... */); // и пренасочваме -``` - -В шаблона тези съобщения са достъпни в променливата `$flashes` като обекти `stdClass`, които съдържат свойства `message` (текст на съобщението), `type` (тип на съобщението) и могат да съдържат вече споменатите потребителски информации. Рендираме ги например така: - -```latte -{foreach $flashes as $flash} - <div class="flash {$flash->type}">{$flash->message}</div> -{/foreach} -``` - - -Грешка 404 и др. -================ - -Ако не може да се изпълни заявката, например поради това, че статията, която искаме да покажем, не съществува в базата данни, хвърляме грешка 404 с метода `error(?string $message = null, int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -HTTP кодът на грешката може да се предаде като втори параметър, по подразбиране е 404. Методът работи така, че хвърля изключение `Nette\Application\BadRequestException`, след което `Application` предава управлението на error-presenter. Което е презентер, чиято задача е да покаже страница, информираща за възникналата грешка. Настройката на error-preseter се извършва в [конфигурацията application|configuration]. - - -Изпращане на JSON -================= - -Пример за action-метод, който изпраща данни във формат JSON и прекратява презентера: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Параметри на заявката .{data-version:3.1.14} -============================================ - -Презентерът, както и всеки компонент, получава своите параметри от HTTP заявката. Тяхната стойност можете да разберете с метода `getParameter($name)` или `getParameters()`. Стойностите са низове или масиви от низове, това са по същество сурови данни, получени директно от URL. - -За по-голямо удобство препоръчваме параметрите да се достъпват чрез свойство. Достатъчно е да ги маркирате с атрибута `#[Parameter]`: - -```php -use Nette\Application\Attributes\Parameter; // този ред е важен - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // трябва да е public -} -``` - -При свойството препоръчваме да посочите и типа данни (напр. `string`) и Nette според него автоматично претипира стойността. Стойностите на параметрите могат също да бъдат [валидирани |#Валидация на параметри]. - -При създаване на връзка може директно да се зададе стойност на параметрите: - -```latte -<a n:href="Home:default theme: dark">кликни</a> -``` - - -Персистентни параметри -====================== - -Персистентните параметри служат за поддържане на състоянието между различни заявки. Тяхната стойност остава същата и след кликване върху връзка. За разлика от данните в сесията, те се пренасят в URL. И това става напълно автоматично, не е необходимо да се посочват изрично в `link()` или `n:href`. - -Пример за употреба? Имате многоезично приложение. Текущият език е параметър, който трябва постоянно да бъде част от URL. Но би било изключително уморително да го посочвате във всяка връзка. Така че го правите персистентен параметър `lang` и той ще се пренася сам. Страхотно! - -Създаването на персистентен параметър в Nette е изключително лесно. Достатъчно е да създадете публично свойство и да го маркирате с атрибут: (преди се използваше `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // този ред е важен - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // трябва да е public -} -``` - -Ако `$this->lang` има стойност например `'en'`, то и връзките, създадени с помощта на `link()` или `n:href`, ще съдържат параметъра `lang=en`. И след кликване върху връзката отново ще бъде `$this->lang = 'en'`. - -При свойството препоръчваме да посочите и типа данни (напр. `string`) и можете да посочите и стойност по подразбиране. Стойностите на параметрите могат да бъдат [валидирани |#Валидация на параметри]. - -Персистентните параметри стандартно се пренасят между всички действия на дадения презентер. За да се пренасят и между няколко презентера, е необходимо да се дефинират или: - -- в общ родител, от който презентерите наследяват -- в trait, който презентерите използват: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -При създаване на връзка може да се промени стойността на персистентния параметър: - -```latte -<a n:href="Product:show $id, lang: cs">детайл на български</a> -``` - -Или може да бъде *ресетнат*, т.е. премахнат от URL. Тогава ще приеме своята стойност по подразбиране: - -```latte -<a n:href="Product:show $id, lang: null">кликни</a> -``` - - -Интерактивни компоненти -======================= - -Презентерите имат вградена компонентна система. Компонентите са самостоятелни цялости за многократна употреба, които вмъкваме в презентерите. Могат да бъдат [формуляри |forms:in-presenter], datagrid-ове, менюта, всъщност всичко, което има смисъл да се използва многократно. - -Как се вмъкват компоненти в презентера и след това се използват? Това ще научите в главата [Компоненти |components]. Дори ще разберете какво общо имат с Холивуд. - -А къде мога да намеря компоненти? На страницата [Componette |https://componette.org/search/component] ще намерите open-source компоненти, както и редица други добавки за Nette, които са поставени тук от доброволци от общността около фреймуърка. - - -Навлизаме в дълбочина -===================== - -.[tip] -С това, което показахме досега в тази глава, най-вероятно ще се справите напълно. Следващите редове са предназначени за тези, които се интересуват от презентерите в дълбочина и искат да знаят абсолютно всичко. - - -Валидация на параметри ----------------------- - -Стойностите на [параметрите на заявката |#Параметри на заявката] и [персистентните параметри |#Персистентни параметри], получени от URL, се записват в свойствата от метода `loadState()`. Той също така проверява дали съответства типът данни, посочен при свойството, в противен случай отговаря с грешка 404 и страницата не се показва. - -Никога не вярвайте сляпо на параметрите, защото те могат лесно да бъдат презаписани от потребителя в URL. Така например ще проверим дали езикът `$this->lang` е сред поддържаните. Подходящ начин е да презапишем споменатия метод `loadState()`: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // тук се задава $this->lang - // следва собствена проверка на стойността: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Запазване и възстановяване на заявка ------------------------------------- - -Заявката, която обработва презентерът, е обект [api:Nette\Application\Request] и се връща от метода на презентера `getRequest()`. - -Текущата заявка може да се запази в сесията или обратно, да се възстанови от нея и да се остави презентерът да я изпълни отново. Това е полезно например в ситуация, когато потребителят попълва формуляр и му изтече сесията. За да не загуби данните, преди пренасочването към страницата за вход запазваме текущата заявка в сесията с помощта на `$reqId = $this->storeRequest()`, което връща нейния идентификатор под формата на кратък низ и го предаваме като параметър на презентера за вход. - -След влизане извикваме метода `$this->restoreRequest($reqId)`, който извлича заявката от сесията и пренасочва към нея. Методът при това проверява дали заявката е създадена от същия потребител, който сега се е вписал. Ако се е вписал друг потребител или ключът е невалиден, не прави нищо и програмата продължава нататък. - -Вижте ръководството [Как да се върнем към предишна страница |best-practices:restore-request]. - - -Канонизация ------------ - -Презентерите имат една наистина страхотна характеристика, която допринася за по-добро SEO (оптимизация за намиране в интернет). Те автоматично предотвратяват съществуването на дублирано съдържание на различни URL адреси. Ако към определена цел водят няколко URL адреса, напр. `/index` и `/index?page=1`, фреймуъркът определя един от тях за основен (каноничен) и останалите пренасочва към него с помощта на HTTP код 301. Благодарение на това търсачките не индексират страниците ви два пъти и не размиват техния page rank. - -Този процес се нарича канонизация. Каноничният URL е този, който генерира [рутерът|routing], обикновено първият съответстващ маршрут в колекцията. - -Канонизацията е включена по подразбиране и може да се изключи чрез `$this->autoCanonicalize = false`. - -Пренасочване не се извършва при AJAX или POST заявка, защото би довело до загуба на данни или не би имало добавена стойност от гледна точка на SEO. - -Канонизацията можете да извикате и ръчно с помощта на метода `canonicalize()`, на който, подобно на метода `link()`, се предават презентер, действие и параметри. Той създава връзка и я сравнява с текущия URL адрес. Ако се различават, пренасочва към генерираната връзка. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // пренасочва, ако $slug се различава от $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - - -Събития -------- - -Освен методите `startup()`, `beforeRender()` и `shutdown()`, които се извикват като част от жизнения цикъл на презентера, могат да се дефинират и други функции, които да се извикват автоматично. Презентерът дефинира т.нар. [събития |nette:glossary#Събития events], чиито хендлъри добавяте към масивите `$onStartup`, `$onRender` и `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Хендлърите в масива `$onStartup` се извикват точно преди метода `startup()`, след това `$onRender` между `beforeRender()` и `render<View>()` и накрая `$onShutdown` точно преди `shutdown()`. - - -Отговори --------- - -Отговорът, който връща презентерът, е обект, имплементиращ интерфейса [api:Nette\Application\Response]. На разположение са редица готови отговори: - -- [api:Nette\Application\Responses\CallbackResponse] - изпраща callback -- [api:Nette\Application\Responses\FileResponse] - изпраща файл -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - изпраща JSON -- [api:Nette\Application\Responses\RedirectResponse] - пренасочване -- [api:Nette\Application\Responses\TextResponse] - изпраща текст -- [api:Nette\Application\Responses\VoidResponse] - празен отговор - -Отговорите се изпращат с метода `sendResponse()`: - -```php -use Nette\Application\Responses; - -// Обикновен текст -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Изпраща файл -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// Отговорът ще бъде callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '<h1>Hello</h1>'; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - - -Ограничаване на достъпа с `#[Requires]` .{data-version:3.2.2} -------------------------------------------------------------- - -Атрибутът `#[Requires]` предоставя разширени възможности за ограничаване на достъпа до презентери и техните методи. Може да се използва за специфициране на HTTP методи, изискване на AJAX заявка, ограничаване до същия произход (same origin) и достъп само чрез пренасочване (forward). Атрибутът може да се прилага както към класове на презентери, така и към отделни методи `action<Action>()`, `render<View>()`, `handle<Signal>()` и `createComponent<Name>()`. - -Можете да посочите следните ограничения: -- на HTTP методи: `#[Requires(methods: ['GET', 'POST'])]` -- изискване на AJAX заявка: `#[Requires(ajax: true)]` -- достъп само от същия произход: `#[Requires(sameOrigin: true)]` -- достъп само чрез forward: `#[Requires(forward: true)]` -- ограничение до конкретни действия: `#[Requires(actions: 'default')]` - -Подробности ще намерите в ръководството [Как да използваме атрибута Requires |best-practices:attribute-requires]. - - -Проверка на HTTP метода ------------------------ - -Презентерите в Nette автоматично проверяват HTTP метода на всяка входяща заявка. Причината за тази проверка е предимно сигурността. Стандартно са разрешени методите `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. - -Ако искате да разрешите допълнително например метода `OPTIONS`, използвайте за това атрибута `#[Requires]` (от Nette Application v3.2): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Във версия 3.1 проверката се извършва в `checkHttpMethod()`, която проверява дали методът, специфициран в заявката, се съдържа в масива `$presenter->allowedMethods`. Добавянето на метод направете така: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Важно е да се подчертае, че ако разрешите метода `OPTIONS`, трябва впоследствие и да го обслужите подобаващо в рамките на вашия презентер. Методът често се използва като т.нар. preflight request, който браузърът автоматично изпраща преди реалната заявка, когато е необходимо да се провери дали заявката е разрешена от гледна точка на CORS (Cross-Origin Resource Sharing) политиката. Ако разрешите метода, но не имплементирате правилен отговор, това може да доведе до неконсистентности и потенциални проблеми със сигурността. - - -Друго четене -============ - -- [Методи и атрибути inject |best-practices:inject-method-attribute] -- [Сглобяване на презентери от trait |best-practices:presenter-traits] -- [Предаване на настройки към презентери |best-practices:passing-settings-to-presenters] -- [Как да се върнем към предишна страница |best-practices:restore-request] diff --git a/application/bg/routing.texy b/application/bg/routing.texy deleted file mode 100644 index d3a66e32fa..0000000000 --- a/application/bg/routing.texy +++ /dev/null @@ -1,721 +0,0 @@ -Маршрутизация -************* - -<div class=perex> - -Рутерът отговаря за всичко около URL адресите, за да не се налага вие да мислите за тях. Ще покажем: - -- как да настроим рутера, така че URL адресите да са според представите ни -- ще поговорим за SEO и пренасочване -- и ще покажем как да напишем собствен рутер - -</div> - - -По-човешките URL адреси (или също cool или pretty URL) са по-използваеми, по-лесно запомнящи се и допринасят положително за SEO. Nette мисли за това и излиза напълно в помощ на разработчиците. Можете да проектирате за своето приложение точно такава структура на URL адресите, каквато искате. Можете да я проектирате дори когато приложението вече е готово, защото това става без намеса в кода или шаблоните. Дефинира се по елегантен начин на едно [единствено място |#Включване в приложението], в рутера, и не е разпръснато под формата на анотации във всички презентери. - -Рутерът в Nette е изключителен с това, че е **двупосочен.** Той може както да декодира URL в HTTP заявка, така и да създава връзки. Следователно играе ключова роля в [Nette Application |how-it-works#Nette Application], защото от една страна решава кой презентер и действие ще изпълняват текущата заявка, но също така се използва за [генериране на URL |creating-links] в шаблон и т.н. - -Въпреки това, рутерът не е ограничен само до тази употреба, можете да го използвате в приложения, където изобщо не се използват презентери, за REST API и т.н. Повече в частта [#Самостоятелно използване]. - - -Колекция от маршрути -==================== - -Най-приятният начин за дефиниране на формата на URL адресите в приложението предлага класът [api:Nette\Application\Routers\RouteList]. Дефиницията се състои от списък с т.нар. маршрути, т.е. маски на URL адреси и към тях асоциирани презентери и действия с помощта на просто API. Не е необходимо да именуваме маршрутите по никакъв начин. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/<id>', 'Article:view'); -// ... -``` - -Примерът казва, че ако в браузъра отворим `https://domain.com/rss.xml`, ще се покаже презентерът `Feed` с действие `rss`, ако `https://domain.com/article/12`, ще се покаже презентерът `Article` с действие `view` и т.н. В случай на ненамерен подходящ маршрут, Nette Application реагира с хвърляне на изключение [BadRequestException |api:Nette\Application\BadRequestException], което се показва на потребителя като страница за грешка 404 Not Found. - - -Ред на маршрутите ------------------ - -Абсолютно **ключов е редът**, в който са посочени отделните маршрути, защото те се оценяват последователно отгоре надолу. Важи правилото, че маршрутите декларираме **от специфични към общи**: - -```php -// ГРЕШНО: 'rss.xml' се улавя от първия маршрут и разбира този низ като <slug> -$router->addRoute('<slug>', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// ДОБРЕ -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('<slug>', 'Article:view'); -``` - -Маршрутите се оценяват отгоре надолу и при генериране на връзки: - -```php -// ГРЕШНО: връзка към 'Feed:rss' генерира като 'admin/feed/rss' -$router->addRoute('admin/<presenter>/<action>', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// ДОБРЕ -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin/<presenter>/<action>', 'Admin:default'); -``` - -Няма да крием от вас, че правилното съставяне на маршрути изисква известна умелост. Преди да я усвоите, полезен помощник ще ви бъде [панелът за маршрутизация |#Дебъгване на рутера]. - - -Маска и параметри ------------------ - -Маската описва относителния път от коренната директория на уебсайта. Най-простата маска е статичен URL: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Често маските съдържат т.нар. **параметри**. Те са посочени в ъглови скоби (напр. `<year>`) и се предават на целевия презентер, например на метода `renderShow(int $year)` или на персистентния параметър `$year`: - -```php -$router->addRoute('chronicle/<year>', 'History:show'); -``` - -Примерът казва, че ако в браузъра отворим `https://example.com/chronicle/2020`, ще се покаже презентерът `History` с действие `show` и параметър `year: 2020`. - -На параметрите можем да зададем стойност по подразбиране директно в маската и така те стават незадължителни: - -```php -$router->addRoute('chronicle/<year=2020>', 'History:show'); -``` - -Маршрутът сега ще приема и URL `https://example.com/chronicle/`, който отново ще покаже `History:show` с параметър `year: 2020`. - -Параметърът може, разбира се, да бъде и името на презентера и действието. Например така: - -```php -$router->addRoute('<presenter>/<action>', 'Home:default'); -``` - -Посоченият маршрут приема напр. URL във формата `/article/edit` или също `/catalog/list` и ги разбира като презентери и действия `Article:edit` и `Catalog:list`. - -Същевременно дава на параметрите `presenter` и `action` стойности по подразбиране `Home` и `default` и следователно те също са незадължителни. Така че маршрутът приема и URL във формата `/article` и го разбира като `Article:default`. Или обратно, връзка към `Product:default` генерира пътя `/product`, връзка към подразбиращия се `Home:default` пътя `/`. - -Маската може да описва не само относителния път от коренната директория на уебсайта, но и абсолютния път, ако започва с наклонена черта, или дори целия абсолютен URL, ако започва с две наклонени черти: - -```php -// относително към document root -$router->addRoute('<presenter>/<action>', /* ... */); - -// абсолютен път (относителен към домейна) -$router->addRoute('/<presenter>/<action>', /* ... */); - -// абсолютен URL, включително домейна (относителен към схемата) -$router->addRoute('//<lang>.example.com/<presenter>/<action>', /* ... */); - -// абсолютен URL, включително схемата -$router->addRoute('https://<lang>.example.com/<presenter>/<action>', /* ... */); -``` - - -Валидационни изрази -------------------- - -За всеки параметър може да се установи валидационно условие с помощта на [регулярен израз|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Например на параметъра `id` ще определим, че може да приема само цифри с помощта на регулярен израз `\d+`: - -```php -$router->addRoute('<presenter>/<action>[/<id \d+>]', /* ... */); -``` - -Регулярният израз по подразбиране за всички параметри е `[^/]+`, т.е. всичко освен наклонена черта. Ако параметърът трябва да приема и наклонени черти, ще посочим израз `.+`: - -```php -// приема https://example.com/a/b/c, path ще бъде 'a/b/c' -$router->addRoute('<path .+>', /* ... */); -``` - - -Незадължителни последователности --------------------------------- - -В маската могат да се маркират незадължителни части с помощта на квадратни скоби. Незадължителна може да бъде всяка част от маската, в нея могат да се намират и параметри: - -```php -$router->addRoute('[<lang [a-z]{2}>/]<name>', /* ... */); - -// Приема пътища: -// /cs/download => lang => cs, name => download -// /download => lang => null, name => download -``` - -Когато параметърът е част от незадължителна последователност, той става разбира се също незадължителен. Ако няма посочена стойност по подразбиране, тогава ще бъде null. - -Незадължителни части могат да бъдат и в домейна: - -```php -$router->addRoute('//[<lang=en>.]example.com/<presenter>/<action>', /* ... */); -``` - -Последователностите могат да се влагат и комбинират свободно: - -```php -$router->addRoute( - '[<lang [a-z]{2}>[-<sublang>]/]<name>[/page-<page=0>]', - 'Home:default', -); - -// Приема пътища: -// /cs/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -При генериране на URL се стремим към най-краткия вариант, така че всичко, което може да се пропусне, се пропуска. Затова например маршрутът `index[.html]` генерира пътя `/index`. Обръщането на поведението е възможно чрез посочване на удивителен знак след лявата квадратна скоба: - -```php -// приема /hello и /hello.html, генерира /hello -$router->addRoute('<name>[.html]', /* ... */); - -// приема /hello и /hello.html, генерира /hello.html -$router->addRoute('<name>[!.html]', /* ... */); -``` - -Незадължителните параметри (т.е. параметри, имащи стойност по подразбиране) без квадратни скоби се държат по същество така, сякаш са оградени по следния начин: - -```php -$router->addRoute('<presenter=Home>/<action=default>/<id=>', /* ... */); - -// съответства на това: -$router->addRoute('[<presenter=Home>/[<action=default>/[<id>]]]', /* ... */); -``` - -Ако искаме да повлияем на поведението на крайната наклонена черта, така че напр. вместо `/home/` да се генерира само `/home`, това може да се постигне така: - -```php -$router->addRoute('[<presenter=Home>[/<action=default>[/<id>]]]', /* ... */); -``` - - -Заместващи знаци ----------------- - -В маската на абсолютния път можем да използваме следните заместващи знаци и така да избегнем напр. необходимостта да записваме в маската домейна, който може да се различава в среда за разработка и продукционна среда: - -- `%tld%` = top level domain, напр. `com` или `org` -- `%sld%` = second level domain, напр. `example` -- `%domain%` = домейн без субдомейни, напр. `example.com` -- `%host%` = цял хост, напр. `www.example.com` -- `%basePath%` = път към коренната директория - -```php -$router->addRoute('//www.%domain%/%basePath%/<presenter>/<action>', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%/<presenter>/<action', /* ... */); -``` - - -Разширен запис --------------- - -Целта на маршрута, обикновено записвана във формата `Presenter:action`, може да бъде записана и с помощта на масив, който дефинира отделните параметри и техните стойности по подразбиране: - -```php -$router->addRoute('<presenter>/<action>[/<id \d+>]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -За по-подробна спецификация може да се използва още по-разширена форма, където освен стойностите по подразбиране можем да зададем и други свойства на параметрите, като например валидационен регулярен израз (виж параметъра `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('<presenter>/<action>[/<id>]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Важно е да се отбележи, че ако параметрите, дефинирани в масива, не са посочени в маската на пътя, техните стойности не могат да бъдат променени, дори и с помощта на query параметри, посочени след въпросителния знак в URL. - - -Филтри и преводи ----------------- - -Изходните кодове на приложението пишем на английски, но ако уебсайтът трябва да има български URL адреси, тогава простото маршрутизиране от типа: - -```php -$router->addRoute('<presenter>/<action>', 'Home:default'); -``` - -ще генерира английски URL адреси, като например `/product/123` или `/cart`. Ако искаме презентерите и действията в URL да бъдат представени с български думи (напр. `/produkt/123` или `/kosik`), можем да използваме преводен речник. За неговия запис вече се нуждаем от "по-многословния" вариант на втория параметър: - -```php -use Nette\Routing\Route; - -$router->addRoute('<presenter>/<action>', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // низ в URL => презентер - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Повече ключове на преводния речник могат да водят към един и същ презентер. Така към него се създават различни псевдоними. За каноничен вариант (т.е. този, който ще бъде в генерирания URL) се счита последният ключ. - -Преводната таблица може по този начин да се използва за всеки параметър. При което, ако преводът не съществува, се взема оригиналната стойност. Това поведение можем да променим, като добавим `Route::FilterStrict => true` и маршрутът тогава ще отхвърли URL, ако стойността не е в речника. - -Освен преводния речник под формата на масив, могат да се приложат и собствени преводни функции. - -```php -use Nette\Routing\Route; - -$router->addRoute('<presenter>/<action>/<id>', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -Функцията `Route::FilterIn` преобразува между параметър в URL и низ, който след това се предава на презентера, функцията `FilterOut` осигурява преобразуването в обратна посока. - -Параметрите `presenter`, `action` и `module` вече имат предварително дефинирани филтри, които преобразуват между стила PascalCase, респ. camelCase, и kebab-case, използван в URL. Стойността по подразбиране на параметрите се записва вече в трансформирана форма, така че например в случая с презентера пишем `<presenter=ProductEdit>`, а не `<presenter=product-edit>`. - - -Общи филтри ------------ - -Освен филтрите, предназначени за конкретни параметри, можем да дефинираме и общи филтри, които получават асоциативен масив от всички параметри, които могат да модифицират по всякакъв начин и след това ги връщат. Общите филтри дефинираме под ключ `null`. - -```php -use Nette\Routing\Route; - -$router->addRoute('<presenter>/<action>', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Общите филтри дават възможност да се промени поведението на маршрута по абсолютно всякакъв начин. Можем да ги използваме например за модификация на параметри въз основа на други параметри. Например превеждане на `<presenter>` и `<action>` въз основа на текущата стойност на параметъра `<lang>`. - -Ако параметърът има дефиниран собствен филтър и същевременно съществува общ филтър, се изпълнява собственият `FilterIn` преди общия и обратно, общият `FilterOut` преди собствения. Тоест, вътре в общия филтър стойностите на параметрите `presenter`, респ. `action`, са записани в стил PascalCase, респ. camelCase. - - -Еднопосочни OneWay ------------------- - -Еднопосочните маршрути се използват за запазване на функционалността на стари URL адреси, които приложението вече не генерира, но все още приема. Маркираме ги с флаг `OneWay`: - -```php -// стар URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); -// нов URL /product/123 -$router->addRoute('product/<id>', 'Product:detail'); -``` - -При достъп до стария URL презентерът автоматично пренасочва към новия URL, така че търсачките няма да индексират тези страници два пъти (виж [#SEO и канонизация]). - - -Динамично маршрутизиране с callback-ове ---------------------------------------- - -Динамичното маршрутизиране с callback-ове ви позволява да присвоите на маршрутите директно функции (callback-ове), които се изпълняват, когато даденият път е посетен. Тази гъвкава функционалност ви позволява бързо и ефективно да създавате различни крайни точки (endpoints) за вашето приложение: - -```php -$router->addRoute('test', function () { - echo 'вие сте на адрес /test'; -}); -``` - -Можете също така да дефинирате в маската параметри, които автоматично се предават на вашия callback: - -```php -$router->addRoute('<lang cs|en>', function (string $lang) { - echo match ($lang) { - 'cs' => 'Добре дошли в българската версия на нашия уебсайт!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - - -Модули ------- - -Ако имаме повече маршрути, които попадат в общ [модул |directory-structure#Презентери и шаблони], ще използваме `withModule()`: - -```php -$router = new RouteList; -$router->withModule('Forum') // следващите маршрути са част от модула Forum - ->addRoute('rss', 'Feed:rss') // презентерът ще бъде Forum:Feed - ->addRoute('<presenter>/<action>') - - ->withModule('Admin') // следващите маршрути са част от модула Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Алтернатива е използването на параметъра `module`: - -```php -// URL manage/dashboard/default се мапва към презентера Admin:Dashboard -$router->addRoute('manage/<presenter>/<action>', [ - 'module' => 'Admin', -]); -``` - - -Субдомейни ----------- - -Колекциите от маршрути можем да групираме по субдомейни: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('<presenter>/<action>'); -``` - -В името на домейна могат да се използват и [#Заместващи знаци]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Префикс на пътя ---------------- - -Колекциите от маршрути можем да групираме по път в URL: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // улавя URL /eshop/rss - ->addRoute('<presenter>/<action>'); // улавя URL /eshop/<presenter>/<action> -``` - - -Комбинации ----------- - -Горепосочените групирания можем да комбинираме взаимно: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Query параметри ---------------- - -Маските могат също да съдържат query параметри (параметри след въпросителния знак в URL). За тях не може да се дефинира валидационен израз, но може да се промени името, под което се предават на презентера: - -```php -// query параметъра 'cat' искаме в приложението да използваме под името 'categoryId' -$router->addRoute('product ? id=<productId> & cat=<categoryId>', /* ... */); -``` - - -Foo параметри -------------- - -Сега вече навлизаме по-дълбоко. Foo параметрите са по същество неименувани параметри, които позволяват съвпадение с регулярен израз. Пример е маршрут, приемащ `/index`, `/index.html`, `/index.htm` и `/index.php`: - -```php -$router->addRoute('index<? \.html?|\.php|>', /* ... */); -``` - -Може също така изрично да се дефинира низ, който ще бъде използван при генериране на URL. Низът трябва да бъде поставен директно след въпросителния знак. Следващият маршрут е подобен на предходния, но генерира `/index.html` вместо `/index`, защото низът `.html` е зададен като генерираща стойност: - -```php -$router->addRoute('index<?.html \.html?|\.php|>', /* ... */); -``` - - -Включване в приложението -======================== - -За да включим създадения рутер в приложението, трябва да кажем за него на DI контейнера. Най-лесният начин е да подготвим фабрика, която ще произведе обекта на рутера, и да съобщим в конфигурацията на контейнера, че трябва да я използва. Да кажем, че за тази цел ще напишем метод `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -В [конфигурацията |dependency-injection:services] след това ще запишем: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Всякакви зависимости, например към база данни и т.н., се предават на фабричния метод като негови параметри с помощта на [autowiring|dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Много по-прост рутер от колекцията от маршрути е [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Използваме го тогава, когато нямаме специални изисквания към формата на URL, когато не е наличен `mod_rewrite` (или негови алтернативи) или когато засега не искаме да се занимаваме с хубави URL адреси. - -Генерира адреси приблизително в този вид: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Параметърът на конструктора на SimpleRouter е подразбиращият се презентер и действие, към който трябва да се насочи, ако отворим страница без параметри, напр. `http://example.com/`. - -```php -// подразбиращият се презентер ще бъде 'Home' и действието 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Препоръчваме SimpleRouter директно да се дефинира в [конфигурацията |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO и канонизация -================= - -Фреймуъркът допринася за SEO (оптимизация за намиране в интернет), като предотвратява дублирането на съдържание на различни URL адреси. Ако към определена цел водят няколко адреса, напр. `/index` и `/index.html`, фреймуъркът определя първия от тях за основен (каноничен) и останалите пренасочва към него с помощта на HTTP код 301. Благодарение на това търсачките не индексират страниците ви два пъти и не размиват техния page rank. - -Този процес се нарича канонизация. Каноничният URL е този, който генерира рутерът, т.е. първият удовлетворяващ маршрут в колекцията без флаг OneWay. Затова в колекцията посочваме **основните маршрути като първи**. - -Канонизацията се извършва от презентера, повече в главата [канонизация |presenters#Канонизация]. - - -HTTPS -===== - -За да можем да използваме HTTPS протокол, е необходимо да го разрешим на хостинга и правилно да конфигурираме сървъра си. - -Пренасочването на целия уебсайт към HTTPS трябва да се настрои на ниво сървър, например с помощта на файла .htaccess в коренната директория на нашето приложение, и то с HTTP код 301. Настройката може да се различава според хостинга и изглежда приблизително така: - -``` -<IfModule mod_rewrite.c> - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... -</IfModule> -``` - -Рутерът генерира URL със същия протокол, с който е била заредена страницата, така че нищо повече не е необходимо да се настройва. - -Ако обаче по изключение се нуждаем различните маршрути да работят под различни протоколи, ще го посочим в маската на маршрута: - -```php -// Ще генерира адрес с HTTP -$router->addRoute('http://%host%/<presenter>/<action>', /* ... */); - -// Ще генерира адрес с HTTPS -$router->addRoute('https://%host%/<presenter>/<action>', /* ... */); -``` - - -Дебъгване на рутера -=================== - -Панелът за маршрутизация, показващ се в [Tracy Bar |tracy:], е полезен помощник, който показва списък с маршрути, както и параметри, които рутерът е получил от URL. - -Зелената лента със символ ✓ представлява маршрута, който е обработил текущия URL, със син цвят и символ ≈ са маркирани маршрутите, които също биха обработили URL, ако зеленият не ги беше изпреварил. По-нататък виждаме текущия презентер и действие. - -[* routing-debugger.webp *] - -Същевременно, ако се случи неочаквано пренасочване поради [канонизация |#SEO и канонизация], е полезно да се погледне в панела в лентата *redirect*, къде ще разберете как рутерът първоначално е разбрал URL и защо е пренасочил. - -.[note] -При дебъгване на рутера препоръчваме да отворите в браузъра Developer Tools (Ctrl+Shift+I или Cmd+Option+I) и в панела Network да изключите кеша, за да не се съхраняват в него пренасочванията. - - -Производителност -================ - -Броят на маршрутите влияе на скоростта на рутера. Техният брой определено не трябва да надхвърля няколко десетки. Ако вашият уебсайт има прекалено сложна структура на URL, можете да си напишете по мярка [#Собствен рутер]. - -Ако рутерът няма никакви зависимости, например към база данни, и неговата фабрика не приема никакви аргументи, можем да сериализираме неговата сглобена форма директно в DI контейнера и така леко да ускорим приложението. - -```neon -routing: - cache: true -``` - - -Собствен рутер -============== - -Следващите редове са предназначени за много напреднали потребители. Можете да си създадете собствен рутер и напълно естествено да го включите в колекцията от маршрути. Рутерът е имплементация на интерфейса [api:Nette\Routing\Router] с два метода: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -Методът `match` обработва текущата заявка [$httpRequest |http:request], от която може да се получи не само URL, но и хедъри и т.н., в масив, съдържащ името на презентера и неговите параметри. Ако не може да обработи заявката, връща null. При обработка на заявката трябва да върнем поне презентер и действие. Името на презентера е пълно и съдържа и евентуални модули: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -Методът `constructUrl` обратно, сглобява от масив с параметри крайния абсолютен URL. За това може да използва информация от параметъра [`$refUrl`|api:Nette\Http\UrlScript], което е текущият URL. - -В колекцията от маршрути го добавяте с помощта на `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Самостоятелно използване -======================== - -Под самостоятелно използване разбираме използването на възможностите на рутера в приложение, което не използва Nette Application и презентери. За него важи почти всичко, което показахме в тази глава, със следните разлики: - -- за колекции от маршрути използваме клас [api:Nette\Routing\RouteList] -- като simple router клас [api:Nette\Routing\SimpleRouter] -- тъй като не съществува двойка `Presenter:action`, използваме [#Разширен запис] - -Така че отново си създаваме метод, който ще ни сглоби рутера, напр.: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/<id \d+>', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Ако използвате DI контейнер, което препоръчваме, отново добавяме метода в конфигурацията и след това рутера заедно с HTTP заявката получаваме от контейнера: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Или обектите директно произвеждаме: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Сега вече остава да пуснем рутера да работи: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // не беше намерен удовлетворяващ маршрут, изпращаме грешка 404 - exit; -} - -// обработваме получените параметри -$controller = $params['controller']; -// ... -``` - -И обратно, използваме рутера за сглобяване на връзка: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/router}} diff --git a/application/bg/templates.texy b/application/bg/templates.texy deleted file mode 100644 index d6f421e713..0000000000 --- a/application/bg/templates.texy +++ /dev/null @@ -1,323 +0,0 @@ -Шаблони -******* - -.[perex] -Nette използва шаблониращата система [Latte |latte:]. От една страна, защото е най-добре защитената шаблонираща система за PHP, а същевременно и най-интуитивната система. Не е необходимо да учите много нови неща, достатъчно е да познавате PHP и няколко тага. - -Обичайно е страницата да се състои от шаблон на лейаута + шаблон на даденото действие. Така например може да изглежда шаблонът на лейаута, забележете блоковете `{block}` и тага `{include}`: - -```latte -<!DOCTYPE html> -<html> -<head> - <title>{block title}My App{/block} - - -
...
- {include content} -
...
- - -``` - -А това ще бъде шаблонът на действието: - -```latte -{block title}Homepage{/block} - -{block content} -

Homepage

-... -{/block} -``` - -Той дефинира блок `content`, който се вмъква на мястото на `{include content}` в лейаута, и също така ре-дефинира блок `title`, с който презаписва `{block title}` в лейаута. Опитайте да си представите резултата. - - -Търсене на шаблони ------------------- - -Не е необходимо в презентерите да посочвате кой шаблон трябва да се рендира, фреймуъркът сам извежда пътя и ви спестява писане. - -Ако използвате директорийна структура, където всеки презентер има собствена директория, просто поставете шаблона в тази директория под името на действието (респ. view), т.е. за действието `default` използвайте шаблона `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Ако използвате структура, където презентерите са заедно в една директория, а шаблоните в папка `templates`, съхранете го или във файл `..latte`, или `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1. вариант - └── Home/ - └── default.latte ← 2. вариант -\-- - -Директорията `templates` може да бъде разположена и едно ниво по-високо, т.е. на същото ниво, на което е директорията с класовете на презентерите. - -Ако шаблонът не бъде намерен, презентерът отговаря с [грешка 404 - page not found |presenters#Грешка 404 и др]. - -View се променя с помощта на `$this->setView('jineView')`. Също така може директно да се посочи файл с шаблон с помощта на `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Файловете, където се търсят шаблони, могат да се променят чрез презаписване на метода [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], който връща масив от възможни имена на файлове. - - -Търсене на шаблон на лейаута ----------------------------- - -Nette също така автоматично търси файл с лейаут. - -Ако използвате директорийна структура, където всеки презентер има собствена директория, поставете лейаута или в папката с презентера, ако е специфичен само за него, или едно ниво по-високо, ако е общ за няколко презентера: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← общ лейаут - └── Home/ - ├── @layout.latte ← само за презентера Home - ├── HomePresenter.php - └── default.latte -\-- - -Ако използвате структура, където презентерите са заедно в една директория, а шаблоните в папка `templates`, лейаутът ще се очаква на тези места: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← общ лейаут - ├── Home.@layout.latte ← само за Home, 1. вариант - └── Home/ - └── @layout.latte ← само за Home, 2. вариант -\-- - -Ако презентерът се намира в модул, ще се търси и на други директорийни нива по-високо, според влагането на модула. - -Името на лейаута може да се промени с помощта на `$this->setLayout('layoutAdmin')` и тогава ще се очаква във файл `@layoutAdmin.latte`. Също така може директно да се посочи файл с шаблон на лейаута с помощта на `$this->setLayout('/path/to/template.latte')`. - -С помощта на `$this->setLayout(false)` или тага `{layout none}` вътре в шаблона търсенето на лейаут се изключва. - -.[note] -Файловете, където се търсят шаблони на лейаута, могат да се променят чрез презаписване на метода [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], който връща масив от възможни имена на файлове. - - -Променливи в шаблона --------------------- - -Променливите в шаблона предаваме така, че ги записваме в `$this->template` и след това ги имаме на разположение в шаблона като локални променливи: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Така лесно можем да предадем в шаблоните всякакви променливи. При разработката на стабилни приложения обаче е по-полезно да се ограничим. Например така, че изрично да дефинираме списък с променливите, които шаблонът очаква, и техните типове. Благодарение на това PHP ще може да проверява типовете, IDE правилно да подсказва и статичният анализ да открива грешки. - -А как да дефинираме такъв списък? Просто под формата на клас и неговите свойства. Ще го наречем подобно на презентера, само с `Template` накрая: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // и други променливи -} -``` - -Обектът `$this->template` в презентера сега ще бъде инстанция на класа `ArticleTemplate`. Така че PHP при запис ще проверява декларираните типове. И започвайки от версия PHP 8.2 ще предупреждава и за запис в несъществуваща променлива, в предишните версии същото може да се постигне с използването на trait [Nette\SmartObject |utils:smartobject]. - -Анотацията `@property-read` е предназначена за IDE и статичен анализ, благодарение на нея ще работи подсказването, вижте "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Лукса на подсказването можете да си позволите и в шаблоните, достатъчно е да инсталирате в PhpStorm плъгин за Latte и да посочите в началото на шаблона името на класа, повече в статията "Latte: как да използваме системата за типове":https://blog.nette.org/bg/latte-how-to-use-type-system: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Така работят и шаблоните в компонентите, достатъчно е само да се спазва именната конвенция и за компонент напр. `FifteenControl` да се създаде клас на шаблона `FifteenTemplate`. - -Ако трябва да създадете `$template` като инстанция на друг клас, използвайте метода `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Променливи по подразбиране --------------------------- - -Презентерите и компонентите предават на шаблоните няколко полезни променливи автоматично: - -- `$basePath` е абсолютният URL път до коренната директория (напр. `/eshop`) -- `$baseUrl` е абсолютният URL до коренната директория (напр. `http://localhost/eshop`) -- `$user` е обект [представляващ потребителя |security:authentication] -- `$presenter` е текущият презентер -- `$control` е текущият компонент или презентер -- `$flashes` масив от [съобщения |presenters#Flash съобщения], изпратени с функцията `flashMessage()` - -Ако използвате собствен клас на шаблона, тези променливи се предават, ако създадете свойство за тях. - - -Създаване на връзки -------------------- - -В шаблона се създават връзки към други презентери и действия по следния начин: - -```latte -детайл на продукта -``` - -Атрибутът `n:href` е много удобен за HTML тагове ``. Ако искаме да изпишем връзка другаде, например в текст, използваме `{link}`: - -```latte -Адресът е: {link Home:default} -``` - -Повече информация ще намерите в главата [Създаване на URL връзки|creating-links]. - - -Собствени филтри, тагове и др. ------------------------------- - -Шаблониращата система Latte може да бъде разширена със собствени филтри, функции, тагове и др. Това може да се направи директно в метода `render` или `beforeRender()`: - -```php -public function beforeRender(): void -{ - // добавяне на филтър - $this->template->addFilter('foo', /* ... */); - - // или конфигурираме директно обекта Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -Latte във версия 3 предлага по-напреднал начин, а именно създаването на [extension |latte:extending-latte#Latte Extension] за всеки уеб проект. Частичен пример за такъв клас: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Регистрираме го с помощта на [конфигурацията |configuration#Шаблони Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Превод ------- - -Ако програмирате многоезично приложение, най-вероятно ще трябва да изпишете някои текстове в шаблона на различни езици. Nette Framework за тази цел дефинира интерфейс за превод [api:Nette\Localization\Translator], който има единствен метод `translate()`. Той приема съобщение `$message`, което обикновено е низ, и всякакви други параметри. Задачата е да върне преведен низ. В Nette няма реализация по подразбиране, можете да изберете според своите нужди от няколко готови решения, които ще намерите на [Componette |https://componette.org/search/localization]. В тяхната документация ще научите как да конфигурирате преводача. - -На шаблоните може да се зададе преводач, който си [изискваме |dependency-injection:passing-dependencies], с метода `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Преводачът може алтернативно да се настрои с помощта на [конфигурацията |configuration#Шаблони Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -След това преводачът може да се използва например като филтър `|translate`, и то включително с допълнителни параметри, които се предават на метода `translate()` (виж `foo, bar`): - -```latte -{='Количка'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Или като таг с долна черта: - -```latte -{_'Количка'} -{_$item} -{_$item, foo, bar} -``` - -За превод на част от шаблона съществува двоен таг `{translate}` (от Latte 2.11, преди се използваше тагът `{_}`): - -```latte -{translate}Поръчка{/translate} -{translate foo, bar}Поръчка{/translate} -``` - -Преводачът стандартно се извиква по време на изпълнение при рендиране на шаблона. Latte версия 3 обаче може да превежда всички статични текстове още по време на компилацията на шаблона. С това се спестява производителност, защото всеки низ се превежда само веднъж и крайният превод се записва в компилираната форма. В директорията с кеша така възникват повече компилирани версии на шаблона, по една за всеки език. За това е достатъчно само да се посочи езикът като втори параметър: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Статичен текст е например `{_'hello'}` или `{translate}hello{/translate}`. Нестатичните текстове, като например `{_$foo}`, ще продължат да се превеждат по време на изпълнение. diff --git a/application/el/@home.texy b/application/el/@home.texy deleted file mode 100644 index 1abad7558e..0000000000 --- a/application/el/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Η Nette Application είναι ο πυρήνας του Nette Framework, παρέχοντας ισχυρά εργαλεία για τη δημιουργία σύγχρονων web εφαρμογών. Προσφέρει μια σειρά από εξαιρετικά χαρακτηριστικά που διευκολύνουν σημαντικά την ανάπτυξη και βελτιώνουν την ασφάλεια και τη συντηρησιμότητα του κώδικα. - - -Εγκατάσταση ------------ - -Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας το εργαλείο [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Γιατί να επιλέξετε την Nette Application; ------------------------------------------ - -Το Nette ήταν πάντα πρωτοπόρο στον τομέα των web τεχνολογιών. - -**Αμφίδρομος router:** Το Nette διαθέτει ένα προηγμένο σύστημα δρομολόγησης, το οποίο είναι μοναδικό για την αμφίδρομη φύση του - όχι μόνο μεταφράζει τα URL σε ενέργειες (actions) της εφαρμογής, αλλά μπορεί επίσης να δημιουργήσει αντίστροφα διευθύνσεις URL. Αυτό σημαίνει ότι: -- Μπορείτε να αλλάξετε τη δομή των URL ολόκληρης της εφαρμογής ανά πάσα στιγμή χωρίς να χρειάζεται να επεξεργαστείτε τα templates -- Τα URL κανονικοποιούνται αυτόματα, γεγονός που βελτιώνει το SEO -- Η δρομολόγηση ορίζεται σε ένα σημείο, αντί να είναι διάσπαρτη σε annotations - -**Components και signals:** Το ενσωματωμένο σύστημα component, εμπνευσμένο από το Delphi και το React.js, είναι εντελώς μοναδικό μεταξύ των PHP frameworks: -- Επιτρέπει τη δημιουργία επαναχρησιμοποιήσιμων στοιχείων UI -- Υποστηρίζει την ιεραρχική σύνθεση components -- Προσφέρει κομψή επεξεργασία αιτημάτων AJAX χρησιμοποιώντας signals -- Πλούσια βιβλιοθήκη έτοιμων components στο [Componette](https://componette.org) - -**AJAX και snippets:** Το Nette παρουσίασε έναν επαναστατικό τρόπο εργασίας με AJAX ήδη από το 2009, πολύ πριν από παρόμοιες λύσεις όπως το Hotwire για Ruby on Rails ή το Symfony UX Turbo: -- Τα snippets επιτρέπουν την ενημέρωση μόνο τμημάτων της σελίδας χωρίς την ανάγκη γραφής JavaScript -- Αυτόματη ενσωμάτωση με το σύστημα component -- Έξυπνη ακύρωση (invalidation) τμημάτων σελίδων -- Ελάχιστη ποσότητα μεταφερόμενων δεδομένων - -**Διαισθητικά templates [Latte|latte:]:** Το ασφαλέστερο σύστημα templating για PHP με προηγμένες λειτουργίες: -- Αυτόματη προστασία από XSS με context-aware escaping -- Επεκτασιμότητα μέσω προσαρμοσμένων φίλτρων, συναρτήσεων και tags -- Κληρονομικότητα templates και snippets για AJAX -- Εξαιρετική υποστήριξη PHP 8.x με σύστημα τύπων - -**Dependency Injection:** Το Nette αξιοποιεί πλήρως το Dependency Injection: -- Αυτόματη μεταβίβαση εξαρτήσεων (autowiring) -- Διαμόρφωση μέσω σαφούς μορφής NEON -- Υποστήριξη για factories component - - -Κύρια πλεονεκτήματα -------------------- - -- **Ασφάλεια**: Αυτόματη άμυνα έναντι [ευπαθειών |nette:vulnerability-protection] όπως XSS, CSRF, κ.λπ. -- **Παραγωγικότητα**: Λιγότερη πληκτρολόγηση, περισσότερες λειτουργίες χάρη στον έξυπνο σχεδιασμό -- **Debugging**: [Tracy debugger |tracy:] με πίνακα δρομολόγησης -- **Απόδοση**: Έξυπνη cache, lazy loading components -- **Ευελιξία**: Εύκολη τροποποίηση των URL ακόμη και μετά την ολοκλήρωση της εφαρμογής -- **Components**: Μοναδικό σύστημα επαναχρησιμοποιήσιμων στοιχείων UI -- **Σύγχρονο**: Πλήρης υποστήριξη PHP 8.4+ και συστήματος τύπων - - -Ξεκινώντας ----------- - -1. [Πώς λειτουργούν οι εφαρμογές; |how-it-works] - Κατανόηση της βασικής αρχιτεκτονικής -2. [Presenters |presenters] - Εργασία με presenters και actions -3. [Templates |templates] - Δημιουργία templates στο Latte -4. [Δρομολόγηση |routing] - Διαμόρφωση διευθύνσεων URL -5. [Διαδραστικά components |components] - Χρήση του συστήματος component - - -Συμβατότητες με PHP -------------------- - -| έκδοση | συμβατό με 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 - -Ισχύει για την τελευταία έκδοση patch. diff --git a/application/el/@left-menu.texy b/application/el/@left-menu.texy deleted file mode 100644 index 2f84a69e5d..0000000000 --- a/application/el/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Πώς λειτουργούν οι εφαρμογές; |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Templates |templates] -- [Δομή Καταλόγων |directory-structure] -- [Δρομολόγηση |routing] -- [Δημιουργία συνδέσμων URL |creating-links] -- [Διαδραστικά Components |components] -- [AJAX & snippets |ajax] -- [Multiplier] -- [Διαμόρφωση |configuration] - - -Περαιτέρω ανάγνωση -****************** -- [Γιατί να χρησιμοποιήσετε το Nette; |www:10-reasons-why-nette] -- [Εγκατάσταση |nette:installation] -- [Γράφουμε την πρώτη εφαρμογή! |quickstart:] -- [Οδηγοί και διαδικασίες |best-practices:] -- [Αντιμετώπιση προβλημάτων |nette:troubleshooting] diff --git a/application/el/@meta.texy b/application/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/application/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/application/el/ajax.texy b/application/el/ajax.texy deleted file mode 100644 index 5158f9a656..0000000000 --- a/application/el/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & Snippets -*************** - -
- -Στην εποχή των σύγχρονων διαδικτυακών εφαρμογών, όπου η λειτουργικότητα συχνά κατανέμεται μεταξύ του διακομιστή και του προγράμματος περιήγησης, το AJAX είναι ένα απαραίτητο συνδετικό στοιχείο. Ποιες επιλογές μας προσφέρει το Nette Framework σε αυτόν τον τομέα; -- αποστολή τμημάτων του template, τα λεγόμενα snippets -- μεταβίβαση μεταβλητών μεταξύ PHP και JavaScript -- εργαλεία για την αποσφαλμάτωση αιτήσεων AJAX - -
- - -Αίτηση AJAX -=========== - -Μια αίτηση AJAX δεν διαφέρει ουσιαστικά από μια κλασική αίτηση HTTP. Καλείται ένας presenter με συγκεκριμένες παραμέτρους. Και εξαρτάται από τον presenter πώς θα ανταποκριθεί στην αίτηση - μπορεί να επιστρέψει δεδομένα σε μορφή JSON, να στείλει ένα τμήμα κώδικα HTML, ένα έγγραφο XML κ.λπ. - -Στην πλευρά του προγράμματος περιήγησης, αρχικοποιούμε την αίτηση AJAX χρησιμοποιώντας τη συνάρτηση `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // επεξεργασία της απάντησης -}); -``` - -Στην πλευρά του διακομιστή, αναγνωρίζουμε μια αίτηση AJAX χρησιμοποιώντας τη μέθοδο `$httpRequest->isAjax()` της υπηρεσίας [που ενσωματώνει την αίτηση HTTP |http:request]. Χρησιμοποιεί την κεφαλίδα HTTP `X-Requested-With` για την ανίχνευση, γι' αυτό είναι σημαντικό να την στέλνετε. Μέσα στον presenter, μπορείτε να χρησιμοποιήσετε τη μέθοδο `$this->isAjax()`. - -Αν θέλετε να στείλετε δεδομένα σε μορφή JSON, χρησιμοποιήστε τη μέθοδο [`sendJson()` |presenters#Αποστολή απάντησης]. Η μέθοδος τερματίζει επίσης τη δραστηριότητα του presenter. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Αν σκοπεύετε να απαντήσετε με ένα ειδικό template σχεδιασμένο για AJAX, μπορείτε να το κάνετε ως εξής: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippets -======== - -Το πιο ισχυρό εργαλείο που προσφέρει το Nette για τη σύνδεση του διακομιστή με τον client είναι τα snippets. Χάρη σε αυτά, μπορείτε να μετατρέψετε μια συνηθισμένη εφαρμογή σε μια εφαρμογή AJAX με ελάχιστη προσπάθεια και λίγες γραμμές κώδικα. Το παράδειγμα Fifteen, του οποίου ο κώδικας βρίσκεται στο [GitHub |https://github.com/nette-examples/fifteen], δείχνει πώς λειτουργεί όλο αυτό. - -Τα snippets, ή αποσπάσματα, επιτρέπουν την ενημέρωση μόνο τμημάτων της σελίδας, αντί για την επαναφόρτωση ολόκληρης της σελίδας. Αυτό δεν είναι μόνο ταχύτερο και πιο αποτελεσματικό, αλλά παρέχει επίσης μια πιο άνετη εμπειρία χρήστη. Τα snippets μπορεί να σας θυμίζουν το Hotwire για Ruby on Rails ή το Symfony UX Turbo. Είναι ενδιαφέρον ότι το Nette εισήγαγε τα snippets 14 χρόνια νωρίτερα. - -Πώς λειτουργούν τα snippets; Κατά την πρώτη φόρτωση της σελίδας (αίτηση μη-AJAX), φορτώνεται ολόκληρη η σελίδα, συμπεριλαμβανομένων όλων των snippets. Όταν ο χρήστης αλληλεπιδρά με τη σελίδα (π.χ. κάνει κλικ σε ένα κουμπί, υποβάλλει μια φόρμα κ.λπ.), αντί να φορτωθεί ολόκληρη η σελίδα, γίνεται μια αίτηση AJAX. Ο κώδικας στον presenter εκτελεί την ενέργεια και αποφασίζει ποια snippets πρέπει να ενημερωθούν. Το Nette αποδίδει αυτά τα snippets και τα στέλνει με τη μορφή ενός πίνακα σε μορφή JSON. Ο κώδικας χειρισμού στο πρόγραμμα περιήγησης εισάγει τα ληφθέντα snippets πίσω στη σελίδα. Έτσι, μεταδίδεται μόνο ο κώδικας των αλλαγμένων snippets, εξοικονομώντας εύρος ζώνης και επιταχύνοντας τη φόρτωση σε σύγκριση με τη μετάδοση του περιεχομένου ολόκληρης της σελίδας. - - -Naja ----- - -Για τον χειρισμό των snippets στην πλευρά του προγράμματος περιήγησης, χρησιμοποιείται η [βιβλιοθήκη Naja |https://naja.js.org]. [Εγκαταστήστε την |https://naja.js.org/#/guide/01-install-setup-naja] ως πακέτο node.js (για χρήση με εφαρμογές Webpack, Rollup, Vite, Parcel και άλλες): - -```shell -npm install naja -``` - -…ή εισάγετέ την απευθείας στο template της σελίδας: - -```latte - -``` - -Πρώτα, πρέπει να [αρχικοποιήσετε |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] τη βιβλιοθήκη: - -```js -naja.initialize(); -``` - -Για να μετατρέψετε έναν συνηθισμένο σύνδεσμο (signal) ή την υποβολή μιας φόρμας σε αίτηση AJAX, απλά επισημάνετε τον σχετικό σύνδεσμο, φόρμα ή κουμπί με την κλάση `ajax`: - -```latte -Go - -
- -
- -ή - -
- -
-``` - - -Επανασχεδίαση Snippets ----------------------- - -Κάθε αντικείμενο της κλάσης [Control |components] (συμπεριλαμβανομένου του ίδιου του Presenter) παρακολουθεί εάν έχουν γίνει αλλαγές που απαιτούν την επανασχεδίασή του. Η μέθοδος `redrawControl()` χρησιμοποιείται για αυτό: - -```php -public function handleLogin(string $user): void -{ - // μετά τη σύνδεση, το σχετικό τμήμα πρέπει να επανασχεδιαστεί - $this->redrawControl(); - // ... -} -``` - -Το Nette επιτρέπει ακόμη πιο λεπτομερή έλεγχο του τι πρέπει να επανασχεδιαστεί. Η αναφερόμενη μέθοδος μπορεί να δεχτεί το όνομα του snippet ως όρισμα. Έτσι, μπορείτε να ακυρώσετε (δηλαδή: να επιβάλετε την επανασχεδίαση) σε επίπεδο τμημάτων του template. Εάν ακυρωθεί ολόκληρο το component, κάθε snippet του θα επανασχεδιαστεί επίσης: - -```php -// ακυρώνει το snippet 'header' -$this->redrawControl('header'); -``` - - -Snippets στο Latte ------------------- - -Η χρήση snippets στο Latte είναι εξαιρετικά εύκολη. Για να ορίσετε ένα τμήμα του template ως snippet, απλά περικλείστε το με τις ετικέτες `{snippet}` και `{/snippet}`: - -```latte -{snippet header} -

Hello ...

-{/snippet} -``` - -Το snippet δημιουργεί ένα στοιχείο `
` στη σελίδα HTML με ένα ειδικό, παραγόμενο `id`. Κατά την επανασχεδίαση του snippet, το περιεχόμενο αυτού του στοιχείου ενημερώνεται. Επομένως, είναι απαραίτητο κατά την αρχική απόδοση της σελίδας να αποδοθούν επίσης όλα τα snippets, ακόμα κι αν μπορεί να είναι αρχικά κενά. - -Μπορείτε επίσης να δημιουργήσετε ένα snippet με ένα στοιχείο διαφορετικό από το `
` χρησιμοποιώντας ένα n:attribute: - -```latte -
-

Hello ...

-
-``` - - -Περιοχές Snippet ----------------- - -Τα ονόματα των snippets μπορούν επίσης να είναι εκφράσεις: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Αυτό δημιουργεί πολλά snippets `item-0`, `item-1`, κ.λπ. Αν ακυρώναμε απευθείας ένα δυναμικό snippet (για παράδειγμα `item-1`), τίποτα δεν θα επανασχεδιαζόταν. Ο λόγος είναι ότι τα snippets λειτουργούν πραγματικά ως αποσπάσματα και αποδίδονται μόνο αυτά τα ίδια. Ωστόσο, στο template, δεν υπάρχει στην πραγματικότητα κανένα snippet με το όνομα `item-1`. Αυτό δημιουργείται μόνο κατά την εκτέλεση του κώδικα γύρω από το snippet, δηλαδή του βρόχου foreach. Επομένως, επισημαίνουμε το τμήμα του template που πρέπει να εκτελεστεί χρησιμοποιώντας την ετικέτα `{snippetArea}`: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -Και ζητάμε την επανασχεδίαση τόσο του ίδιου του snippet όσο και ολόκληρης της γονικής περιοχής: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Ταυτόχρονα, είναι καλό να διασφαλίσουμε ότι ο πίνακας `$items` περιέχει μόνο τα στοιχεία που πρέπει να επανασχεδιαστούν. - -Αν εισάγουμε ένα άλλο template που περιέχει snippets στο template χρησιμοποιώντας την ετικέτα `{include}`, είναι απαραίτητο να συμπεριλάβουμε ξανά την εισαγωγή του template σε ένα `snippetArea` και να το ακυρώσουμε μαζί με το snippet: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippets σε Components ----------------------- - -Μπορείτε επίσης να δημιουργήσετε snippets σε [components|components] και το Nette θα τα επανασχεδιάζει αυτόματα. Ωστόσο, υπάρχει ένας περιορισμός: για την επανασχεδίαση των snippets, καλεί τη μέθοδο `render()` χωρίς παραμέτρους. Επομένως, η μεταβίβαση παραμέτρων στο template δεν θα λειτουργήσει: - -```latte -OK -{control productGrid} - -δεν θα λειτουργήσει: -{control productGrid $arg, $arg} -{control productGrid:paginator} -``` - - -Αποστολή Δεδομένων Χρήστη -------------------------- - -Μαζί με τα snippets, μπορείτε να στείλετε οποιαδήποτε άλλα δεδομένα στον client. Απλά γράψτε τα στο αντικείμενο `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } -} -``` - - -Μεταβίβαση Παραμέτρων -===================== - -Αν στέλνουμε παραμέτρους σε ένα component μέσω μιας αίτησης AJAX, είτε πρόκειται για παραμέτρους signal είτε για persistent παραμέτρους, πρέπει να καθορίσουμε το καθολικό τους όνομα στην αίτηση, το οποίο περιλαμβάνει και το όνομα του component. Η μέθοδος `getParameterId()` επιστρέφει το πλήρες όνομα της παραμέτρου. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -Και η μέθοδος handle με τις αντίστοιχες παραμέτρους στο component: - -```php -public function handleFoo(int $bar): void -{ -} -``` diff --git a/application/el/bootstrapping.texy b/application/el/bootstrapping.texy deleted file mode 100644 index d14b96b9e8..0000000000 --- a/application/el/bootstrapping.texy +++ /dev/null @@ -1,297 +0,0 @@ -Εκκίνηση -******** - -
    - -Η εκκίνηση είναι η διαδικασία αρχικοποίησης του περιβάλλοντος της εφαρμογής, δημιουργίας ενός κοντέινερ dependency injection (DI) και εκκίνησης της εφαρμογής. Θα συζητήσουμε: - -- πώς η κλάση Bootstrap αρχικοποιεί το περιβάλλον -- πώς οι εφαρμογές διαμορφώνονται χρησιμοποιώντας αρχεία NEON -- πώς να διακρίνουμε μεταξύ παραγωγικής και αναπτυξιακής λειτουργίας -- πώς να δημιουργήσουμε και να διαμορφώσουμε το DI κοντέινερ - -
    - - -Οι εφαρμογές, είτε πρόκειται για διαδικτυακές εφαρμογές είτε για σενάρια που εκτελούνται από τη γραμμή εντολών, ξεκινούν την εκτέλεσή τους με κάποια μορφή αρχικοποίησης περιβάλλοντος. Στο παρελθόν, αυτό γινόταν συνήθως από ένα αρχείο με όνομα όπως `include.inc.php`, το οποίο το αρχικό αρχείο συμπεριλάμβανε. Στις σύγχρονες εφαρμογές Nette, αυτό έχει αντικατασταθεί από την κλάση `Bootstrap`, την οποία, ως μέρος της εφαρμογής, θα βρείτε στο αρχείο `app/Bootstrap.php`. Μπορεί να μοιάζει κάπως έτσι: - -```php -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Ο Configurator είναι υπεύθυνος για τη ρύθμιση του περιβάλλοντος της εφαρμογής και των υπηρεσιών. - $this->configurator = new Configurator; - // Ορίζει τον κατάλογο για προσωρινά αρχεία που δημιουργούνται από το Nette (π.χ. μεταγλωττισμένα 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 είναι έξυπνο και η λειτουργία ανάπτυξης ενεργοποιείται αυτόματα, - // ή μπορείτε να την ενεργοποιήσετε για μια συγκεκριμένη διεύθυνση IP αποσχολιάζοντας την ακόλουθη γραμμή: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Ενεργοποιεί το Tracy: το απόλυτο "ελβετικό μαχαίρι" για αποσφαλμάτωση. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: φορτώνει αυτόματα όλες τις κλάσεις στον επιλεγμένο κατάλογο - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Φορτώνει αρχεία διαμόρφωσης - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -Το αρχικό αρχείο στην περίπτωση των διαδικτυακών εφαρμογών είναι το `index.php`, το οποίο βρίσκεται στον [δημόσιο κατάλογο |directory-structure#Δημόσιος Κατάλογος www] `www/`. Αυτό ζητά από την κλάση Bootstrap να αρχικοποιήσει το περιβάλλον και να δημιουργήσει το DI container. Στη συνέχεια, λαμβάνει την υπηρεσία `Application` από αυτό, η οποία εκκινεί την διαδικτυακή εφαρμογή: - -```php -$bootstrap = new App\Bootstrap; -// Αρχικοποίηση περιβάλλοντος + δημιουργία DI container -$container = $bootstrap->bootWebApplication(); -// Το DI container δημιουργεί ένα αντικείμενο Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Εκκίνηση της εφαρμογής Nette και επεξεργασία της εισερχόμενης αίτησης -$application->run(); -``` - -Όπως μπορείτε να δείτε, η κλάση [api:Nette\Bootstrap\Configurator] βοηθά στη ρύθμιση του περιβάλλοντος και στη δημιουργία του dependency injection (DI) container, την οποία θα παρουσιάσουμε τώρα λεπτομερέστερα. - - -Λειτουργία Ανάπτυξης vs Παραγωγής -================================= - -Το Nette συμπεριφέρεται διαφορετικά ανάλογα με το αν εκτελείται σε διακομιστή ανάπτυξης ή παραγωγής: - -🛠️ Λειτουργία Ανάπτυξης (Development): - - Εμφανίζει τη γραμμή αποσφαλμάτωσης Tracy με χρήσιμες πληροφορίες (ερωτήματα SQL, χρόνος εκτέλεσης, χρησιμοποιούμενη μνήμη) - - Σε περίπτωση σφάλματος, εμφανίζει μια λεπτομερή σελίδα σφάλματος με κλήσεις συναρτήσεων και περιεχόμενο μεταβλητών - - Ανανεώνει αυτόματα την cache κατά την αλλαγή templates Latte, την τροποποίηση αρχείων διαμόρφωσης κ.λπ. - - -🚀 Λειτουργία Παραγωγής (Production): - - Δεν εμφανίζει καμία πληροφορία αποσφαλμάτωσης, όλα τα σφάλματα καταγράφονται στο αρχείο καταγραφής - - Σε περίπτωση σφάλματος, εμφανίζει τον ErrorPresenter ή μια γενική σελίδα "Server Error" - - Η cache δεν ανανεώνεται ποτέ αυτόματα! - - Βελτιστοποιημένο για ταχύτητα και ασφάλεια - - -Η επιλογή της λειτουργίας γίνεται με αυτόματη ανίχνευση, οπότε συνήθως δεν χρειάζεται να διαμορφώσετε ή να αλλάξετε τίποτα χειροκίνητα: - -- λειτουργία ανάπτυξης: στο localhost (διεύθυνση IP `127.0.0.1` ή `::1`) εάν δεν υπάρχει proxy (δηλαδή η κεφαλίδα HTTP του) -- λειτουργία παραγωγής: παντού αλλού - -Αν θέλουμε να ενεργοποιήσουμε τη λειτουργία ανάπτυξης και σε άλλες περιπτώσεις, για παράδειγμα για προγραμματιστές που έχουν πρόσβαση από μια συγκεκριμένη διεύθυνση IP, χρησιμοποιούμε το `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // μπορείτε επίσης να καθορίσετε έναν πίνακα διευθύνσεων IP -``` - -Συνιστούμε οπωσδήποτε να συνδυάσετε τη διεύθυνση IP με ένα cookie. Αποθηκεύουμε ένα μυστικό token, π.χ. `secret1234`, στο cookie `nette-debug` και με αυτόν τον τρόπο ενεργοποιούμε τη λειτουργία ανάπτυξης για προγραμματιστές που έχουν πρόσβαση από μια συγκεκριμένη διεύθυνση IP και ταυτόχρονα έχουν το αναφερόμενο token στο cookie: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Μπορούμε επίσης να απενεργοποιήσουμε εντελώς τη λειτουργία ανάπτυξης, ακόμη και για το localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Προσοχή, η τιμή `true` ενεργοποιεί τη λειτουργία ανάπτυξης μόνιμα, κάτι που δεν πρέπει ποτέ να συμβεί σε διακομιστή παραγωγής. - - -Εργαλείο Αποσφαλμάτωσης Tracy -============================= - -Για εύκολη αποσφαλμάτωση, ενεργοποιούμε επίσης το εξαιρετικό εργαλείο [Tracy |tracy:]. Στη λειτουργία ανάπτυξης, οπτικοποιεί τα σφάλματα και στη λειτουργία παραγωγής, καταγράφει τα σφάλματα στον καθορισμένο κατάλογο: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Προσωρινά Αρχεία -================ - -Το Nette χρησιμοποιεί cache για το DI container, το RobotLoader, τα templates κ.λπ. Επομένως, είναι απαραίτητο να ορίσετε τη διαδρομή προς τον κατάλογο όπου θα αποθηκεύεται η cache: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Σε Linux ή macOS, ορίστε [δικαιώματα εγγραφής |nette:troubleshooting#Ρύθμιση δικαιωμάτων καταλόγου] για τους καταλόγους `log/` και `temp/`. - - -RobotLoader -=========== - -Συνήθως, θα θέλουμε να φορτώνουμε αυτόματα κλάσεις χρησιμοποιώντας το [RobotLoader |robot-loader:], οπότε πρέπει να το ξεκινήσουμε και να το αφήσουμε να φορτώνει κλάσεις από τον κατάλογο όπου βρίσκεται το `Bootstrap.php` (δηλαδή `__DIR__`), και όλους τους υποκαταλόγους: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Μια εναλλακτική προσέγγιση είναι να αφήσετε τις κλάσεις να φορτώνονται μόνο μέσω του [Composer |best-practices:composer] τηρώντας το PSR-4. - - -Ζώνη Ώρας -========= - -Μέσω του configurator, μπορείτε να ορίσετε την προεπιλεγμένη ζώνη ώρας. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Διαμόρφωση του DI Container -=========================== - -Μέρος της διαδικασίας εκκίνησης είναι η δημιουργία του DI container ή factory αντικειμένων, το οποίο είναι η καρδιά ολόκληρης της εφαρμογής. Πρόκειται στην πραγματικότητα για μια κλάση PHP που δημιουργείται από το Nette και αποθηκεύεται στον κατάλογο cache. Το factory παράγει τα βασικά αντικείμενα της εφαρμογής και, χρησιμοποιώντας αρχεία διαμόρφωσης, το καθοδηγούμε πώς να τα δημιουργεί και να τα ρυθμίζει, επηρεάζοντας έτσι τη συμπεριφορά ολόκληρης της εφαρμογής. - -Τα αρχεία διαμόρφωσης συνήθως γράφονται σε μορφή [NEON |neon:format]. Σε ένα ξεχωριστό κεφάλαιο, θα μάθετε [τι μπορεί να διαμορφωθεί |nette:configuring]. - -.[tip] -Στη λειτουργία ανάπτυξης, το container ενημερώνεται αυτόματα κάθε φορά που αλλάζει ο κώδικας ή τα αρχεία διαμόρφωσης. Στη λειτουργία παραγωγής, δημιουργείται μόνο μία φορά και οι αλλαγές δεν ελέγχονται για μεγιστοποίηση της απόδοσης. - -Φορτώνουμε τα αρχεία διαμόρφωσης χρησιμοποιώντας το `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Αν θέλουμε να προσθέσουμε περισσότερα αρχεία διαμόρφωσης, μπορούμε να καλέσουμε τη συνάρτηση `addConfig()` πολλές φορές. - -```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'); -} -``` - -Το όνομα `cli.php` δεν είναι τυπογραφικό λάθος, η διαμόρφωση μπορεί επίσης να γραφτεί σε ένα αρχείο PHP που την επιστρέφει ως array. - -Μπορούμε επίσης να προσθέσουμε άλλα αρχεία διαμόρφωσης στην [ενότητα `includes` |dependency-injection:configuration#Εισαγωγή αρχείων]. - -Αν εμφανιστούν στοιχεία με τα ίδια κλειδιά στα αρχεία διαμόρφωσης, θα αντικατασταθούν ή, στην περίπτωση [arrays, θα συγχωνευθούν |dependency-injection:configuration#Συγχώνευση]. Το αρχείο που εισάγεται αργότερα έχει υψηλότερη προτεραιότητα από το προηγούμενο. Το αρχείο στο οποίο αναφέρεται η ενότητα `includes` έχει υψηλότερη προτεραιότητα από τα αρχεία που περιλαμβάνονται σε αυτό. - - -Στατικές Παράμετροι -------------------- - -Μπορούμε να ορίσουμε παραμέτρους που χρησιμοποιούνται στα αρχεία διαμόρφωσης στην [ενότητα `parameters` |dependency-injection:configuration#Παράμετροι] και επίσης να τις μεταβιβάσουμε (ή να τις αντικαταστήσουμε) με τη μέθοδο `addStaticParameters()` (έχει το ψευδώνυμο `addParameters()`). Είναι σημαντικό ότι διαφορετικές τιμές παραμέτρων προκαλούν τη δημιουργία πρόσθετων DI containers, δηλαδή πρόσθετων κλάσεων. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -Στην παράμετρο `projectId` μπορείτε να αναφερθείτε στη διαμόρφωση με τη συνηθισμένη σύνταξη `%projectId%`. - - -Δυναμικές Παράμετροι --------------------- - -Μπορούμε επίσης να προσθέσουμε δυναμικές παραμέτρους στο container, των οποίων οι διαφορετικές τιμές, σε αντίθεση με τις στατικές παραμέτρους, δεν προκαλούν τη δημιουργία νέων DI containers. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Με αυτόν τον τρόπο, μπορούμε εύκολα να προσθέσουμε, για παράδειγμα, μεταβλητές περιβάλλοντος, στις οποίες μπορείτε στη συνέχεια να αναφερθείτε στη διαμόρφωση χρησιμοποιώντας τη σύνταξη `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Προεπιλεγμένες Παράμετροι -------------------------- - -Στα αρχεία διαμόρφωσης, μπορείτε να χρησιμοποιήσετε αυτές τις στατικές παραμέτρους: - -- `%appDir%` είναι η απόλυτη διαδρομή προς τον κατάλογο με το αρχείο `Bootstrap.php` -- `%wwwDir%` είναι η απόλυτη διαδρομή προς τον κατάλογο με το αρχείο εισόδου `index.php` -- `%tempDir%` είναι η απόλυτη διαδρομή προς τον κατάλογο για προσωρινά αρχεία -- `%vendorDir%` είναι η απόλυτη διαδρομή προς τον κατάλογο όπου ο Composer εγκαθιστά βιβλιοθήκες -- `%rootDir%` είναι η απόλυτη διαδρομή προς τον ριζικό κατάλογο του έργου -- `%debugMode%` υποδεικνύει εάν η εφαρμογή βρίσκεται σε λειτουργία debugging -- `%consoleMode%` υποδεικνύει εάν η request προήλθε από τη γραμμή εντολών - - -Εισαγόμενες Υπηρεσίες ---------------------- - -Τώρα πηγαίνουμε βαθύτερα. Αν και ο σκοπός του DI container είναι να παράγει αντικείμενα, εξαιρετικά μπορεί να προκύψει η ανάγκη να εισαγάγουμε ένα υπάρχον αντικείμενο στο container. Αυτό το κάνουμε ορίζοντας την υπηρεσία με τη σημαία `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -Και στο bootstrap, εισάγουμε το αντικείμενο στο container: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Διαφορετικό Περιβάλλον -====================== - -Μη διστάσετε να τροποποιήσετε την κλάση Bootstrap σύμφωνα με τις ανάγκες σας. Μπορείτε να προσθέσετε παραμέτρους στη μέθοδο `bootWebApplication()` για να διακρίνετε τα διαδικτυακά έργα. Ή μπορούμε να προσθέσουμε άλλες μεθόδους, όπως `bootTestEnvironment()`, που αρχικοποιεί το περιβάλλον για unit tests, `bootConsoleApplication()` για σενάρια που καλούνται από τη γραμμή εντολών κ.λπ. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // αρχικοποίηση του Nette Tester - $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/el/components.texy b/application/el/components.texy deleted file mode 100644 index b50bd4fdb0..0000000000 --- a/application/el/components.texy +++ /dev/null @@ -1,485 +0,0 @@ -Διαδραστικά Components -********************** - -
    - -Τα components είναι ανεξάρτητα, επαναχρησιμοποιήσιμα αντικείμενα που ενσωματώνουμε σε σελίδες. Μπορεί να είναι φόρμες, datagrids, δημοσκοπήσεις, στην πραγματικότητα οτιδήποτε έχει νόημα να χρησιμοποιείται επανειλημμένα. Θα δείξουμε: - -- πώς να χρησιμοποιείτε τα components; -- πώς να τα γράφετε; -- τι είναι τα signals; - -
    - -Το Nette έχει ενσωματωμένο ένα σύστημα components. Κάτι παρόμοιο μπορεί να θυμούνται οι παλαιότεροι από τα Delphi ή τα ASP.NET Web Forms, ενώ κάτι παρόμοιο αποτελεί τη βάση του React ή του Vue.js. Ωστόσο, στον κόσμο των PHP frameworks, πρόκειται για ένα μοναδικό χαρακτηριστικό. - -Τα components επηρεάζουν θεμελιωδώς την προσέγγιση στην ανάπτυξη εφαρμογών. Μπορείτε να συνθέτετε σελίδες από προκατασκευασμένες μονάδες. Χρειάζεστε ένα datagrid στη διαχείριση; Θα το βρείτε στο [Componette |https://componette.org/search/component], ένα αποθετήριο open-source πρόσθετων (όχι μόνο components) για το Nette, και απλά το ενσωματώνετε στον presenter. - -Μπορείτε να ενσωματώσετε οποιονδήποτε αριθμό components σε έναν presenter. Και σε ορισμένα components, μπορείτε να ενσωματώσετε άλλα components. Αυτό δημιουργεί ένα δέντρο components, του οποίου η ρίζα είναι ο presenter. - - -Μέθοδοι Εργοστασίου -=================== - -Πώς ενσωματώνονται και στη συνέχεια χρησιμοποιούνται τα components στον presenter; Συνήθως μέσω factory μεθόδων. - -Ένα factory component είναι ένας κομψός τρόπος δημιουργίας components μόνο όταν είναι πραγματικά απαραίτητα (lazy / on demand). Η όλη μαγεία έγκειται στην υλοποίηση μιας μεθόδου με το όνομα `createComponent()`, όπου `` είναι το όνομα του component που δημιουργείται, και η οποία δημιουργεί και επιστρέφει το component. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->item; - return $poll; - } -} -``` - -Χάρη στο γεγονός ότι όλα τα components δημιουργούνται σε ξεχωριστές μεθόδους, ο κώδικας γίνεται πιο ευανάγνωστος. - -.[note] -Τα ονόματα των components ξεκινούν πάντα με μικρό γράμμα, παρόλο που γράφονται με κεφαλαίο στο όνομα της μεθόδου. - -Δεν καλούμε ποτέ απευθείας τα factories, καλούνται μόνα τους την πρώτη φορά που χρησιμοποιούμε το component. Χάρη σε αυτό, το component δημιουργείται τη σωστή στιγμή και μόνο όταν είναι πραγματικά απαραίτητο. Αν δεν χρησιμοποιήσουμε το component (για παράδειγμα, κατά τη διάρκεια μιας αίτησης AJAX όπου μεταδίδεται μόνο ένα μέρος της σελίδας, ή κατά την προσωρινή αποθήκευση του template), δεν δημιουργείται καθόλου και εξοικονομούμε απόδοση του διακομιστή. - -```php .{file:DefaultPresenter.php} -// προσπελάζουμε το component και αν είναι η πρώτη φορά, -// καλείται η createComponentPoll() η οποία το δημιουργεί -$poll = $this->getComponent('poll'); -// εναλλακτική σύνταξη: $poll = $this['poll']; -``` - -Στο template, είναι δυνατό να αποδοθεί ένα component χρησιμοποιώντας την ετικέτα [{control} |#Απόδοση]. Επομένως, δεν χρειάζεται να μεταβιβάζετε χειροκίνητα τα components στο template. - -```latte -

    Ψηφίστε

    - -{control poll} -``` - - -Hollywood Style -=============== - -Τα components χρησιμοποιούν συνήθως μια φρέσκια τεχνική, την οποία μας αρέσει να αποκαλούμε Hollywood style. Σίγουρα γνωρίζετε τη φράση που ακούν τόσο συχνά οι συμμετέχοντες σε οντισιόν ταινιών: «Μην μας καλέσετε, θα σας καλέσουμε εμείς». Και ακριβώς περί αυτού πρόκειται. - -Στο Nette, αντί να πρέπει συνεχώς να ρωτάτε κάτι («υποβλήθηκε η φόρμα;», «ήταν έγκυρη;» ή «πάτησε ο χρήστης αυτό το κουμπί;»), λέτε στο framework «όταν συμβεί αυτό, κάλεσε αυτή τη μέθοδο» και αφήνετε την υπόλοιπη δουλειά σε αυτό. Αν προγραμματίζετε σε JavaScript, αυτό το στυλ προγραμματισμού σας είναι οικείο. Γράφετε συναρτήσεις που καλούνται όταν συμβεί ένα συγκεκριμένο γεγονός. Και η γλώσσα τους μεταβιβάζει τις κατάλληλες παραμέτρους. - -Αυτό αλλάζει εντελώς την οπτική γωνία της συγγραφής εφαρμογών. Όσο περισσότερες εργασίες μπορείτε να αφήσετε στο framework, τόσο λιγότερη δουλειά έχετε εσείς. Και τόσο λιγότερα πράγματα μπορείτε, για παράδειγμα, να παραλείψετε. - - -Γράφοντας ένα Component -======================= - -Με τον όρο component, συνήθως εννοούμε έναν απόγονο της κλάσης [api:Nette\Application\UI\Control]. (Θα ήταν πιο ακριβές να χρησιμοποιούμε τον όρο «controls», αλλά οι «έλεγχοι» έχουν εντελώς διαφορετική σημασία στα Ελληνικά και ο όρος «components» έχει επικρατήσει.) Ο ίδιος ο presenter [api:Nette\Application\UI\Presenter] είναι, παρεμπιπτόντως, επίσης απόγονος της κλάσης `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Απόδοση -======= - -Γνωρίζουμε ήδη ότι για την απόδοση ενός component χρησιμοποιείται η ετικέτα `{control componentName}`. Αυτή στην πραγματικότητα καλεί τη μέθοδο `render()` του component, στην οποία φροντίζουμε για την απόδοση. Έχουμε στη διάθεσή μας, ακριβώς όπως στον presenter, ένα [Latte template|templates] στη μεταβλητή `$this->template`, στην οποία μεταβιβάζουμε παραμέτρους. Σε αντίθεση με τον presenter, πρέπει να καθορίσουμε το αρχείο με το template και να το αφήσουμε να αποδοθεί: - -```php .{file:PollControl.php} -public function render(): void -{ - // εισάγουμε κάποιες παραμέτρους στο template - $this->template->param = $value; - // και το αποδίδουμε - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -Η ετικέτα `{control}` επιτρέπει τη μεταβίβαση παραμέτρων στη μέθοδο `render()`: - -```latte -{control poll $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Μερικές φορές, ένα component μπορεί να αποτελείται από πολλά μέρη που θέλουμε να αποδώσουμε ξεχωριστά. Για καθένα από αυτά, δημιουργούμε τη δική του μέθοδο απόδοσης, εδώ στο παράδειγμα, για παράδειγμα, `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -Και στο template, την καλούμε στη συνέχεια χρησιμοποιώντας: - -```latte -{control poll:paginator} -``` - -Για καλύτερη κατανόηση, είναι καλό να γνωρίζουμε πώς μεταφράζεται αυτή η ετικέτα σε PHP. - -```latte -{control poll} -{control poll:paginator 123, 'hello'} -``` - -μεταφράζεται ως: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -Η μέθοδος `getComponent()` επιστρέφει το component `poll` και πάνω σε αυτό το component καλεί τη μέθοδο `render()`, ή `renderPaginator()` αν έχει καθοριστεί διαφορετικός τρόπος απόδοσης στην ετικέτα μετά την άνω και κάτω τελεία. - -.[caution] -Προσοχή, αν εμφανιστεί οπουδήποτε στις παραμέτρους το **`=>`**, όλες οι παράμετροι θα συσκευαστούν σε έναν πίνακα και θα μεταβιβαστούν ως το πρώτο όρισμα: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -μεταφράζεται ως: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Απόδοση υπο-component: - -```latte -{control cartControl-someForm} -``` - -μεταφράζεται ως: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Τα components, όπως και οι presenters, μεταβιβάζουν αυτόματα αρκετές χρήσιμες μεταβλητές στα templates: - -- `$basePath` είναι η απόλυτη διαδρομή URL προς τον ριζικό κατάλογο (π.χ. `/eshop`) -- `$baseUrl` είναι η απόλυτη URL προς τον ριζικό κατάλογο (π.χ. `http://localhost/eshop`) -- `$user` είναι το αντικείμενο [που αντιπροσωπεύει τον χρήστη |security:authentication] -- `$presenter` είναι ο τρέχων presenter -- `$control` είναι το τρέχον component -- `$flashes` array [μηνυμάτων |#Flash Μηνύματα] που στάλθηκαν από τη συνάρτηση `flashMessage()` - - -Σήμα -==== - -Γνωρίζουμε ήδη ότι η πλοήγηση σε μια εφαρμογή Nette βασίζεται στη σύνδεση ή την ανακατεύθυνση σε ζεύγη `Presenter:action`. Αλλά τι γίνεται αν θέλουμε απλώς να εκτελέσουμε μια ενέργεια στην **τρέχουσα σελίδα**; Για παράδειγμα, να αλλάξουμε τη διάταξη των στηλών σε έναν πίνακα; να διαγράψουμε ένα στοιχείο; να αλλάξουμε σε φωτεινή/σκοτεινή λειτουργία; να υποβάλουμε μια φόρμα; να ψηφίσουμε σε μια δημοσκόπηση; κ.λπ. - -Αυτό το είδος αιτήματος ονομάζεται signal. Και όπως οι ενέργειες καλούν τις μεθόδους `action()` ή `render()`, τα signals καλούν τις μεθόδους `handle()`. Ενώ ο όρος ενέργεια (ή view) σχετίζεται καθαρά μόνο με τους presenters, τα signals αφορούν όλα τα components. Και επομένως και τους presenters, επειδή το `UI\Presenter` είναι απόγονος του `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... επεξεργασία του signal ... -} -``` - -Έναν σύνδεσμο που καλεί ένα signal τον δημιουργούμε με τον συνηθισμένο τρόπο, δηλαδή στο template με το χαρακτηριστικό `n:href` ή την ετικέτα `{link}`, στον κώδικα με τη μέθοδο `link()`. Περισσότερα στο κεφάλαιο [Δημιουργία συνδέσμων URL |creating-links#Σύνδεσμοι προς Σήμα]. - -```latte -κάντε κλικ εδώ -``` - -Ένα signal καλείται πάντα στον τρέχοντα presenter και action, δεν είναι δυνατό να το καλέσετε σε άλλο presenter ή άλλη action. - -Ένα signal προκαλεί λοιπόν την επαναφόρτωση της σελίδας ακριβώς όπως στην αρχική αίτηση, απλώς επιπλέον καλεί τη μέθοδο χειρισμού του signal με τις κατάλληλες παραμέτρους. Αν η μέθοδος δεν υπάρχει, δημιουργείται μια εξαίρεση [api:Nette\Application\UI\BadSignalException], η οποία εμφανίζεται στον χρήστη ως σελίδα σφάλματος 403 Forbidden. - - -Snippets και AJAX -================= - -Τα signals μπορεί να σας θυμίζουν λίγο το AJAX: handlers που καλούνται στην τρέχουσα σελίδα. Και έχετε δίκιο, τα signals καλούνται πράγματι συχνά μέσω AJAX και στη συνέχεια μεταφέρουμε στο πρόγραμμα περιήγησης μόνο τα αλλαγμένα τμήματα της σελίδας. Δηλαδή τα λεγόμενα snippets. Περισσότερες πληροφορίες θα βρείτε στη [σελίδα αφιερωμένη στο AJAX |ajax]. - - -Flash Μηνύματα -============== - -Ένα component έχει το δικό του χώρο αποθήκευσης flash μηνυμάτων, ανεξάρτητο από τον presenter. Πρόκειται για μηνύματα που, για παράδειγμα, ενημερώνουν για το αποτέλεσμα μιας λειτουργίας. Ένα σημαντικό χαρακτηριστικό των flash μηνυμάτων είναι ότι είναι διαθέσιμα στο template ακόμη και μετά από ανακατεύθυνση. Ακόμη και μετά την εμφάνισή τους, παραμένουν ενεργά για άλλα 30 δευτερόλεπτα – για παράδειγμα, σε περίπτωση που ο χρήστης ανανεώσει τη σελίδα λόγω σφάλματος μετάδοσης - το μήνυμα δεν εξαφανίζεται αμέσως. - -Η αποστολή γίνεται από τη μέθοδο [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Η πρώτη παράμετρος είναι το κείμενο του μηνύματος ή ένα αντικείμενο `stdClass` που αντιπροσωπεύει το μήνυμα. Η προαιρετική δεύτερη παράμετρος είναι ο τύπος του (error, warning, info κ.λπ.). Η μέθοδος `flashMessage()` επιστρέφει μια παρουσία του flash μηνύματος ως αντικείμενο `stdClass`, στο οποίο μπορούν να προστεθούν περαιτέρω πληροφορίες. - -```php -$this->flashMessage('Το στοιχείο διαγράφηκε.'); -$this->redirect(/* ... */); // και ανακατευθύνουμε -``` - -Στο template, αυτά τα μηνύματα είναι διαθέσιμα στη μεταβλητή `$flashes` ως αντικείμενα `stdClass`, τα οποία περιέχουν τις ιδιότητες `message` (κείμενο μηνύματος), `type` (τύπος μηνύματος) και μπορούν να περιέχουν τις ήδη αναφερθείσες πληροφορίες χρήστη. Τα αποδίδουμε, για παράδειγμα, ως εξής: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Ανακατεύθυνση μετά από Σήμα -=========================== - -Μετά την επεξεργασία ενός signal component, συχνά ακολουθεί ανακατεύθυνση. Είναι μια παρόμοια κατάσταση με τις φόρμες - μετά την υποβολή τους, ανακατευθύνουμε επίσης, ώστε η ανανέωση της σελίδας στο πρόγραμμα περιήγησης να μην προκαλέσει εκ νέου υποβολή των δεδομένων. - -```php -$this->redirect('this') // ανακατευθύνει στον τρέχοντα presenter και action -``` - -Επειδή ένα component είναι ένα επαναχρησιμοποιήσιμο στοιχείο και συνήθως δεν θα πρέπει να έχει άμεση σύνδεση με συγκεκριμένους presenters, οι μέθοδοι `redirect()` και `link()` ερμηνεύουν αυτόματα την παράμετρο ως signal του component: - -```php -$this->redirect('click') // ανακατευθύνει στο signal 'click' του ίδιου component -``` - -Αν χρειαστεί να ανακατευθύνετε σε άλλο presenter ή ενέργεια, μπορείτε να το κάνετε μέσω του presenter: - -```php -$this->getPresenter()->redirect('Product:show'); // ανακατευθύνει σε άλλο presenter/action -``` - - -Persistent Παράμετροι -===================== - -Οι persistent παράμετροι χρησιμοποιούνται για τη διατήρηση της κατάστασης στα components μεταξύ διαφορετικών αιτήσεων. Η τιμή τους παραμένει η ίδια ακόμη και μετά το κλικ σε έναν σύνδεσμο. Σε αντίθεση με τα δεδομένα στη session, μεταφέρονται στη διεύθυνση URL. Και αυτό γίνεται εντελώς αυτόματα, συμπεριλαμβανομένων των συνδέσμων που δημιουργούνται σε άλλα components στην ίδια σελίδα. - -Έχετε, για παράδειγμα, ένα component για τη σελιδοποίηση περιεχομένου. Μπορεί να υπάρχουν πολλά τέτοια components σε μια σελίδα. Και θέλουμε, μετά το κλικ σε έναν σύνδεσμο, όλα τα components να παραμείνουν στην τρέχουσα σελίδα τους. Γι' αυτό, κάνουμε τον αριθμό σελίδας (`page`) μια persistent παράμετρο. - -Η δημιουργία μιας persistent παραμέτρου στο Nette είναι εξαιρετικά απλή. Αρκεί να δημιουργήσετε μια δημόσια property και να την επισημάνετε με ένα attribute: (παλαιότερα χρησιμοποιούνταν το `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // αυτή η γραμμή είναι σημαντική - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // πρέπει να είναι public -} -``` - -Συνιστούμε να καθορίσετε τον τύπο δεδομένων για την property (π.χ. `int`) και μπορείτε επίσης να καθορίσετε μια προεπιλεγμένη τιμή. Οι τιμές των παραμέτρων μπορούν να [επικυρωθούν |#Επικύρωση Persistent Παραμέτρων]. - -Κατά τη δημιουργία ενός συνδέσμου, η τιμή της persistent παραμέτρου μπορεί να αλλάξει: - -```latte -επόμενο -``` - -Ή μπορεί να *επαναφερθεί*, δηλαδή να αφαιρεθεί από τη διεύθυνση URL. Στη συνέχεια, θα πάρει την προεπιλεγμένη της τιμή: - -```latte -επαναφορά -``` - - -Persistent Components -===================== - -Όχι μόνο οι παράμετροι, αλλά και τα components μπορούν να είναι persistent. Σε ένα τέτοιο component, οι persistent παράμετροί του μεταφέρονται ακόμη και μεταξύ διαφορετικών actions του presenter ή μεταξύ πολλών presenters. Σημειώνουμε τα persistent components με μια annotation στην κλάση του presenter. Για παράδειγμα, έτσι σημειώνουμε τα components `calendar` και `poll`: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Τα υπο-components μέσα σε αυτά τα components δεν χρειάζεται να σημειωθούν, γίνονται επίσης persistent. - -Στην PHP 8, μπορείτε επίσης να χρησιμοποιήσετε attributes για να σημειώσετε τα persistent components: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Components με Εξαρτήσεις -======================== - -Πώς να δημιουργήσετε components με εξαρτήσεις χωρίς να «μολύνετε» τους presenters που θα τα χρησιμοποιήσουν; Χάρη στις έξυπνες ιδιότητες του DI container στο Nette, όπως και με τη χρήση κλασικών υπηρεσιών, μπορείτε να αφήσετε το μεγαλύτερο μέρος της δουλειάς στο framework. - -Ας πάρουμε ως παράδειγμα ένα component που έχει εξάρτηση από την υπηρεσία `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // Id της δημοσκόπησης για την οποία δημιουργούμε το component - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($id, $voteId); - // ... - } -} -``` - -Αν γράφαμε μια κλασική υπηρεσία, δεν θα υπήρχε πρόβλημα. Ο DI container θα φρόντιζε αόρατα για τη μεταβίβαση όλων των εξαρτήσεων. Αλλά με τα components, συνήθως τα χειριζόμαστε δημιουργώντας μια νέα παρουσία τους απευθείας στον presenter στις [factory μεθόδους |#Μέθοδοι Εργοστασίου] `createComponent…()`. Αλλά η μεταβίβαση όλων των εξαρτήσεων όλων των components στον presenter, για να τις μεταβιβάσουμε στη συνέχεια στα components, είναι δυσκίνητη. Και πόσος γραμμένος κώδικας… - -Το λογικό ερώτημα είναι, γιατί απλά δεν καταχωρούμε το component ως κλασική υπηρεσία, δεν το μεταβιβάζουμε στον presenter και στη συνέχεια δεν το επιστρέφουμε στη μέθοδο `createComponent…()`? Αυτή η προσέγγιση είναι όμως ακατάλληλη, επειδή θέλουμε να έχουμε τη δυνατότητα να δημιουργούμε το component ακόμη και πολλές φορές. - -Η σωστή λύση είναι να γράψουμε ένα factory για το component, δηλαδή μια κλάση που θα μας δημιουργήσει το component: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Καταχωρούμε αυτό το factory στο container μας στη διαμόρφωση: - -```neon -services: - - PollControlFactory -``` - -και τέλος το χρησιμοποιούμε στον presenter μας: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // μπορούμε να περάσουμε την παράμετρό μας - return $this->pollControlFactory->create($pollId); - } -} -``` - -Το υπέροχο είναι ότι το Nette DI μπορεί να [δημιουργήσει |dependency-injection:factory] τέτοια απλά factories, οπότε αντί για ολόκληρο τον κώδικά του, αρκεί να γράψουμε μόνο το interface του: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -Και αυτό είναι όλο. Το Nette υλοποιεί εσωτερικά αυτό το interface και το μεταβιβάζει στον presenter, όπου μπορούμε ήδη να το χρησιμοποιήσουμε. Προσθέτει μαγικά στην component μας την παράμετρο `$id` και την παρουσία της κλάσης `PollFacade`. - - -Components σε Βάθος -=================== - -Τα components στην Nette Application είναι επαναχρησιμοποιήσιμα μέρη μιας διαδικτυακής εφαρμογής που ενσωματώνουμε σε σελίδες και στα οποία, άλλωστε, είναι αφιερωμένο ολόκληρο αυτό το κεφάλαιο. Ποιες ακριβώς δυνατότητες έχει ένα τέτοιο component; - -1) μπορεί να αποδοθεί σε ένα template -2) γνωρίζει [ποιο μέρος του |ajax#Snippets] πρέπει να αποδώσει κατά τη διάρκεια μιας αίτησης AJAX (snippets) -3) έχει τη δυνατότητα να αποθηκεύει την κατάστασή του στη διεύθυνση URL (persistent παράμετροι) -4) έχει τη δυνατότητα να αντιδρά στις ενέργειες του χρήστη (signals) -5) δημιουργεί μια ιεραρχική δομή (όπου η ρίζα είναι ο presenter) - -Κάθε μία από αυτές τις λειτουργίες παρέχεται από κάποια από τις κλάσεις της γραμμής κληρονομικότητας. Η απόδοση (1 + 2) γίνεται από την [api:Nette\Application\UI\Control], η ενσωμάτωση στον [κύκλο ζωής |presenters#Κύκλος ζωής του presenter] (3, 4) από την κλάση [api:Nette\Application\UI\Component] και η δημιουργία της ιεραρχικής δομής (5) από τις κλάσεις [Container και Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Κύκλος Ζωής του Component -------------------------- - -[* lifecycle-component.svg *] *** *Κύκλος ζωής του Component* .<> - - -Επικύρωση Persistent Παραμέτρων -------------------------------- - -Οι τιμές των [persistent παραμέτρων |#Persistent Παράμετροι] που λαμβάνονται από τη διεύθυνση URL γράφονται στις properties από τη μέθοδο `loadState()`. Αυτή ελέγχει επίσης εάν ο τύπος δεδομένων που καθορίζεται στην property αντιστοιχεί, διαφορετικά απαντά με σφάλμα 404 και η σελίδα δεν εμφανίζεται. - -Ποτέ μην εμπιστεύεστε τυφλά τις persistent παραμέτρους, επειδή μπορούν εύκολα να αντικατασταθούν από τον χρήστη στη διεύθυνση URL. Έτσι, για παράδειγμα, επαληθεύουμε εάν ο αριθμός σελίδας `$this->page` είναι μεγαλύτερος από 0. Ένας κατάλληλος τρόπος είναι να αντικαταστήσετε την αναφερόμενη μέθοδο `loadState()`: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // εδώ ορίζεται το $this->page - // ακολουθεί ο έλεγχος της τιμής: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Η αντίστροφη διαδικασία, δηλαδή η συλλογή τιμών από τις persistent properties, γίνεται από τη μέθοδο `saveState()`. - - -Σήματα σε Βάθος ---------------- - -Ένα signal προκαλεί την επαναφόρτωση της σελίδας ακριβώς όπως στην αρχική αίτηση (εκτός από την περίπτωση που καλείται μέσω AJAX) και καλεί τη μέθοδο `signalReceived($signal)`, της οποίας η προεπιλεγμένη υλοποίηση στην κλάση `Nette\Application\UI\Component` προσπαθεί να καλέσει μια μέθοδο που αποτελείται από τις λέξεις `handle{signal}`. Η περαιτέρω επεξεργασία εξαρτάται από το συγκεκριμένο αντικείμενο. Τα αντικείμενα που κληρονομούν από το `Component` (δηλαδή `Control` και `Presenter`) αντιδρούν προσπαθώντας να καλέσουν τη μέθοδο `handle{signal}` με τις κατάλληλες παραμέτρους. - -Με άλλα λόγια: λαμβάνεται ο ορισμός της συνάρτησης `handle{signal}` και όλες οι παράμετροι που ήρθαν με την αίτηση, και στα ορίσματα αντιστοιχίζονται οι παράμετροι από τη διεύθυνση URL με βάση το όνομα και γίνεται προσπάθεια κλήσης της συγκεκριμένης μεθόδου. Για παράδειγμα, ως παράμετρος `$id` μεταβιβάζεται η τιμή από την παράμετρο `id` στη διεύθυνση URL, ως `$something` μεταβιβάζεται το `something` από τη διεύθυνση URL, κ.λπ. Και αν η μέθοδος δεν υπάρχει, η μέθοδος `signalReceived` δημιουργεί μια [εξαίρεση |api:Nette\Application\UI\BadSignalException]. - -Ένα signal μπορεί να ληφθεί από οποιοδήποτε component, presenter ή αντικείμενο που υλοποιεί το interface `SignalReceiver` και είναι συνδεδεμένο στο δέντρο των components. - -Οι κύριοι παραλήπτες signals θα είναι οι `Presenters` και τα οπτικά components που κληρονομούν από το `Control`. Ένα signal προορίζεται να χρησιμεύσει ως ένδειξη για ένα αντικείμενο ότι πρέπει να κάνει κάτι – μια δημοσκόπηση πρέπει να καταμετρήσει μια ψήφο από έναν χρήστη, ένα μπλοκ με ειδήσεις πρέπει να επεκταθεί και να εμφανίσει διπλάσιες ειδήσεις, μια φόρμα υποβλήθηκε και πρέπει να επεξεργαστεί τα δεδομένα, και ούτω καθεξής. - -Η διεύθυνση URL για ένα signal δημιουργείται χρησιμοποιώντας τη μέθοδο [Component::link() |api:Nette\Application\UI\Component::link()]. Ως παράμετρο `$destination` μεταβιβάζουμε τη συμβολοσειρά `{signal}!` και ως `$args` έναν πίνακα ορισμάτων που θέλουμε να μεταβιβάσουμε στο signal. Το signal καλείται πάντα στον τρέχοντα presenter και action με τις τρέχουσες παραμέτρους, οι παράμετροι του signal απλώς προστίθενται. Επιπλέον, προστίθεται αμέσως στην αρχή η **παράμετρος `?do`, η οποία καθορίζει το signal**. - -Η μορφή του είναι είτε `{signal}`, είτε `{signalReceiver}-{signal}`. Το `{signalReceiver}` είναι το όνομα του component στον presenter. Γι' αυτό δεν μπορεί να υπάρχει παύλα στο όνομα του component – χρησιμοποιείται για να διαχωρίσει το όνομα του component και του signal, ωστόσο είναι δυνατό να ενσωματωθούν έτσι πολλά components. - -Η μέθοδος [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] ελέγχει εάν το component (πρώτο όρισμα) είναι ο παραλήπτης του signal (δεύτερο όρισμα). Μπορούμε να παραλείψουμε το δεύτερο όρισμα – τότε ελέγχει εάν το component είναι ο παραλήπτης οποιουδήποτε signal. Ως δεύτερη παράμετρο, μπορείτε να καθορίσετε `true` για να επαληθεύσετε εάν ο παραλήπτης δεν είναι μόνο το αναφερόμενο component, αλλά και οποιοσδήποτε απόγονός του. - -Σε οποιαδήποτε φάση πριν από το `handle{signal}`, μπορούμε να εκτελέσουμε το signal χειροκίνητα καλώντας τη μέθοδο [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], η οποία αναλαμβάνει τη διαχείριση του signal – παίρνει το component που έχει οριστεί ως παραλήπτης του signal (αν δεν έχει οριστεί παραλήπτης signal, είναι ο ίδιος ο presenter) και του στέλνει το signal. - -Παράδειγμα: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Με αυτόν τον τρόπο, το signal εκτελείται πρόωρα και δεν θα κληθεί ξανά. diff --git a/application/el/configuration.texy b/application/el/configuration.texy deleted file mode 100644 index c97ce203af..0000000000 --- a/application/el/configuration.texy +++ /dev/null @@ -1,191 +0,0 @@ -Διαμόρφωση εφαρμογών -******************** - -.[perex] -Επισκόπηση των επιλογών διαμόρφωσης για τις Εφαρμογές Nette. - - -Application -=========== - -```neon -application: - # εμφάνιση του πίνακα "Nette Application" στο Tracy BlueScreen; - debugger: ... # (bool) προεπιλογή είναι true - - # θα κληθεί ο error-presenter σε περίπτωση σφάλματος; - # ισχύει μόνο σε κατάσταση ανάπτυξης - catchExceptions: ... # (bool) προεπιλογή είναι true - - # όνομα του error-presenter - errorPresenter: Error # (string|array) προεπιλογή είναι 'Nette:Error' - - # ορίζει ψευδώνυμα για presenters και actions - aliases: ... - - # ορίζει κανόνες για τη μετάφραση του ονόματος του presenter σε κλάση - mapping: ... - - # οι μη έγκυροι σύνδεσμοι δεν δημιουργούν προειδοποιήσεις; - # ισχύει μόνο σε κατάσταση ανάπτυξης - silentLinks: ... # (bool) προεπιλογή είναι false -``` - -Από την έκδοση `nette/application` 3.2, μπορείτε να ορίσετε ένα ζεύγος error-presenters: - -```neon -application: - errorPresenter: - 4xx: Error4xx # για την εξαίρεση Nette\Application\BadRequestException - 5xx: Error5xx # για άλλες εξαιρέσεις -``` - -Η επιλογή `silentLinks` καθορίζει πώς συμπεριφέρεται το Nette στην κατάσταση ανάπτυξης όταν η δημιουργία ενός συνδέσμου αποτυγχάνει (για παράδειγμα, επειδή ο presenter δεν υπάρχει, κ.λπ.). Η προεπιλεγμένη τιμή `false` σημαίνει ότι το Nette θα δημιουργήσει ένα σφάλμα `E_USER_WARNING`. Η ρύθμιση σε `true` θα καταστείλει αυτό το μήνυμα σφάλματος. Στο περιβάλλον παραγωγής, το `E_USER_WARNING` δημιουργείται πάντα. Αυτή η συμπεριφορά μπορεί επίσης να ελεγχθεί ορίζοντας τη μεταβλητή του presenter [$invalidLinkMode |creating-links#Μη Έγκυροι Σύνδεσμοι]. - -Τα [Ψευδώνυμα απλοποιούν τη σύνδεση |creating-links#Ψευδώνυμα] σε συχνά χρησιμοποιούμενους presenters. - -Η [Αντιστοίχιση ορίζει κανόνες |directory-structure#Αντιστοίχιση Presenters], σύμφωνα με τους οποίους το όνομα της κλάσης προκύπτει από το όνομα του presenter. - - -Αυτόματη καταχώρηση presenters ------------------------------- - -Το Nette προσθέτει αυτόματα τους presenters ως υπηρεσίες στο DI container, γεγονός που επιταχύνει σημαντικά τη δημιουργία τους. Ο τρόπος με τον οποίο το Nette βρίσκει τους presenters μπορεί να διαμορφωθεί: - -```neon -application: - # αναζήτηση presenters στο Composer class map; - scanComposer: ... # (bool) προεπιλογή είναι true - - # μάσκα που πρέπει να ταιριάζει με το όνομα της κλάσης και του αρχείου - scanFilter: ... # (string) προεπιλογή είναι '*Presenter' - - # σε ποιους καταλόγους να αναζητηθούν οι presenters; - scanDirs: # (string[]|false) προεπιλογή είναι '%appDir%' - - %vendorDir%/mymodule -``` - -Οι κατάλογοι που αναφέρονται στο `scanDirs` δεν αντικαθιστούν την προεπιλεγμένη τιμή `%appDir%`, αλλά την συμπληρώνουν, οπότε το `scanDirs` θα περιέχει και τις δύο διαδρομές `%appDir%` και `%vendorDir%/mymodule`. Αν θέλουμε να παραλείψουμε τον προεπιλεγμένο κατάλογο, χρησιμοποιούμε ένα [θαυμαστικό |dependency-injection:configuration#Συγχώνευση], το οποίο αντικαθιστά την τιμή: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Η σάρωση καταλόγων μπορεί να απενεργοποιηθεί καθορίζοντας την τιμή false. Δεν συνιστούμε την πλήρη καταστολή της αυτόματης προσθήκης presenters, καθώς αυτό θα μειώσει την απόδοση της εφαρμογής. - - -Templates Latte -=============== - -Με αυτή τη ρύθμιση, μπορείτε να επηρεάσετε καθολικά τη συμπεριφορά του Latte στα components και τους presenters. - -```neon -latte: - # εμφάνιση του πίνακα Latte στο Tracy Bar για το κύριο template (true) ή όλα τα components (all); - debugger: ... # (true|false|'all') προεπιλογή είναι true - - # δημιουργεί templates με την κεφαλίδα declare(strict_types=1) - strictTypes: ... # (bool) προεπιλογή είναι false - - # ενεργοποιεί την [κατάσταση αυστηρού parser |latte:develop#striktní režim] - strictParsing: ... # (bool) προεπιλογή είναι false - - # ενεργοποιεί τον [έλεγχο του παραγόμενου κώδικα |latte:develop#Kontrola vygenerovaného kódu] - phpLinter: ... # (string) προεπιλογή είναι null - - # ορίζει το locale - locale: cs_CZ # (string) προεπιλογή είναι null - - # κλάση του αντικειμένου $this->template - templateClass: App\MyTemplateClass # προεπιλογή είναι Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Αν χρησιμοποιείτε την έκδοση 3 του Latte, μπορείτε να προσθέσετε νέες [επεκτάσεις |latte:extending-latte#Latte Extension] χρησιμοποιώντας: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Αν χρησιμοποιείτε την έκδοση 2 του Latte, μπορείτε να καταχωρήσετε νέα tags (macros) είτε καθορίζοντας το όνομα της κλάσης είτε με αναφορά σε μια υπηρεσία. Ως προεπιλογή, καλείται η μέθοδος `install()`, αλλά αυτό μπορεί να αλλάξει καθορίζοντας το όνομα μιας άλλης μεθόδου: - -```neon -latte: - # καταχώρηση προσαρμοσμένων Latte tags - macros: - - App\MyLatteMacros::register # στατική μέθοδος, όνομα κλάσης ή callable - - @App\MyLatteMacrosFactory # υπηρεσία με μέθοδο install() - - @App\MyLatteMacrosFactory::register # υπηρεσία με μέθοδο register() - -services: - - App\MyLatteMacrosFactory -``` - - -Δρομολόγηση -=========== - -Βασικές ρυθμίσεις: - -```neon -routing: - # εμφάνιση του πίνακα δρομολόγησης στο Tracy Bar; - debugger: ... # (bool) προεπιλογή είναι true - - # σειριοποιεί τον router στο DI container - cache: ... # (bool) προεπιλογή είναι false -``` - -Η δρομολόγηση συνήθως ορίζεται στην κλάση [RouterFactory |routing#Συλλογή διαδρομών]. Εναλλακτικά, οι διαδρομές (routes) μπορούν επίσης να οριστούν στη διαμόρφωση χρησιμοποιώντας ζεύγη `mask: action`, αλλά αυτή η μέθοδος δεν προσφέρει τόσο μεγάλη ευελιξία στις ρυθμίσεις: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Σταθερές -======== - -Δημιουργία σταθερών PHP. - -```neon -constants: - Foobar: 'baz' -``` - -Μετά την εκκίνηση της εφαρμογής, θα δημιουργηθεί η σταθερά `Foobar`. - -.[note] -Οι σταθερές δεν πρέπει να χρησιμεύουν ως κάποιου είδους καθολικά διαθέσιμες μεταβλητές. Για τη μεταβίβαση τιμών σε αντικείμενα, χρησιμοποιήστε το [dependency injection |dependency-injection:passing-dependencies]. - - -PHP -=== - -Ρύθμιση οδηγιών PHP. Μια επισκόπηση όλων των οδηγιών μπορείτε να βρείτε στο [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -Υπηρεσίες DI -============ - -Αυτές οι υπηρεσίες προστίθενται στο DI container: - -| Όνομα | Τύπος | Περιγραφή -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [εκκινητής ολόκληρης της εφαρμογής |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | factory για presenters -| `application.###` | [api:Nette\Application\UI\Presenter] | μεμονωμένοι presenters -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | factory αντικειμένου `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | factory για [`$this->template` |templates] diff --git a/application/el/creating-links.texy b/application/el/creating-links.texy deleted file mode 100644 index bd357844e1..0000000000 --- a/application/el/creating-links.texy +++ /dev/null @@ -1,286 +0,0 @@ -Δημιουργία συνδέσμων URL -************************ - -
    - -Η δημιουργία συνδέσμων στο Nette είναι τόσο απλή όσο το να δείχνεις με το δάχτυλο. Απλά στοχεύστε και το framework θα κάνει όλη τη δουλειά για εσάς. Θα δείξουμε: - -- πώς να δημιουργείτε συνδέσμους σε templates και αλλού -- πώς να διακρίνετε έναν σύνδεσμο προς την τρέχουσα σελίδα -- τι να κάνετε με τους μη έγκυρους συνδέσμους - -
    - - -Χάρη στην [αμφίδρομη δρομολόγηση |routing], δεν θα χρειαστεί ποτέ να γράψετε σκληρά κωδικοποιημένες διευθύνσεις URL της εφαρμογής σας σε templates ή κώδικα, οι οποίες μπορεί να αλλάξουν αργότερα, ή να τις συνθέσετε πολύπλοκα. Στον σύνδεσμο, αρκεί να καθορίσετε τον presenter και την action, να περάσετε τυχόν παραμέτρους και το framework θα δημιουργήσει το URL μόνο του. Στην πραγματικότητα, είναι πολύ παρόμοιο με την κλήση μιας συνάρτησης. Αυτό θα σας αρέσει. - - -Στο Πρότυπο του Presenter -========================= - -Τις περισσότερες φορές δημιουργούμε συνδέσμους σε templates και ένα εξαιρετικό βοήθημα είναι το attribute `n:href`: - -```latte -λεπτομέρεια -``` - -Παρατηρήστε ότι αντί για το HTML attribute `href`, χρησιμοποιήσαμε το [n:attribute |latte:syntax#n:attributes] `n:href`. Η τιμή του δεν είναι ένα URL, όπως θα ήταν στην περίπτωση του attribute `href`, αλλά το όνομα του presenter και της action. - -Το κλικ σε έναν σύνδεσμο είναι, απλοποιημένα, κάτι σαν την κλήση της μεθόδου `ProductPresenter::renderShow()`. Και αν έχει παραμέτρους στην υπογραφή της, μπορούμε να την καλέσουμε με ορίσματα: - -```latte -λεπτομέρεια προϊόντος -``` - -Είναι επίσης δυνατό να περάσετε ονομασμένες παραμέτρους. Ο παρακάτω σύνδεσμος περνάει την παράμετρο `lang` με την τιμή `cs`: - -```latte -λεπτομέρεια προϊόντος -``` - -Αν η μέθοδος `ProductPresenter::renderShow()` δεν έχει το `$lang` στην υπογραφή της, μπορεί να λάβει την τιμή της παραμέτρου χρησιμοποιώντας το `$lang = $this->getParameter('lang')` ή από την [property |presenters#Παράμετροι αιτήματος]. - -Αν οι παράμετροι είναι αποθηκευμένες σε έναν πίνακα, μπορούν να επεκταθούν με τον τελεστή `...` (στο Latte 2.x με τον τελεστή `(expand)`): - -```latte -{var $args = [$product->id, lang => cs]} -λεπτομέρεια προϊόντος -``` - -Στους συνδέσμους, μεταβιβάζονται επίσης αυτόματα οι λεγόμενες [persistent παράμετροι |presenters#Persistent παράμετροι]. - -Το attribute `n:href` είναι πολύ χρήσιμο για τις ετικέτες HTML ``. Αν θέλουμε να εμφανίσουμε έναν σύνδεσμο αλλού, για παράδειγμα σε κείμενο, χρησιμοποιούμε το `{link}`: - -```latte -Η διεύθυνση είναι: {link Home:default} -``` - - -Στον Κώδικα -=========== - -Για τη δημιουργία ενός συνδέσμου στον presenter, χρησιμοποιείται η μέθοδος `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Οι παράμετροι μπορούν επίσης να περαστούν χρησιμοποιώντας έναν πίνακα, όπου μπορούν επίσης να καθοριστούν ονομασμένες παράμετροι: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); -``` - -Οι σύνδεσμοι μπορούν επίσης να δημιουργηθούν χωρίς presenter, γι' αυτό υπάρχει το [#LinkGenerator] και η μέθοδός του `link()`. - - -Σύνδεσμοι προς Presenter -======================== - -Αν ο στόχος του συνδέσμου είναι ένας presenter και μια action, έχει αυτή τη σύνταξη: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Η μορφή υποστηρίζεται από όλες τις ετικέτες Latte και όλες τις μεθόδους του presenter που λειτουργούν με συνδέσμους, δηλαδή `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` και επίσης το [#LinkGenerator]. Έτσι, ακόμα κι αν χρησιμοποιείται το `n:href` στα παραδείγματα, θα μπορούσε να είναι οποιαδήποτε από τις συναρτήσεις. - -Η βασική μορφή είναι επομένως `Presenter:action`: - -```latte -αρχική σελίδα -``` - -Αν συνδέουμε σε μια action του τρέχοντος presenter, μπορούμε να παραλείψουμε το όνομά του: - -```latte -αρχική σελίδα -``` - -Αν ο στόχος είναι η action `default`, μπορούμε να την παραλείψουμε, αλλά η άνω και κάτω τελεία πρέπει να παραμείνει: - -```latte -αρχική σελίδα -``` - -Οι σύνδεσμοι μπορούν επίσης να οδηγούν σε άλλα [modules |directory-structure#Presenters και Πρότυπα]. Εδώ, οι σύνδεσμοι διακρίνονται σε σχετικούς προς ένα ένθετο sub-module, ή απόλυτους. Η αρχή είναι ανάλογη με τις διαδρομές στο δίσκο, μόνο που αντί για κάθετες χρησιμοποιούνται άνω και κάτω τελείες. Ας υποθέσουμε ότι ο τρέχων presenter είναι μέρος του module `Front`, τότε γράφουμε: - -```latte -σύνδεσμος προς Front:Shop:Product:show -σύνδεσμος προς Admin:Product:show -``` - -Μια ειδική περίπτωση είναι ένας σύνδεσμος [προς τον εαυτό του |#Σύνδεσμος προς την Τρέχουσα Σελίδα], όπου καθορίζουμε το `this` ως στόχο. - -```latte -ανανέωση -``` - -Μπορούμε να συνδέσουμε σε ένα συγκεκριμένο τμήμα της σελίδας μέσω ενός λεγόμενου fragment μετά το σύμβολο δίεσης `#`: - -```latte -σύνδεσμος προς Home:default και fragment #main -``` - - -Απόλυτες Διαδρομές -================== - -Οι σύνδεσμοι που δημιουργούνται χρησιμοποιώντας το `link()` ή το `n:href` είναι πάντα απόλυτες διαδρομές (δηλαδή ξεκινούν με το σύμβολο `/`), αλλά όχι απόλυτες διευθύνσεις URL με πρωτόκολλο και domain όπως `https://domain`. - -Για να δημιουργήσετε μια απόλυτη διεύθυνση URL, προσθέστε δύο κάθετες στην αρχή (π.χ. `n:href="//Home:"`). Ή μπορείτε να αλλάξετε τον presenter ώστε να δημιουργεί μόνο απόλυτους συνδέσμους ορίζοντας `$this->absoluteUrls = true`. - - -Σύνδεσμος προς την Τρέχουσα Σελίδα -================================== - -Ο στόχος `this` δημιουργεί έναν σύνδεσμο προς την τρέχουσα σελίδα: - -```latte -ανανέωση -``` - -Ταυτόχρονα, μεταβιβάζονται όλες οι παράμετροι που καθορίζονται στην υπογραφή της μεθόδου `action()` ή `render()`, αν η `action()` δεν έχει οριστεί. Έτσι, αν βρισκόμαστε στη σελίδα `Product:show` και `id: 123`, ο σύνδεσμος προς το `this` θα μεταβιβάσει και αυτή την παράμετρο. - -Φυσικά, είναι δυνατό να καθορίσετε τις παραμέτρους απευθείας: - -```latte -ανανέωση -``` - -Η συνάρτηση `isLinkCurrent()` ελέγχει εάν ο στόχος του συνδέσμου είναι ο ίδιος με την τρέχουσα σελίδα. Αυτό μπορεί να χρησιμοποιηθεί, για παράδειγμα, σε ένα template για τη διάκριση συνδέσμων κ.λπ. - -Οι παράμετροι είναι ίδιες με αυτές της μεθόδου `link()`, αλλά επιπλέον είναι δυνατό να καθορίσετε έναν χαρακτήρα μπαλαντέρ `*` αντί για μια συγκεκριμένη action, ο οποίος σημαίνει οποιαδήποτε action του συγκεκριμένου presenter. - -```latte -{if !isLinkCurrent('Admin:login')} - Σύνδεση -{/if} - -
  • - ... -
  • -``` - -Σε συνδυασμό με το `n:href` σε ένα στοιχείο, μπορεί να χρησιμοποιηθεί μια συντομευμένη μορφή: - -```latte -... -``` - -Ο χαρακτήρας μπαλαντέρ `*` μπορεί να χρησιμοποιηθεί μόνο αντί για την action, όχι για τον presenter. - -Για να ελέγξουμε εάν βρισκόμαστε σε ένα συγκεκριμένο module ή το sub-module του, χρησιμοποιούμε τη μέθοδο `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Σύνδεσμοι προς Σήμα -=================== - -Ο στόχος ενός συνδέσμου δεν χρειάζεται να είναι μόνο ένας presenter και μια action, αλλά μπορεί επίσης να είναι ένα [signal |components#Σήμα] (καλούν τη μέθοδο `handle()`). Τότε η σύνταξη είναι η εξής: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -Το signal διακρίνεται λοιπόν από το θαυμαστικό: - -```latte -signal -``` - -Μπορείτε επίσης να δημιουργήσετε έναν σύνδεσμο προς το signal ενός sub-component (ή sub-sub-component): - -```latte -signal -``` - - -Σύνδεσμοι σε Component -====================== - -Επειδή τα [components|components] είναι ανεξάρτητες, επαναχρησιμοποιήσιμες μονάδες που δεν θα πρέπει να έχουν καμία σύνδεση με τους γύρω presenters, οι σύνδεσμοι λειτουργούν λίγο διαφορετικά εδώ. Το attribute Latte `n:href` και η ετικέτα `{link}` καθώς και οι μέθοδοι του component όπως το `link()` και άλλες θεωρούν τον στόχο του συνδέσμου **πάντα ως το όνομα του signal**. Επομένως, δεν είναι καν απαραίτητο να συμπεριλάβετε το θαυμαστικό: - -```latte -signal, όχι action -``` - -Αν θέλαμε να συνδέσουμε σε presenters στο template του component, θα χρησιμοποιούσαμε την ετικέτα `{plink}`: - -```latte -αρχική -``` - -ή στον κώδικα - -```php -$this->getPresenter()->link('Home:default') -``` - - -Ψευδώνυμα .{data-version:v3.2.2} -================================ - -Μερικές φορές μπορεί να είναι χρήσιμο να αντιστοιχίσετε ένα εύκολα απομνημονεύσιμο ψευδώνυμο σε ένα ζεύγος Presenter:action. Για παράδειγμα, να ονομάσετε την αρχική σελίδα `Front:Home:default` απλά ως `home` ή το `Admin:Dashboard:default` ως `admin`. - -Τα ψευδώνυμα ορίζονται στη [διαμόρφωση|configuration] κάτω από το κλειδί `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -Στους συνδέσμους, γράφονται στη συνέχεια χρησιμοποιώντας το σύμβολο @, για παράδειγμα: - -```latte -διαχείριση -``` - -Υποστηρίζονται επίσης σε όλες τις μεθόδους που λειτουργούν με συνδέσμους, όπως το `redirect()` και παρόμοιες. - - -Μη Έγκυροι Σύνδεσμοι -==================== - -Μπορεί να συμβεί να δημιουργήσουμε έναν μη έγκυρο σύνδεσμο - είτε επειδή οδηγεί σε έναν ανύπαρκτο presenter, είτε επειδή περνάει περισσότερες παραμέτρους από όσες δέχεται η μέθοδος προορισμού στην υπογραφή της, είτε όταν δεν μπορεί να δημιουργηθεί URL για την action προορισμού. Ο τρόπος χειρισμού των μη έγκυρων συνδέσμων καθορίζεται από τη στατική μεταβλητή `Presenter::$invalidLinkMode`. Αυτή μπορεί να πάρει έναν συνδυασμό αυτών των τιμών (σταθερών): - -- `Presenter::InvalidLinkSilent` - σιωπηλή λειτουργία, το σύμβολο # επιστρέφεται ως URL -- `Presenter::InvalidLinkWarning` - δημιουργείται μια προειδοποίηση E_USER_WARNING, η οποία θα καταγραφεί στη λειτουργία παραγωγής, αλλά δεν θα προκαλέσει διακοπή της εκτέλεσης του σεναρίου -- `Presenter::InvalidLinkTextual` - οπτική προειδοποίηση, εμφανίζει το σφάλμα απευθείας στον σύνδεσμο -- `Presenter::InvalidLinkException` - δημιουργείται η εξαίρεση InvalidLinkException - -Η προεπιλεγμένη ρύθμιση είναι `InvalidLinkWarning` στη λειτουργία παραγωγής και `InvalidLinkWarning | InvalidLinkTextual` στη λειτουργία ανάπτυξης. Το `InvalidLinkWarning` στο περιβάλλον παραγωγής δεν προκαλεί διακοπή του σεναρίου, αλλά η προειδοποίηση θα καταγραφεί. Στο περιβάλλον ανάπτυξης, το [Tracy |tracy:] το συλλαμβάνει και εμφανίζει ένα bluescreen. Το `InvalidLinkTextual` λειτουργεί επιστρέφοντας ένα μήνυμα σφάλματος ως URL, το οποίο ξεκινά με τους χαρακτήρες `#error:`. Για να κάνουμε τέτοιους συνδέσμους ορατούς με την πρώτη ματιά, προσθέτουμε στο CSS μας: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Αν δεν θέλουμε να δημιουργούνται προειδοποιήσεις στο περιβάλλον ανάπτυξης, μπορούμε να ορίσουμε τη σιωπηλή λειτουργία απευθείας στη [διαμόρφωση|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Πώς να δημιουργήσετε συνδέσμους με παρόμοια άνεση όπως η μέθοδος `link()`, αλλά χωρίς την παρουσία ενός presenter; Γι' αυτό υπάρχει το [api:Nette\Application\LinkGenerator]. - -Το LinkGenerator είναι μια υπηρεσία που μπορείτε να ζητήσετε να σας περάσει μέσω του constructor και στη συνέχεια να δημιουργήσετε συνδέσμους χρησιμοποιώντας τη μέθοδό του `link()`. - -Υπάρχει μια διαφορά σε σύγκριση με τους presenters. Το LinkGenerator δημιουργεί όλους τους συνδέσμους απευθείας ως απόλυτες διευθύνσεις URL. Επιπλέον, δεν υπάρχει "τρέχων presenter", οπότε δεν μπορείτε να καθορίσετε μόνο το όνομα της action ως στόχο `link('default')` ή να καθορίσετε σχετικές διαδρομές προς τα modules. - -Οι μη έγκυροι σύνδεσμοι δημιουργούν πάντα την εξαίρεση `Nette\Application\UI\InvalidLinkException`. diff --git a/application/el/directory-structure.texy b/application/el/directory-structure.texy deleted file mode 100644 index c0a5c4906d..0000000000 --- a/application/el/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Δομή Καταλόγου της Εφαρμογής -**************************** - -
    - -Πώς να σχεδιάσετε μια σαφή και επεκτάσιμη δομή καταλόγων για έργα στο Nette Framework; Θα σας δείξουμε δοκιμασμένες πρακτικές που θα σας βοηθήσουν να οργανώσετε τον κώδικά σας. Θα μάθετε: - -- πώς να **χωρίσετε λογικά** την εφαρμογή σε καταλόγους -- πώς να σχεδιάσετε τη δομή ώστε να **επεκτείνεται καλά** με την ανάπτυξη του έργου -- ποιες είναι οι **πιθανές εναλλακτικές** και τα πλεονεκτήματα ή μειονεκτήματά τους - -
    - - -Είναι σημαντικό να αναφέρουμε ότι το ίδιο το Nette Framework δεν επιμένει σε καμία συγκεκριμένη δομή. Είναι σχεδιασμένο έτσι ώστε να μπορεί εύκολα να προσαρμοστεί σε οποιεσδήποτε ανάγκες και προτιμήσεις. - - -Βασική Δομή Έργου -================= - -Παρόλο που το Nette Framework δεν υπαγορεύει καμία σταθερή δομή καταλόγων, υπάρχει μια δοκιμασμένη προεπιλεγμένη διάταξη με τη μορφή του [Web Project|https://github.com/nette/web-project]: - -/--pre -web-project/ -├── app/ ← κατάλογος με την εφαρμογή -├── assets/ ← αρχεία SCSS, JS, εικόνες..., εναλλακτικά resources/ -├── bin/ ← σενάρια για τη γραμμή εντολών -├── config/ ← διαμόρφωση -├── log/ ← καταγεγραμμένα σφάλματα -├── temp/ ← προσωρινά αρχεία, cache -├── tests/ ← δοκιμές -├── vendor/ ← βιβλιοθήκες εγκατεστημένες από τον Composer -└── www/ ← δημόσιος κατάλογος (document-root) -\-- - -Μπορείτε να τροποποιήσετε αυτή τη δομή ελεύθερα σύμφωνα με τις ανάγκες σας - να μετονομάσετε ή να μετακινήσετε φακέλους. Στη συνέχεια, αρκεί μόνο να ενημερώσετε τις σχετικές διαδρομές προς τους καταλόγους στο αρχείο `Bootstrap.php` και ενδεχομένως στο `composer.json`. Τίποτα περισσότερο δεν χρειάζεται, καμία πολύπλοκη επαναδιαμόρφωση, καμία αλλαγή σταθερών. Το Nette διαθέτει έξυπνη αυτόματη ανίχνευση και αναγνωρίζει αυτόματα τη θέση της εφαρμογής, συμπεριλαμβανομένης της βασικής της διεύθυνσης URL. - - -Αρχές Οργάνωσης Κώδικα -====================== - -Όταν εξερευνάτε για πρώτη φορά ένα νέο έργο, θα πρέπει να μπορείτε να προσανατολιστείτε γρήγορα σε αυτό. Φανταστείτε ότι ανοίγετε τον κατάλογο `app/Model/` και βλέπετε αυτή τη δομή: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -Από αυτό, μπορείτε να συμπεράνετε μόνο ότι το έργο χρησιμοποιεί κάποιες υπηρεσίες, repositories και entities. Δεν μαθαίνετε τίποτα για τον πραγματικό σκοπό της εφαρμογής. - -Ας δούμε μια διαφορετική προσέγγιση - **οργάνωση ανά τομείς**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Εδώ είναι διαφορετικά - με την πρώτη ματιά είναι σαφές ότι πρόκειται για ένα e-shop. Τα ίδια τα ονόματα των καταλόγων αποκαλύπτουν τι μπορεί να κάνει η εφαρμογή - λειτουργεί με πληρωμές, παραγγελίες και προϊόντα. - -Η πρώτη προσέγγιση (οργάνωση ανά τύπο κλάσης) φέρνει στην πράξη μια σειρά προβλημάτων: ο κώδικας που σχετίζεται λογικά είναι διάσπαρτος σε διαφορετικούς φακέλους και πρέπει να πηδάτε μεταξύ τους. Γι' αυτό θα οργανώσουμε ανά τομείς. - - -Χώροι Ονομάτων --------------- - -Είναι σύνηθες η δομή καταλόγων να αντιστοιχεί στους χώρους ονομάτων στην εφαρμογή. Αυτό σημαίνει ότι η φυσική θέση των αρχείων αντιστοιχεί στο namespace τους. Για παράδειγμα, μια κλάση που βρίσκεται στο `app/Model/Product/ProductRepository.php` θα πρέπει να έχει το namespace `App\Model\Product`. Αυτή η αρχή βοηθά στον προσανατολισμό στον κώδικα και απλοποιεί την αυτόματη φόρτωση (autoloading). - - -Ενικός vs Πληθυντικός Αριθμός στα Ονόματα ------------------------------------------ - -Παρατηρήστε ότι για τους κύριους καταλόγους της εφαρμογής χρησιμοποιούμε ενικό αριθμό: `app`, `config`, `log`, `temp`, `www`. Το ίδιο και μέσα στην εφαρμογή: `Model`, `Core`, `Presentation`. Αυτό συμβαίνει επειδή καθένας από αυτούς αντιπροσωπεύει μια ενιαία, ολοκληρωμένη έννοια. - -Ομοίως, για παράδειγμα, το `app/Model/Product` αντιπροσωπεύει τα πάντα γύρω από τα προϊόντα. Δεν θα το ονομάσουμε `Products`, επειδή δεν είναι ένας φάκελος γεμάτος προϊόντα (αυτό θα σήμαινε ότι θα υπήρχαν αρχεία `nokia.php`, `samsung.php`). Είναι ένας namespace που περιέχει κλάσεις για την εργασία με προϊόντα - `ProductRepository.php`, `ProductService.php`. - -Ο φάκελος `app/Tasks` είναι στον πληθυντικό αριθμό επειδή περιέχει ένα σύνολο ανεξάρτητων εκτελέσιμων σεναρίων - `CleanupTask.php`, `ImportTask.php`. Καθένα από αυτά είναι μια ξεχωριστή μονάδα. - -Για λόγους συνέπειας, συνιστούμε να χρησιμοποιείτε: -- Ενικό αριθμό για namespace που αντιπροσωπεύει μια λειτουργική ενότητα (byť pracující s více entitami) -- Πληθυντικό αριθμό για συλλογές ανεξάρτητων μονάδων -- Σε περίπτωση αβεβαιότητας ή αν δεν θέλετε να το σκεφτείτε, επιλέξτε τον ενικό αριθμό - - -Δημόσιος Κατάλογος `www/` -========================= - -Αυτός ο κατάλογος είναι ο μόνος προσβάσιμος από τον ιστό (το λεγόμενο document-root). Συχνά μπορείτε να συναντήσετε και το όνομα `public/` αντί για `www/` - είναι απλώς θέμα σύμβασης και δεν επηρεάζει τη λειτουργικότητα. Ο κατάλογος περιέχει: -- Το [σημείο εισόδου |bootstrapping#index.php] της εφαρμογής `index.php` -- Το αρχείο `.htaccess` με κανόνες για το mod_rewrite (για τον Apache) -- Στατικά αρχεία (CSS, JavaScript, εικόνες) -- Ανεβασμένα αρχεία - -Για τη σωστή ασφάλεια της εφαρμογής, είναι ζωτικής σημασίας να έχετε σωστά [διαμορφωμένο το document-root |nette:troubleshooting#Πώς να αλλάξετε ή να αφαιρέσετε τον κατάλογο www από το URL]. - -.[note] -Ποτέ μην τοποθετείτε τον φάκελο `node_modules/` σε αυτόν τον κατάλογο - περιέχει χιλιάδες αρχεία που μπορεί να είναι εκτελέσιμα και δεν θα πρέπει να είναι δημόσια προσβάσιμα. - - -Κατάλογος Εφαρμογής `app/` -========================== - -Αυτός είναι ο κύριος κατάλογος με τον κώδικα της εφαρμογής. Η βασική δομή: - -/--pre -app/ -├── Core/ ← θέματα υποδομής -├── Model/ ← business λογική -├── Presentation/ ← presenters και templates -├── Tasks/ ← σενάρια εντολών -└── Bootstrap.php ← κλάση εκκίνησης της εφαρμογής -\-- - -Το `Bootstrap.php` είναι η [κλάση εκκίνησης της εφαρμογής|bootstrapping], η οποία αρχικοποιεί το περιβάλλον, φορτώνει τη διαμόρφωση και δημιουργεί το DI container. - -Ας ρίξουμε τώρα μια πιο λεπτομερή ματιά στους επιμέρους υποκαταλόγους. - - -Presenters και Πρότυπα -====================== - -Το τμήμα παρουσίασης της εφαρμογής βρίσκεται στον κατάλογο `app/Presentation`. Μια εναλλακτική είναι το σύντομο `app/UI`. Είναι ο τόπος για όλους τους presenters, τα templates τους και τυχόν βοηθητικές κλάσεις. - -Οργανώνουμε αυτό το επίπεδο ανά τομείς. Σε ένα σύνθετο έργο που συνδυάζει e-shop, blog και API, η δομή θα έμοιαζε ως εξής: - -/--pre -app/Presentation/ -├── Shop/ ← e-shop frontend -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← διαχείριση -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← API endpoints - └── V1/ -\-- - -Αντίθετα, για ένα απλό blog, θα χρησιμοποιούσαμε την εξής διάρθρωση: - -/--pre -app/Presentation/ -├── Front/ ← frontend webu -│ ├── Home/ -│ └── Post/ -├── Admin/ ← διαχείριση -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, sitemaps κ.λπ. -\-- - -Φάκελοι όπως `Home/` ή `Dashboard/` περιέχουν presenters και templates. Φάκελοι όπως `Front/`, `Admin/` ή `Api/` ονομάζονται **modules**. Τεχνικά, πρόκειται για συνηθισμένους καταλόγους που χρησιμεύουν για τη λογική διάρθρωση της εφαρμογής. - -Κάθε φάκελος με presenter περιέχει έναν ομώνυμο presenter και τα templates του. Για παράδειγμα, ο φάκελος `Dashboard/` περιέχει: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← template -\-- - -Αυτή η δομή καταλόγων αντικατοπτρίζεται στους χώρους ονομάτων των κλάσεων. Για παράδειγμα, το `DashboardPresenter` βρίσκεται στον χώρο ονομάτων `App\Presentation\Admin\Dashboard` (βλ. [#Αντιστοίχιση Presenters]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Στον presenter `Dashboard` μέσα στο module `Admin` αναφερόμαστε στην εφαρμογή χρησιμοποιώντας τη σημειογραφία με άνω και κάτω τελεία ως `Admin:Dashboard`. Στην action του `default` στη συνέχεια ως `Admin:Dashboard:default`. Σε περίπτωση ένθετων modules, χρησιμοποιούμε περισσότερες άνω και κάτω τελείες, για παράδειγμα `Shop:Order:Detail:default`. - - -Ευέλικτη Ανάπτυξη Δομής ------------------------ - -Ένα από τα μεγάλα πλεονεκτήματα αυτής της δομής είναι το πόσο κομψά προσαρμόζεται στις αυξανόμενες ανάγκες του έργου. Ας πάρουμε ως παράδειγμα το τμήμα που δημιουργεί XML feeds. Στην αρχή, έχουμε μια απλή μορφή: - -/--pre -Export/ -├── ExportPresenter.php ← ένας presenter για όλες τις εξαγωγές -├── sitemap.latte ← template για το sitemap -└── feed.latte ← template για το RSS feed -\-- - -Με τον καιρό, προστίθενται περισσότεροι τύποι feeds και χρειαζόμαστε περισσότερη λογική γι' αυτούς... Κανένα πρόβλημα! Ο φάκελος `Export/` γίνεται απλά ένα module: - -/--pre -Export/ -├── Sitemap/ -│ ├── SitemapPresenter.php -│ └── sitemap.latte -└── Feed/ - ├── FeedPresenter.php - ├── zbozi.latte ← feed για το Zboží.cz - └── heureka.latte ← feed για το Heureka.cz -\-- - -Αυτή η μετατροπή είναι απολύτως ομαλή - αρκεί να δημιουργήσετε νέους υποφακέλους, να χωρίσετε τον κώδικα σε αυτούς και να ενημερώσετε τους συνδέσμους (π.χ. από `Export:feed` σε `Export:Feed:zbozi`). Χάρη σε αυτό, μπορούμε να επεκτείνουμε σταδιακά τη δομή ανάλογα με τις ανάγκες, το επίπεδο ένθεσης δεν περιορίζεται με κανέναν τρόπο. - -Αν, για παράδειγμα, στη διαχείριση έχετε πολλούς presenters που σχετίζονται με τη διαχείριση παραγγελιών, όπως `OrderDetail`, `OrderEdit`, `OrderDispatch` κ.λπ., μπορείτε για καλύτερη οργάνωση σε αυτό το σημείο να δημιουργήσετε ένα module (φάκελο) `Order`, στο οποίο θα βρίσκονται (οι φάκελοι για) οι presenters `Detail`, `Edit`, `Dispatch` και άλλοι. - - -Τοποθέτηση Προτύπων -------------------- - -Στα προηγούμενα παραδείγματα, είδαμε ότι τα templates βρίσκονται απευθείας στον φάκελο με τον presenter: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← προαιρετική κλάση για το template -└── default.latte ← template -\-- - -Αυτή η τοποθέτηση αποδεικνύεται στην πράξη η πιο βολική - έχετε όλα τα σχετικά αρχεία αμέσως πρόχειρα. - -Εναλλακτικά, μπορείτε να τοποθετήσετε τα templates στον υποφάκελο `templates/`. Το Nette υποστηρίζει και τις δύο παραλλαγές. Μπορείτε ακόμη και να τοποθετήσετε τα templates εντελώς εκτός του φακέλου `Presentation/`. Όλα σχετικά με τις δυνατότητες τοποθέτησης templates θα βρείτε στο κεφάλαιο [Αναζήτηση templates |templates#Αναζήτηση προτύπου]. - - -Βοηθητικές Κλάσεις και Components ---------------------------------- - -Στους presenters και τα templates συχνά ανήκουν και άλλα βοηθητικά αρχεία. Τα τοποθετούμε λογικά ανάλογα με το πεδίο εφαρμογής τους: - -1. **Απευθείας στον presenter** σε περίπτωση συγκεκριμένων components για τον συγκεκριμένο presenter: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← component για την εμφάνιση προϊόντων -└── FilterForm.php ← φόρμα για φιλτράρισμα -\-- - -2. **Για το module** - συνιστούμε να χρησιμοποιήσετε τον φάκελο `Accessory`, ο οποίος τοποθετείται βολικά στην αρχή της αλφαβήτου: - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← components για το frontend -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **Για ολόκληρη την εφαρμογή** - στο `Presentation/Accessory/`: -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Ή μπορείτε να τοποθετήσετε βοηθητικές κλάσεις όπως `LatteExtension.php` ή `TemplateFilters.php` στον φάκελο υποδομής `app/Core/Latte/`. Και τα components στο `app/Components`. Η επιλογή εξαρτάται από τις συνήθειες της ομάδας. - - -Model - Η Καρδιά της Εφαρμογής -============================== - -Το Model περιέχει όλη την business λογική της εφαρμογής. Για την οργάνωσή του ισχύει ξανά ο κανόνας - δομούμε ανά τομείς: - -/--pre -app/Model/ -├── Payment/ ← όλα γύρω από τις πληρωμές -│ ├── PaymentFacade.php ← κύριο σημείο εισόδου -│ ├── PaymentRepository.php -│ ├── Payment.php ← entity -├── Order/ ← όλα γύρω από τις παραγγελίες -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← όλα γύρω από την αποστολή -\-- - -Στο model, τυπικά συναντάμε αυτούς τους τύπους κλάσεων: - -**Facades**: αντιπροσωπεύουν το κύριο σημείο εισόδου σε έναν συγκεκριμένο τομέα στην εφαρμογή. Λειτουργούν ως ενορχηστρωτής, που συντονίζει τη συνεργασία μεταξύ διαφόρων υπηρεσιών με σκοπό την υλοποίηση πλήρων use-cases (όπως "δημιουργία παραγγελίας" ή "επεξεργασία πληρωμής"). Κάτω από το επίπεδο ενορχήστρωσης, η facade κρύβει τις λεπτομέρειες υλοποίησης από την υπόλοιπη εφαρμογή, παρέχοντας έτσι μια καθαρή διεπαφή για την εργασία με τον συγκεκριμένο τομέα. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // επικύρωση - // δημιουργία παραγγελίας - // αποστολή e-mail - // καταγραφή στα στατιστικά - } -} -``` - -**Υπηρεσίες (Services)**: εστιάζουν σε μια συγκεκριμένη business λειτουργία εντός του τομέα. Σε αντίθεση με τη facade, η οποία ενορχηστρώνει ολόκληρα use-cases, μια υπηρεσία υλοποιεί συγκεκριμένη business λογική (όπως υπολογισμούς τιμών ή επεξεργασία πληρωμών). Οι υπηρεσίες είναι τυπικά stateless και μπορούν να χρησιμοποιηθούν είτε από facades ως δομικά στοιχεία για πιο σύνθετες λειτουργίες, είτε απευθείας από άλλα μέρη της εφαρμογής για απλούστερες εργασίες. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // υπολογισμός τιμής - } -} -``` - -**Repositories**: εξασφαλίζουν όλη την επικοινωνία με τον χώρο αποθήκευσης δεδομένων, τυπικά μια βάση δεδομένων. Ο ρόλος του είναι η φόρτωση και η αποθήκευση entities και η υλοποίηση μεθόδων για την αναζήτησή τους. Το repository απομονώνει την υπόλοιπη εφαρμογή από τις λεπτομέρειες υλοποίησης της βάσης δεδομένων και παρέχει μια αντικειμενοστραφή διεπαφή για την εργασία με δεδομένα. - -```php -class OrderRepository -{ - public function find(int $id): ?Order - { - } - - public function findByCustomer(int $customerId): array - { - } -} -``` - -**Entities**: αντικείμενα που αντιπροσωπεύουν τις κύριες business έννοιες στην εφαρμογή, οι οποίες έχουν τη δική τους ταυτότητα και αλλάζουν με την πάροδο του χρόνου. Τυπικά, πρόκειται για κλάσεις που αντιστοιχίζονται σε πίνακες βάσης δεδομένων χρησιμοποιώντας ORM (όπως το Nette Database Explorer ή το Doctrine). Οι entities μπορούν να περιέχουν business κανόνες που σχετίζονται με τα δεδομένα τους και λογική επικύρωσης. - -```php -// Entity αντιστοιχισμένη στον πίνακα βάσης δεδομένων 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 objects**: αμετάβλητα αντικείμενα που αντιπροσωπεύουν τιμές χωρίς δική τους ταυτότητα - για παράδειγμα, ένα χρηματικό ποσό ή μια διεύθυνση e-mail. Δύο παρουσίες ενός value object με τις ίδιες τιμές θεωρούνται ταυτόσημες. - - -Κώδικας Υποδομής -================ - -Ο φάκελος `Core/` (ή επίσης `Infrastructure/`) είναι το σπίτι για την τεχνική βάση της εφαρμογής. Ο κώδικας υποδομής τυπικά περιλαμβάνει: - -/--pre -app/Core/ -├── Router/ ← δρομολόγηση και διαχείριση URL -│ └── RouterFactory.php -├── Security/ ← αυθεντικοποίηση και εξουσιοδότηση -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← καταγραφή και παρακολούθηση -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← επίπεδο προσωρινής αποθήκευσης (caching) -│ └── FullPageCache.php -└── Integration/ ← ενσωμάτωση με εξωτερικές υπηρεσίες - ├── Slack/ - └── Stripe/ -\-- - -Για μικρότερα έργα, φυσικά, αρκεί μια επίπεδη διάρθρωση: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -Πρόκειται για κώδικα που: - -- Επιλύει την τεχνική υποδομή (δρομολόγηση, καταγραφή, caching) -- Ενσωματώνει εξωτερικές υπηρεσίες (Sentry, Elasticsearch, Redis) -- Παρέχει βασικές υπηρεσίες για ολόκληρη την εφαρμογή (mail, βάση δεδομένων) -- Είναι ως επί το πλείστον ανεξάρτητος από τον συγκεκριμένο τομέα - η cache ή ο logger λειτουργεί το ίδιο για eshop ή blog. - -Αναρωτιέστε αν μια συγκεκριμένη κλάση ανήκει εδώ, ή στο model; Η βασική διαφορά είναι ότι ο κώδικας στο `Core/`: - -- Δεν γνωρίζει τίποτα για τον τομέα (προϊόντα, παραγγελίες, άρθρα) -- Είναι ως επί το πλείστον δυνατό να μεταφερθεί σε άλλο έργο -- Επιλύει "πώς λειτουργεί" (πώς να στείλετε mail), όχι "τι κάνει" (ποιο mail να στείλετε) - -Παράδειγμα για καλύτερη κατανόηση: - -- `App\Core\MailerFactory` - δημιουργεί παρουσίες της κλάσης για την αποστολή e-mail, διαχειρίζεται τις ρυθμίσεις SMTP -- `App\Model\OrderMailer` - χρησιμοποιεί το `MailerFactory` για την αποστολή e-mail σχετικά με παραγγελίες, γνωρίζει τα templates τους και πότε πρέπει να σταλούν - - -Σενάρια Εντολών -=============== - -Οι εφαρμογές συχνά χρειάζεται να εκτελούν δραστηριότητες εκτός των συνηθισμένων HTTP requests - είτε πρόκειται για επεξεργασία δεδομένων στο παρασκήνιο, συντήρηση, ή περιοδικές εργασίες. Για την εκτέλεση χρησιμοποιούνται απλά σενάρια στον κατάλογο `bin/`, ενώ η λογική υλοποίησης τοποθετείται στο `app/Tasks/` (ή `app/Commands/`). - -Παράδειγμα: - -/--pre -app/Tasks/ -├── Maintenance/ ← σενάρια συντήρησης -│ ├── CleanupCommand.php ← διαγραφή παλιών δεδομένων -│ └── DbOptimizeCommand.php ← βελτιστοποίηση βάσης δεδομένων -├── Integration/ ← ενσωμάτωση με εξωτερικά συστήματα -│ ├── ImportProducts.php ← εισαγωγή από σύστημα προμηθευτή -│ └── SyncOrders.php ← συγχρονισμός παραγγελιών -└── Scheduled/ ← τακτικές εργασίες - ├── NewsletterCommand.php ← αποστολή newsletter - └── ReminderCommand.php ← ειδοποιήσεις πελατών -\-- - -Τι ανήκει στο model και τι στα σενάρια εντολών; Για παράδειγμα, η λογική για την αποστολή ενός e-mail είναι μέρος του model, η μαζική αποστολή χιλιάδων e-mail ανήκει ήδη στο `Tasks/`. - -Οι εργασίες συνήθως [εκκινούνται από τη γραμμή εντολών |https://blog.nette.org/en/cli-scripts-in-nette-application] ή μέσω cron. Μπορούν επίσης να εκκινηθούν μέσω HTTP request, αλλά είναι απαραίτητο να σκεφτείτε την ασφάλεια. Ο presenter που εκκινεί την εργασία πρέπει να ασφαλιστεί, για παράδειγμα, μόνο για συνδεδεμένους χρήστες ή με ισχυρό token και πρόσβαση από επιτρεπόμενες διευθύνσεις IP. Για μεγάλες εργασίες, είναι απαραίτητο να αυξήσετε το χρονικό όριο του σεναρίου και να χρησιμοποιήσετε το `session_write_close()`, ώστε να μην κλειδώνεται η session. - - -Άλλοι Πιθανοί Κατάλογοι -======================= - -Εκτός από τους βασικούς καταλόγους που αναφέρθηκαν, μπορείτε να προσθέσετε άλλους εξειδικευμένους φακέλους ανάλογα με τις ανάγκες του έργου. Ας ρίξουμε μια ματιά στους πιο συνηθισμένους από αυτούς και τη χρήση τους: - -/--pre -app/ -├── Api/ ← λογική για API ανεξάρτητη από το επίπεδο παρουσίασης -├── Database/ ← σενάρια μετανάστευσης και seeders για δοκιμαστικά δεδομένα -├── Components/ ← κοινόχρηστα οπτικά components σε ολόκληρη την εφαρμογή -├── Event/ ← χρήσιμο αν χρησιμοποιείτε event-driven αρχιτεκτονική -├── Mail/ ← e-mail templates και σχετική λογική -└── Utils/ ← βοηθητικές κλάσεις -\-- - -Για κοινόχρηστα οπτικά components που χρησιμοποιούνται σε presenters σε ολόκληρη την εφαρμογή, μπορείτε να χρησιμοποιήσετε τον φάκελο `app/Components` ή `app/Controls`: - -/--pre -app/Components/ -├── Form/ ← κοινόχρηστα components φόρμας -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← components για λίστες δεδομένων -│ └── DataGrid.php -└── Navigation/ ← στοιχεία πλοήγησης - ├── Breadcrumbs.php - └── Menu.php -\-- - -Εδώ ανήκουν τα components που έχουν πιο σύνθετη λογική. Αν θέλετε να μοιραστείτε components μεταξύ πολλών έργων, είναι σκόπιμο να τα διαχωρίσετε σε ένα ξεχωριστό composer πακέτο. - -Στον κατάλογο `app/Mail` μπορείτε να τοποθετήσετε τη διαχείριση της επικοινωνίας μέσω e-mail: - -/--pre -app/Mail/ -├── templates/ ← e-mail templates -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Αντιστοίχιση Presenters -======================= - -Η αντιστοίχιση (mapping) ορίζει κανόνες για την εξαγωγή του ονόματος της κλάσης από το όνομα του presenter. Τους καθορίζουμε στη [διαμόρφωση|configuration] κάτω από το κλειδί `application › mapping`. - -Σε αυτή τη σελίδα, δείξαμε ότι τοποθετούμε τους presenters στον φάκελο `app/Presentation` (ή `app/UI`). Πρέπει να ενημερώσουμε το Nette για αυτή τη σύμβαση στο αρχείο διαμόρφωσης. Μια γραμμή αρκεί: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Πώς λειτουργεί η αντιστοίχιση; Για καλύτερη κατανόηση, ας φανταστούμε πρώτα μια εφαρμογή χωρίς modules. Θέλουμε οι κλάσεις των presenters να ανήκουν στον χώρο ονομάτων `App\Presentation`, ώστε ο presenter `Home` να αντιστοιχεί στην κλάση `App\Presentation\HomePresenter`. Αυτό το επιτυγχάνουμε με αυτή τη διαμόρφωση: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Η αντιστοίχιση λειτουργεί έτσι ώστε το όνομα του presenter `Home` να αντικαθιστά τον αστερίσκο στη μάσκα `App\Presentation\*Presenter`, δίνοντας το τελικό όνομα κλάσης `App\Presentation\HomePresenter`. Απλό! - -Ωστόσο, όπως βλέπετε στα παραδείγματα σε αυτό και σε άλλα κεφάλαια, τοποθετούμε τις κλάσεις των presenters σε ομώνυμους υποκαταλόγους, για παράδειγμα, ο presenter `Home` αντιστοιχεί στην κλάση `App\Presentation\Home\HomePresenter`. Αυτό το επιτυγχάνουμε διπλασιάζοντας την άνω και κάτω τελεία (απαιτεί Nette Application 3.2): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Τώρα προχωράμε στην αντιστοίχιση presenters σε modules. Για κάθε module, μπορούμε να ορίσουμε μια συγκεκριμένη αντιστοίχιση: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -Σύμφωνα με αυτή τη διαμόρφωση, ο presenter `Front:Home` αντιστοιχεί στην κλάση `App\Presentation\Front\Home\HomePresenter`, ενώ ο presenter `Api:OAuth` στην κλάση `App\Api\OAuthPresenter`. - -Επειδή τα modules `Front` και `Admin` έχουν παρόμοιο τρόπο αντιστοίχισης και πιθανότατα θα υπάρχουν περισσότερα τέτοια modules, είναι δυνατό να δημιουργηθεί ένας γενικός κανόνας που τα αντικαθιστά. Έτσι, στη μάσκα της κλάσης προστίθεται ένας νέος αστερίσκος για το module: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Λειτουργεί επίσης για βαθύτερα ένθετες δομές καταλόγων, όπως για παράδειγμα ο presenter `Admin:User:Edit`, με το τμήμα με τον αστερίσκο να επαναλαμβάνεται για κάθε επίπεδο και το αποτέλεσμα να είναι η κλάση `App\Presentation\Admin\User\Edit\EditPresenter`. - -Μια εναλλακτική σύνταξη είναι να χρησιμοποιήσετε έναν πίνακα που αποτελείται από τρία τμήματα αντί για μια συμβολοσειρά. Αυτή η σύνταξη είναι ισοδύναμη με την προηγούμενη: - -```neon -application: - mapping: - *: [App\Presentation, *, **Presenter] - Api: [App\Api, '', *Presenter] -``` diff --git a/application/el/how-it-works.texy b/application/el/how-it-works.texy deleted file mode 100644 index 30acf528ad..0000000000 --- a/application/el/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Πώς λειτουργούν οι εφαρμογές; -***************************** - -
    - -Διαβάζετε το βασικό έγγραφο της τεκμηρίωσης του Nette. Θα μάθετε ολόκληρη την αρχή λειτουργίας των διαδικτυακών εφαρμογών. Από το Α έως το Ω, από τη στιγμή της γέννησης μέχρι την τελευταία πνοή του σεναρίου PHP. Αφού το διαβάσετε, θα γνωρίζετε: - -- πώς λειτουργεί όλο αυτό -- τι είναι το Bootstrap, ο Presenter και το DI container -- πώς μοιάζει η δομή καταλόγων - -
    - - -Δομή καταλόγου -============== - -Ανοίξτε το παράδειγμα του σκελετού της διαδικτυακής εφαρμογής που ονομάζεται [WebProject|https://github.com/nette/web-project] και κατά την ανάγνωση μπορείτε να δείτε τα αρχεία για τα οποία γίνεται λόγος. - -Η δομή καταλόγων μοιάζει κάπως έτσι: - -/--pre -web-project/ -├── app/ ← κατάλογος με την εφαρμογή -│ ├── Core/ ← βασικές κλάσεις απαραίτητες για τη λειτουργία -│ │ └── RouterFactory.php ← διαμόρφωση διευθύνσεων URL -│ ├── Presentation/ ← presenters, πρότυπα & λοιπά -│ │ ├── @layout.latte ← πρότυπο διάταξης -│ │ └── Home/ ← κατάλογος του presenter Home -│ │ ├── HomePresenter.php ← κλάση του presenter Home -│ │ └── default.latte ← πρότυπο της ενέργειας default -│ └── Bootstrap.php ← κλάση εκκίνησης Bootstrap -├── assets/ ← πόροι (SCSS, TypeScript, εικόνες πηγής) -├── bin/ ← σενάρια που εκτελούνται από τη γραμμή εντολών -├── config/ ← αρχεία διαμόρφωσης -│ ├── common.neon -│ └── services.neon -├── log/ ← καταγεγραμμένα σφάλματα -├── temp/ ← προσωρινά αρχεία, cache, … -├── vendor/ ← βιβλιοθήκες εγκατεστημένες από τον Composer -│ ├── ... -│ └── autoload.php ← αυτόματη φόρτωση όλων των εγκατεστημένων πακέτων -├── www/ ← δημόσιος κατάλογος ή document-root του έργου -│ ├── assets/ ← μεταγλωττισμένα στατικά αρχεία (CSS, JS, εικόνες, ...) -│ ├── .htaccess ← κανόνες mod_rewrite -│ └── index.php ← αρχικό αρχείο με το οποίο εκκινεί η εφαρμογή -└── .htaccess ← απαγορεύει την πρόσβαση σε όλους τους καταλόγους εκτός του www -\-- - -Μπορείτε να αλλάξετε τη δομή καταλόγων όπως θέλετε, να μετονομάσετε ή να μετακινήσετε φακέλους, είναι εντελώς ευέλικτη. Το Nette διαθέτει επίσης έξυπνη αυτόματη ανίχνευση και αναγνωρίζει αυτόματα τη θέση της εφαρμογής, συμπεριλαμβανομένης της βασικής της διεύθυνσης URL. - -Για λίγο μεγαλύτερες εφαρμογές, μπορούμε να [χωρίσουμε τους φακέλους με τους presenters και τα πρότυπα σε υποκαταλόγους |directory-structure#Presenters και Πρότυπα] και τις κλάσεις σε χώρους ονομάτων, τους οποίους ονομάζουμε modules. - -Ο κατάλογος `www/` αντιπροσωπεύει τον λεγόμενο δημόσιο κατάλογο ή document-root του έργου. Μπορείτε να τον μετονομάσετε χωρίς να χρειάζεται να ρυθμίσετε τίποτα άλλο στην πλευρά της εφαρμογής. Απλά πρέπει να [διαμορφώσετε το hosting |nette:troubleshooting#Πώς να αλλάξετε ή να αφαιρέσετε τον κατάλογο www από το URL] έτσι ώστε το document-root να δείχνει σε αυτόν τον κατάλογο. - -Μπορείτε επίσης να κατεβάσετε απευθείας το WebProject συμπεριλαμβανομένου του Nette χρησιμοποιώντας τον [Composer |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -Σε Linux ή macOS, ορίστε δικαιώματα εγγραφής για τους καταλόγους `log/` και `temp/` [δικαιώματα εγγραφής |nette:troubleshooting#Ρύθμιση δικαιωμάτων καταλόγου]. - -Η εφαρμογή WebProject είναι έτοιμη για εκκίνηση, δεν χρειάζεται να διαμορφώσετε απολύτως τίποτα και μπορείτε να την εμφανίσετε απευθείας στο πρόγραμμα περιήγησης μεταβαίνοντας στον φάκελο `www/`. - - -Αίτημα HTTP -=========== - -Όλα ξεκινούν τη στιγμή που ο χρήστης ανοίγει μια σελίδα στο πρόγραμμα περιήγησης. Δηλαδή, όταν το πρόγραμμα περιήγησης χτυπάει την πόρτα του διακομιστή με ένα αίτημα HTTP. Το αίτημα κατευθύνεται σε ένα μόνο αρχείο PHP, το οποίο βρίσκεται στον δημόσιο κατάλογο `www/`, και αυτό είναι το `index.php`. Ας υποθέσουμε ότι πρόκειται για ένα αίτημα στη διεύθυνση `https://example.com/product/123`. Χάρη στην κατάλληλη [ρύθμιση του διακομιστή |nette:troubleshooting#Πώς να ρυθμίσετε τον διακομιστή για όμορφα URLs], ακόμη και αυτό το URL αντιστοιχίζεται στο αρχείο `index.php` και αυτό εκτελείται. - -Ο ρόλος του είναι: - -1) να αρχικοποιήσει το περιβάλλον -2) να αποκτήσει το factory -3) να εκκινήσει την εφαρμογή Nette, η οποία θα διεκπεραιώσει το αίτημα - -Ποιο factory; Δεν κατασκευάζουμε τρακτέρ, αλλά ιστοσελίδες! Υπομονή, θα εξηγηθεί αμέσως. - -Με τις λέξεις «αρχικοποίηση περιβάλλοντος» εννοούμε, για παράδειγμα, ότι ενεργοποιείται το [Tracy|tracy:], το οποίο είναι ένα καταπληκτικό εργαλείο για την καταγραφή ή την οπτικοποίηση σφαλμάτων. Στον διακομιστή παραγωγής καταγράφει τα σφάλματα, στον διακομιστή ανάπτυξης τα εμφανίζει απευθείας. Επομένως, η αρχικοποίηση περιλαμβάνει επίσης την απόφαση εάν ο ιστότοπος εκτελείται σε λειτουργία παραγωγής ή ανάπτυξης. Για αυτό, το Nette χρησιμοποιεί [έξυπνη αυτόματη ανίχνευση |bootstrapping#Λειτουργία Ανάπτυξης vs Παραγωγής]: εάν εκτελείτε τον ιστότοπο στο localhost, εκτελείται σε λειτουργία ανάπτυξης. Έτσι, δεν χρειάζεται να διαμορφώσετε τίποτα και η εφαρμογή είναι αμέσως έτοιμη τόσο για ανάπτυξη όσο και για παραγωγική λειτουργία. Αυτά τα βήματα εκτελούνται και περιγράφονται λεπτομερώς στο κεφάλαιο για την [κλάση Bootstrap|bootstrapping]. - -Το τρίτο σημείο (ναι, παραλείψαμε το δεύτερο, αλλά θα επιστρέψουμε σε αυτό) είναι η εκκίνηση της εφαρμογής. Η διεκπεραίωση των αιτημάτων HTTP στο Nette γίνεται από την κλάση `Nette\Application\Application` (στο εξής `Application`), οπότε όταν λέμε εκκίνηση της εφαρμογής, εννοούμε συγκεκριμένα την κλήση της μεθόδου με το εύστοχο όνομα `run()` στο αντικείμενο αυτής της κλάσης. - -Το Nette είναι ένας μέντορας που σας καθοδηγεί στη συγγραφή καθαρών εφαρμογών σύμφωνα με δοκιμασμένες μεθοδολογίες. Και μία από τις πιο δοκιμασμένες ονομάζεται **dependency injection**, συντομογραφικά DI. Αυτή τη στιγμή, δεν θέλουμε να σας επιβαρύνουμε με την εξήγηση του DI, γι' αυτό υπάρχει ένα [ξεχωριστό κεφάλαιο|dependency-injection:introduction], το σημαντικό αποτέλεσμα είναι ότι τα βασικά αντικείμενα συνήθως δημιουργούνται από ένα factory αντικειμένων, το οποίο ονομάζεται **DI container** (συντομογραφικά DIC). Ναι, αυτό είναι το factory για το οποίο μιλήσαμε πριν λίγο. Και θα μας δημιουργήσει επίσης το αντικείμενο `Application`, γι' αυτό χρειαζόμαστε πρώτα το container. Το αποκτούμε χρησιμοποιώντας την κλάση `Configurator` και το αφήνουμε να δημιουργήσει το αντικείμενο `Application`, καλούμε τη μέθοδο `run()` σε αυτό και έτσι εκκινεί η εφαρμογή Nette. Ακριβώς αυτό συμβαίνει στο αρχείο [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -Η κλάση Application έχει έναν μόνο ρόλο: να απαντήσει στο αίτημα HTTP. - -Οι εφαρμογές που γράφονται στο Nette χωρίζονται σε πολλούς λεγόμενους presenters (σε άλλα frameworks μπορεί να συναντήσετε τον όρο controller, πρόκειται για το ίδιο πράγμα), οι οποίοι είναι κλάσεις, καθεμία από τις οποίες αντιπροσωπεύει μια συγκεκριμένη σελίδα του ιστότοπου: π.χ. την αρχική σελίδα, ένα προϊόν σε ένα e-shop, μια φόρμα σύνδεσης, ένα sitemap feed κ.λπ. Μια εφαρμογή μπορεί να έχει από έναν έως χιλιάδες presenters. - -Η Application ξεκινά ζητώντας από τον λεγόμενο router να αποφασίσει σε ποιον από τους presenters θα παραδώσει το τρέχον αίτημα για διεκπεραίωση. Ο router αποφασίζει ποιος έχει την ευθύνη. Εξετάζει το εισερχόμενο URL `https://example.com/product/123` και με βάση το πώς είναι ρυθμισμένος, αποφασίζει ότι αυτή είναι δουλειά, για παράδειγμα, για τον **presenter** `Product`, από τον οποίο θα ζητήσει ως **action** την εμφάνιση (`show`) του προϊόντος με `id: 123`. Το ζεύγος presenter + action συνηθίζεται να γράφεται χωρισμένο με άνω και κάτω τελεία ως `Product:show`. - -Έτσι, ο router μετέτρεψε το URL στο ζεύγος `Presenter:action` + παραμέτρους, στην περίπτωσή μας `Product:show` + `id: 123`. Πώς μοιάζει ένας τέτοιος router μπορείτε να δείτε στο αρχείο `app/Core/RouterFactory.php` και τον περιγράφουμε λεπτομερώς στο κεφάλαιο [Routing |Routing]. - -Ας προχωρήσουμε. Η Application γνωρίζει ήδη το όνομα του presenter και μπορεί να συνεχίσει. Δημιουργώντας το αντικείμενο της κλάσης `ProductPresenter`, που είναι ο κώδικας του presenter `Product`. Πιο συγκεκριμένα, ζητά από το DI container να δημιουργήσει τον presenter, επειδή η δημιουργία είναι δική του δουλειά. - -Ο presenter μπορεί να μοιάζει κάπως έτσι: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // λήψη δεδομένων από το μοντέλο και μεταβίβασή τους στο πρότυπο - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Η διεκπεραίωση του αιτήματος αναλαμβάνεται από τον presenter. Και ο στόχος είναι σαφής: εκτέλεσε την action `show` με `id: 123`. Αυτό, στη γλώσσα των presenters, σημαίνει ότι καλείται η μέθοδος `renderShow()` και στην παράμετρο `$id` λαμβάνει το `123`. - -Ο presenter μπορεί να εξυπηρετεί πολλαπλές actions, δηλαδή να έχει πολλαπλές μεθόδους `render()`. Αλλά συνιστούμε να σχεδιάζετε presenters με μία ή όσο το δυνατόν λιγότερες actions. - -Έτσι, κλήθηκε η μέθοδος `renderShow(123)`, ο κώδικας της οποίας είναι μεν ένα φανταστικό παράδειγμα, αλλά μπορείτε να δείτε σε αυτό πώς μεταβιβάζονται δεδομένα στο πρότυπο, δηλαδή γράφοντας στο `$this->template`. - -Στη συνέχεια, ο presenter επιστρέφει μια response. Αυτή μπορεί να είναι μια σελίδα HTML, μια εικόνα, ένα έγγραφο XML, η αποστολή ενός αρχείου από τον δίσκο, JSON ή ίσως μια ανακατεύθυνση σε άλλη σελίδα. Είναι σημαντικό ότι αν δεν πούμε ρητά πώς πρέπει να απαντήσει (που είναι η περίπτωση του `ProductPresenter`), η response θα είναι η απόδοση ενός προτύπου με μια σελίδα HTML. Γιατί; Επειδή στο 99% των περιπτώσεων θέλουμε να αποδώσουμε ένα πρότυπο, επομένως ο presenter θεωρεί αυτή τη συμπεριφορά ως προεπιλεγμένη και θέλει να μας διευκολύνει τη δουλειά. Αυτός είναι ο σκοπός του Nette. - -Δεν χρειάζεται καν να καθορίσουμε ποιο πρότυπο να αποδοθεί, θα βρει τη διαδρομή προς αυτό μόνος του. Στην περίπτωση της action `show`, απλά θα προσπαθήσει να φορτώσει το πρότυπο `show.latte` στον κατάλογο με την κλάση `ProductPresenter`. Επίσης, θα προσπαθήσει να βρει τη διάταξη στο αρχείο `@layout.latte` (περισσότερα για την [αναζήτηση προτύπων |templates#Αναζήτηση προτύπου]). - -Και στη συνέχεια αποδίδει τα πρότυπα. Με αυτό, ο στόχος του presenter και ολόκληρης της εφαρμογής ολοκληρώνεται και το έργο τελειώνει. Αν το πρότυπο δεν υπήρχε, θα επιστρεφόταν μια σελίδα με σφάλμα 404. Περισσότερα για τους presenters μπορείτε να διαβάσετε στη σελίδα [Presenters|presenters]. - -[* request-flow.svg *] - -Για σιγουριά, ας προσπαθήσουμε να ανακεφαλαιώσουμε ολόκληρη τη διαδικασία με ένα ελαφρώς διαφορετικό URL: - -1) Το URL θα είναι `https://example.com` -2) εκκινούμε την εφαρμογή, δημιουργείται το container και εκτελείται το `Application::run()` -3) ο router αποκωδικοποιεί το URL ως το ζεύγος `Home:default` -4) δημιουργείται το αντικείμενο της κλάσης `HomePresenter` -5) καλείται η μέθοδος `renderDefault()` (αν υπάρχει) -6) αποδίδεται το πρότυπο π.χ. `default.latte` με τη διάταξη π.χ. `@layout.latte` - - -Μπορεί να έχετε συναντήσει τώρα πολλούς νέους όρους, αλλά πιστεύουμε ότι βγάζουν νόημα. Η δημιουργία εφαρμογών στο Nette είναι εξαιρετικά εύκολη. - - -Πρότυπα -======= - -Αφού αναφερθήκαμε στα πρότυπα, στο Nette χρησιμοποιείται το σύστημα προτύπων [Latte |latte:]. Γι' αυτό και οι καταλήξεις `.latte` στα πρότυπα. Το Latte χρησιμοποιείται αφενός επειδή είναι το πιο ασφαλές σύστημα προτύπων για PHP, και αφετέρου το πιο διαισθητικό σύστημα. Δεν χρειάζεται να μάθετε πολλά νέα πράγματα, αρκεί η γνώση της PHP και μερικών ετικετών. Όλα θα τα μάθετε [στην τεκμηρίωση |templates]. - -Στο πρότυπο, [δημιουργούνται σύνδεσμοι |creating-links] προς άλλους presenters & actions ως εξής: - -```latte -λεπτομέρεια προϊόντος -``` - -Απλά αντί για το πραγματικό URL, γράφετε το γνωστό ζεύγος `Presenter:action` και καθορίζετε τυχόν παραμέτρους. Το κόλπο είναι στο `n:href`, το οποίο λέει ότι αυτό το attribute θα επεξεργαστεί το Nette. Και θα δημιουργήσει: - -```latte -λεπτομέρεια προϊόντος -``` - -Η δημιουργία των URL γίνεται από τον προαναφερθέντα router. Συγκεκριμένα, οι routers στο Nette είναι εξαιρετικοί στο ότι μπορούν να εκτελούν όχι μόνο μετασχηματισμούς από URL σε ζεύγος presenter:action, αλλά και αντίστροφα, δηλαδή από το όνομα του presenter + action + παραμέτρους να δημιουργούν ένα URL. Χάρη σε αυτό, στο Nette μπορείτε να αλλάξετε εντελώς τις μορφές των URL σε ολόκληρη την ολοκληρωμένη εφαρμογή, χωρίς να αλλάξετε ούτε έναν χαρακτήρα στο πρότυπο ή τον presenter. Απλά τροποποιώντας τον router. Επίσης, χάρη σε αυτό λειτουργεί η λεγόμενη κανονικοποίηση, η οποία είναι ένα άλλο μοναδικό χαρακτηριστικό του Nette που συμβάλλει στο καλύτερο SEO (βελτιστοποίηση για μηχανές αναζήτησης) αποτρέποντας αυτόματα την ύπαρξη διπλού περιεχομένου σε διαφορετικά URL. Πολλοί προγραμματιστές το θεωρούν εντυπωσιακό. - - -Διαδραστικά Components -====================== - -Για τους presenters πρέπει να σας αποκαλύψουμε ακόμα ένα πράγμα: έχουν ενσωματωμένο σύστημα components. Κάτι παρόμοιο μπορεί να θυμούνται οι παλαιότεροι από τα Delphi ή τα ASP.NET Web Forms, ενώ κάτι παρόμοιο αποτελεί τη βάση του React ή του Vue.js. Στον κόσμο των PHP frameworks, πρόκειται για ένα εντελώς μοναδικό χαρακτηριστικό. - -Τα components είναι ανεξάρτητες, επαναχρησιμοποιήσιμες μονάδες που ενσωματώνουμε σε σελίδες (δηλαδή presenters). Μπορεί να είναι [φόρμες |forms:in-presenter], [datagrids |https://componette.org/contributte/datagrid/], μενού, δημοσκοπήσεις, στην πραγματικότητα οτιδήποτε έχει νόημα να χρησιμοποιείται επανειλημμένα. Μπορούμε να δημιουργήσουμε δικά μας components ή να χρησιμοποιήσουμε κάποια από την [τεράστια προσφορά |https://componette.org] open source components. - -Τα components επηρεάζουν θεμελιωδώς την προσέγγιση στην ανάπτυξη εφαρμογών. Θα σας ανοίξουν νέες δυνατότητες σύνθεσης σελίδων από προκατασκευασμένες μονάδες. Και επιπλέον, έχουν κάτι κοινό με το [Hollywood |components#Hollywood Style]. - - -DI container και Διαμόρφωση -=========================== - -Το DI container ή factory αντικειμένων είναι η καρδιά ολόκληρης της εφαρμογής. - -Μην ανησυχείτε, δεν είναι κάποιο μαγικό μαύρο κουτί, όπως ίσως φάνηκε από τις προηγούμενες γραμμές. Στην πραγματικότητα, είναι μια αρκετά βαρετή κλάση PHP, την οποία δημιουργεί το Nette και την αποθηκεύει στον κατάλογο cache. Έχει πολλές μεθόδους με ονόματα όπως `createServiceAbcd()` και καθεμία από αυτές μπορεί να δημιουργήσει και να επιστρέψει κάποιο αντικείμενο. Ναι, υπάρχει και η μέθοδος `createServiceApplication()`, η οποία δημιουργεί το `Nette\Application\Application`, το οποίο χρειαζόμασταν στο αρχείο `index.php` για την εκκίνηση της εφαρμογής. Και υπάρχουν μέθοδοι που δημιουργούν τους επιμέρους presenters. Και ούτω καθεξής. - -Τα αντικείμενα που δημιουργεί το DI container ονομάζονται για κάποιο λόγο services. - -Αυτό που είναι πραγματικά ιδιαίτερο σε αυτή την κλάση είναι ότι δεν την προγραμματίζετε εσείς, αλλά το framework. Αυτό πράγματι δημιουργεί τον κώδικα PHP και τον αποθηκεύει στον δίσκο. Εσείς απλά δίνετε οδηγίες για το ποια αντικείμενα πρέπει να μπορεί να δημιουργεί το container και πώς ακριβώς. Και αυτές οι οδηγίες είναι γραμμένες στα [αρχεία διαμόρφωσης |bootstrapping#Διαμόρφωση του DI Container], για τα οποία χρησιμοποιείται η μορφή [NEON|neon:format] και επομένως έχουν και την επέκταση `.neon`. - -Τα αρχεία διαμόρφωσης χρησιμεύουν καθαρά για την καθοδήγηση του DI container. Έτσι, όταν για παράδειγμα αναφέρω στην ενότητα [session |http:configuration#Session] την επιλογή `expiration: 14 days`, τότε το DI container κατά τη δημιουργία του αντικειμένου `Nette\Http\Session` που αντιπροσωπεύει τη session, καλεί τη μέθοδό του `setExpiration('14 days')` και έτσι η διαμόρφωση γίνεται πραγματικότητα. - -Υπάρχει ένα ολόκληρο κεφάλαιο έτοιμο για εσάς που περιγράφει τι μπορείτε να [διαμορφώσετε |nette:configuring] και πώς να [ορίσετε τις δικές σας services |dependency-injection:services]. - -Μόλις εμβαθύνετε λίγο στη δημιουργία services, θα συναντήσετε τη λέξη [autowiring |dependency-injection:autowiring]. Αυτό είναι ένα χαρακτηριστικό που θα απλοποιήσει απίστευτα τη ζωή σας. Μπορεί να μεταβιβάσει αυτόματα αντικείμενα εκεί που τα χρειάζεστε (για παράδειγμα, στους κατασκευαστές των κλάσεών σας), χωρίς να χρειάζεται να κάνετε τίποτα. Θα διαπιστώσετε ότι το DI container στο Nette είναι ένα μικρό θαύμα. - - -Πού να πάτε μετά; -================= - -Έχουμε καλύψει τις βασικές αρχές των εφαρμογών στο Nette. Μέχρι στιγμής πολύ επιφανειακά, αλλά σύντομα θα εμβαθύνετε και με τον καιρό θα δημιουργήσετε υπέροχες διαδικτυακές εφαρμογές. Πού να συνεχίσετε; Έχετε δοκιμάσει ήδη το tutorial [Γράφοντας την πρώτη εφαρμογή|quickstart:]? - -Εκτός από τα παραπάνω, το Nette διαθέτει ένα ολόκληρο οπλοστάσιο [χρήσιμων κλάσεων|utils:], [επίπεδο βάσης δεδομένων|database:], κ.λπ. Δοκιμάστε απλά να περιηγηθείτε στην τεκμηρίωση. Ή στο [blog|https://blog.nette.org]. Θα ανακαλύψετε πολλά ενδιαφέροντα πράγματα. - -Ας σας φέρει το framework πολλή χαρά 💙 diff --git a/application/el/multiplier.texy b/application/el/multiplier.texy deleted file mode 100644 index daec1e64bb..0000000000 --- a/application/el/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Πολλαπλασιαστής: Δυναμικά Components -************************************ - -.[perex] -Εργαλείο για δυναμική δημιουργία διαδραστικών components - -Ας ξεκινήσουμε από ένα τυπικό παράδειγμα: έχουμε μια λίστα προϊόντων σε ένα e-shop, και για καθένα θέλουμε να εμφανίσουμε μια φόρμα για την προσθήκη του προϊόντος στο καλάθι. Μια πιθανή παραλλαγή είναι να περικλείσουμε ολόκληρη τη λίστα σε μια ενιαία φόρμα. Ωστόσο, ένας πολύ πιο βολικός τρόπος μας προσφέρεται από το [api:Nette\Application\UI\Multiplier]. - -Ο Multiplier επιτρέπει τον βολικό ορισμό ενός μικρού factory για πολλαπλά components. Λειτουργεί με την αρχή των ένθετων components - κάθε component που κληρονομεί από το [api:Nette\ComponentModel\Container] μπορεί να περιέχει άλλα components. - -.[tip] -Δείτε το κεφάλαιο για το [μοντέλο component |components#Components σε Βάθος] στην τεκμηρίωση ή την [παρουσίαση του Honza Tvrdík|https://www.youtube.com/watch?v=8y3LLexWu-I]. - -Η ουσία του Multiplier είναι ότι λειτουργεί ως γονέας που μπορεί να δημιουργήσει δυναμικά τα παιδιά του χρησιμοποιώντας ένα callback που περνιέται στον κατασκευαστή. Δείτε το παράδειγμα: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Πλήθος ειδών:') - ->setRequired(); - $form->addSubmit('send', 'Προσθήκη στο καλάθι'); - return $form; - }); -} -``` - -Τώρα μπορούμε απλά στο πρότυπο να αφήσουμε να αποδοθεί η φόρμα για κάθε προϊόν - και καθένα θα είναι πραγματικά ένα μοναδικό component. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -Το όρισμα που περνιέται στην ετικέτα `{control}` είναι σε μορφή που λέει: - -1. πάρε το component `shopForm` -2. και από αυτό πάρε τον απόγονο `$item->id` - -Κατά την πρώτη κλήση του σημείου **1.** το `shopForm` δεν υπάρχει ακόμα, οπότε καλείται το factory του `createComponentShopForm`. Στο ληφθέν component (παρουσία του Multiplier) καλείται στη συνέχεια το factory της συγκεκριμένης φόρμας - που είναι η ανώνυμη συνάρτηση που περάσαμε στον Multiplier στον κατασκευαστή. - -Στην επόμενη επανάληψη του foreach, η μέθοδος `createComponentShopForm` δεν θα κληθεί πλέον (το component υπάρχει), αλλά επειδή ψάχνουμε για έναν άλλο απόγονό του (`$item->id` θα είναι διαφορετικό σε κάθε επανάληψη), η ανώνυμη συνάρτηση θα κληθεί ξανά και θα μας επιστρέψει μια νέα φόρμα. - -Το μόνο που μένει είναι να διασφαλίσουμε ότι η φόρμα προσθέτει στο καλάθι πραγματικά το προϊόν που πρέπει - αυτή τη στιγμή η φόρμα είναι εντελώς ίδια για κάθε προϊόν. Η ιδιότητα του Multiplier (και γενικά κάθε factory component στο Nette Framework) θα μας βοηθήσει, και αυτή είναι ότι κάθε factory λαμβάνει ως πρώτο του όρισμα το όνομα του component που δημιουργείται. Στην περίπτωσή μας, αυτό θα είναι το `$item->id`, που είναι ακριβώς η πληροφορία που χρειαζόμαστε. Αρκεί λοιπόν να τροποποιήσουμε ελαφρώς τη δημιουργία της φόρμας: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function ($itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Πλήθος ειδών:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Προσθήκη στο καλάθι'); - return $form; - }); -} -``` diff --git a/application/el/presenters.texy b/application/el/presenters.texy deleted file mode 100644 index a8d6a6f9a8..0000000000 --- a/application/el/presenters.texy +++ /dev/null @@ -1,500 +0,0 @@ -Presenters -********** - -
    - -Θα εξοικειωθούμε με τον τρόπο συγγραφής presenters και προτύπων στο Nette. Μετά την ανάγνωση, θα γνωρίζετε: - -- πώς λειτουργεί ένας presenter -- τι είναι οι persistent παράμετροι -- πώς αποδίδονται τα πρότυπα - -
    - -[Γνωρίζουμε ήδη |how-it-works#Nette Application] ότι ένας presenter είναι μια κλάση που αντιπροσωπεύει μια συγκεκριμένη σελίδα μιας διαδικτυακής εφαρμογής, π.χ. την αρχική σελίδα, ένα προϊόν σε ένα e-shop, μια φόρμα σύνδεσης, ένα sitemap feed κ.λπ. Μια εφαρμογή μπορεί να έχει από έναν έως χιλιάδες presenters. Σε άλλα frameworks, ονομάζονται επίσης controllers. - -Συνήθως, με τον όρο presenter εννοούμε έναν απόγονο της κλάσης [api:Nette\Application\UI\Presenter], ο οποίος είναι κατάλληλος για τη δημιουργία διαδικτυακών διεπαφών και στον οποίο θα επικεντρωθούμε στο υπόλοιπο αυτού του κεφαλαίου. Με γενική έννοια, ένας presenter είναι οποιοδήποτε αντικείμενο που υλοποιεί το interface [api:Nette\Application\IPresenter]. - - -Κύκλος ζωής του presenter -========================= - -Ο ρόλος του presenter είναι να διεκπεραιώσει ένα αίτημα και να επιστρέψει μια response (η οποία μπορεί να είναι μια σελίδα HTML, μια εικόνα, μια ανακατεύθυνση κ.λπ.). - -Έτσι, στην αρχή, του παραδίδεται ένα αίτημα. Δεν είναι απευθείας ένα αίτημα HTTP, αλλά ένα αντικείμενο [api:Nette\Application\Request], στο οποίο το αίτημα HTTP μετασχηματίστηκε με τη βοήθεια του router. Συνήθως δεν ερχόμαστε σε επαφή με αυτό το αντικείμενο, καθώς ο presenter αναθέτει έξυπνα την επεξεργασία του αιτήματος σε άλλες μεθόδους, τις οποίες θα δείξουμε τώρα. - -[* lifecycle.svg *] *** *Κύκλος ζωής του presenter* .<> - -Η εικόνα παρουσιάζει μια λίστα μεθόδων που καλούνται διαδοχικά από πάνω προς τα κάτω, αν υπάρχουν. Καμία από αυτές δεν χρειάζεται να υπάρχει, μπορούμε να έχουμε έναν εντελώς κενό presenter χωρίς ούτε μία μέθοδο και να χτίσουμε πάνω του έναν απλό στατικό ιστότοπο. - - -`__construct()` ---------------- - -Ο κατασκευαστής δεν ανήκει ακριβώς στον κύκλο ζωής του presenter, επειδή καλείται τη στιγμή της δημιουργίας του αντικειμένου. Αλλά τον αναφέρουμε λόγω της σημασίας του. Ο κατασκευαστής (μαζί με τη [μέθοδο inject|best-practices:inject-method-attribute]) χρησιμεύει για τη μεταβίβαση εξαρτήσεων. - -Ο presenter δεν θα πρέπει να χειρίζεται την επιχειρηματική λογική της εφαρμογής, να γράφει και να διαβάζει από τη βάση δεδομένων, να εκτελεί υπολογισμούς κ.λπ. Γι' αυτό υπάρχουν κλάσεις από το επίπεδο που ονομάζουμε model. Για παράδειγμα, η κλάση `ArticleRepository` μπορεί να είναι υπεύθυνη για τη φόρτωση και την αποθήκευση άρθρων. Για να μπορεί ο presenter να συνεργαστεί μαζί της, ζητά να του [περαστεί μέσω dependency injection |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Αμέσως μετά τη λήψη του αιτήματος, καλείται η μέθοδος `startup()`. Μπορείτε να τη χρησιμοποιήσετε για την αρχικοποίηση ιδιοτήτων, την επαλήθευση δικαιωμάτων χρήστη κ.λπ. Απαιτείται η μέθοδος να καλεί πάντα τον πρόγονο `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -Αντίστοιχο της μεθόδου `render()`. Ενώ η `render()` προορίζεται για την προετοιμασία δεδομένων για ένα συγκεκριμένο πρότυπο που θα αποδοθεί στη συνέχεια, στην `action()` επεξεργάζεται το αίτημα χωρίς σύνδεση με την απόδοση του προτύπου. Για παράδειγμα, επεξεργάζονται δεδομένα, συνδέεται ή αποσυνδέεται ο χρήστης, και ούτω καθεξής, και στη συνέχεια [ανακατευθύνεται αλλού |#Ανακατεύθυνση]. - -Είναι σημαντικό ότι η `action()` καλείται νωρίτερα από την `render()`, οπότε σε αυτήν μπορούμε ενδεχομένως να αλλάξουμε την περαιτέρω πορεία των γεγονότων, δηλαδή να αλλάξουμε το πρότυπο που θα αποδοθεί, καθώς και τη μέθοδο `render()` που θα κληθεί. Και αυτό γίνεται χρησιμοποιώντας το `setView('jineView')`. - -Στη μέθοδο μεταβιβάζονται παράμετροι από το αίτημα. Είναι δυνατό και συνιστάται να καθορίσετε τύπους για τις παραμέτρους, π.χ. `actionShow(int $id, ?string $slug = null)` - αν η παράμετρος `id` λείπει ή αν δεν είναι integer, ο presenter θα επιστρέψει [σφάλμα 404 |#Σφάλμα 404 κ.λπ] και θα τερματίσει τη λειτουργία του. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -Η μέθοδος επεξεργάζεται τα λεγόμενα signals, με τα οποία θα εξοικειωθούμε στο κεφάλαιο που είναι αφιερωμένο στα [components |components#Σήμα]. Προορίζεται κυρίως για components και την επεξεργασία αιτήσεων AJAX. - -Στη μέθοδο μεταβιβάζονται παράμετροι από το αίτημα, όπως στην περίπτωση της `action()`, συμπεριλαμβανομένου του ελέγχου τύπου. - - -`beforeRender()` ----------------- - -Η μέθοδος `beforeRender`, όπως υποδηλώνει και το όνομά της, καλείται πριν από κάθε μέθοδο `render()`. Χρησιμοποιείται για την κοινή διαμόρφωση του προτύπου, τη μεταβίβαση μεταβλητών για τη διάταξη και παρόμοια. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -Το μέρος όπου προετοιμάζουμε το πρότυπο για την επακόλουθη απόδοση, του μεταβιβάζουμε δεδομένα κ.λπ. - -Στη μέθοδο μεταβιβάζονται παράμετροι από το αίτημα, όπως στην περίπτωση της `action()`, συμπεριλαμβανομένου του ελέγχου τύπου. - -```php -public function renderShow(int $id): void -{ - // λήψη δεδομένων από το μοντέλο και μεταβίβασή τους στο πρότυπο - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -Η μέθοδος `afterRender`, όπως υποδηλώνει ξανά το όνομα, καλείται μετά από κάθε μέθοδο `render()`. Χρησιμοποιείται μάλλον σπάνια. - - -`shutdown()` ------------- - -Καλείται στο τέλος του κύκλου ζωής του presenter. - - -**Καλή συμβουλή, πριν προχωρήσουμε**. Ο presenter, όπως φαίνεται, μπορεί να εξυπηρετεί πολλαπλές actions/views, δηλαδή να έχει πολλαπλές μεθόδους `render()`. Αλλά συνιστούμε να σχεδιάζετε presenters με μία ή όσο το δυνατόν λιγότερες actions. - - -Αποστολή απάντησης -================== - -Η response του presenter είναι συνήθως η [απόδοση ενός προτύπου με μια σελίδα HTML|templates], αλλά μπορεί επίσης να είναι η αποστολή ενός αρχείου, JSON ή ίσως μια ανακατεύθυνση σε άλλη σελίδα. - -Οποιαδήποτε στιγμή κατά τη διάρκεια του κύκλου ζωής, μπορούμε να στείλουμε μια response με μία από τις παρακάτω μεθόδους και ταυτόχρονα να τερματίσουμε τον presenter: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` και `forward()` [ανακατευθύνουν |#Ανακατεύθυνση] -- `error()` τερματίζει τον presenter [λόγω σφάλματος |#Σφάλμα 404 κ.λπ] -- `sendJson($data)` τερματίζει τον presenter και [στέλνει δεδομένα |#Αποστολή JSON] σε μορφή JSON -- `sendTemplate()` τερματίζει τον presenter και αμέσως [αποδίδει το πρότυπο |templates] -- `sendResponse($response)` τερματίζει τον presenter και στέλνει [μια προσαρμοσμένη response |#Απαντήσεις] -- `terminate()` τερματίζει τον presenter χωρίς response - -Αν δεν καλέσετε καμία από αυτές τις μεθόδους, ο presenter θα προχωρήσει αυτόματα στην απόδοση του προτύπου. Γιατί; Επειδή στο 99% των περιπτώσεων θέλουμε να αποδώσουμε ένα πρότυπο, επομένως ο presenter θεωρεί αυτή τη συμπεριφορά ως προεπιλεγμένη και θέλει να μας διευκολύνει τη δουλειά. - - -Δημιουργία συνδέσμων -==================== - -Ο presenter διαθέτει τη μέθοδο `link()`, με την οποία μπορείτε να δημιουργήσετε συνδέσμους URL προς άλλους presenters. Η πρώτη παράμετρος είναι ο presenter & η action προορισμού, ακολουθούν τα μεταβιβαζόμενα ορίσματα, τα οποία μπορούν να καθοριστούν ως array: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'cs']); -``` - -Στο πρότυπο, οι σύνδεσμοι προς άλλους presenters & actions δημιουργούνται με αυτόν τον τρόπο: - -```latte -λεπτομέρεια προϊόντος -``` - -Απλά αντί για το πραγματικό URL, γράφετε το γνωστό ζεύγος `Presenter:action` και καθορίζετε τυχόν παραμέτρους. Το κόλπο είναι στο `n:href`, το οποίο λέει ότι αυτό το attribute θα επεξεργαστεί το Latte και θα δημιουργήσει το πραγματικό URL. Στο Nette, επομένως, δεν χρειάζεται καθόλου να σκέφτεστε τα URL, μόνο τους presenters και τις actions. - -Περισσότερες πληροφορίες θα βρείτε στο κεφάλαιο [Δημιουργία συνδέσμων URL|creating-links]. - - -Ανακατεύθυνση -============= - -Για τη μετάβαση σε άλλο presenter, χρησιμοποιούνται οι μέθοδοι `redirect()` και `forward()`, οι οποίες έχουν πολύ παρόμοια σύνταξη με τη μέθοδο [link() |#Δημιουργία συνδέσμων]. - -Η μέθοδος `forward()` μεταβαίνει στον νέο presenter αμέσως χωρίς ανακατεύθυνση HTTP: - -```php -$this->forward('Product:show'); -``` - -Παράδειγμα της λεγόμενης προσωρινής ανακατεύθυνσης με κωδικό HTTP 302 (ή 303, αν η μέθοδος της τρέχουσας αίτησης είναι POST): - -```php -$this->redirect('Product:show', $id); -``` - -Μόνιμη ανακατεύθυνση με κωδικό HTTP 301 επιτυγχάνεται ως εξής: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Σε άλλη διεύθυνση URL εκτός της εφαρμογής μπορείτε να ανακατευθύνετε με τη μέθοδο `redirectUrl()`. Ως δεύτερη παράμετρο, μπορείτε να καθορίσετε τον κωδικό HTTP, ο προεπιλεγμένος είναι 302 (ή 303, αν η μέθοδος της τρέχουσας αίτησης είναι POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Η ανακατεύθυνση τερματίζει αμέσως τη λειτουργία του presenter δημιουργώντας τη λεγόμενη σιωπηλή εξαίρεση τερματισμού `Nette\Application\AbortException`. - -Πριν από την ανακατεύθυνση, μπορείτε να στείλετε [flash message |#Flash μηνύματα], δηλαδή μηνύματα που θα εμφανιστούν στο πρότυπο μετά την ανακατεύθυνση. - - -Flash μηνύματα -============== - -Πρόκειται για μηνύματα που συνήθως ενημερώνουν για το αποτέλεσμα κάποιας λειτουργίας. Ένα σημαντικό χαρακτηριστικό των flash μηνυμάτων είναι ότι είναι διαθέσιμα στο πρότυπο ακόμη και μετά από ανακατεύθυνση. Ακόμη και μετά την εμφάνισή τους, παραμένουν ενεργά για άλλα 30 δευτερόλεπτα – για παράδειγμα, σε περίπτωση που ο χρήστης ανανεώσει τη σελίδα λόγω σφάλματος μετάδοσης - το μήνυμα δεν εξαφανίζεται αμέσως. - -Αρκεί να καλέσετε τη μέθοδο [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] και ο presenter θα φροντίσει για τη μεταβίβασή τους στο πρότυπο. Η πρώτη παράμετρος είναι το κείμενο του μηνύματος και η προαιρετική δεύτερη παράμετρος ο τύπος του (error, warning, info κ.λπ.). Η μέθοδος `flashMessage()` επιστρέφει μια παρουσία του flash μηνύματος, στην οποία μπορούν να προστεθούν περαιτέρω πληροφορίες. - -```php -$this->flashMessage('Το στοιχείο διαγράφηκε.'); -$this->redirect(/* ... */); // και ανακατεύθυνση -``` - -Στο πρότυπο, αυτά τα μηνύματα είναι διαθέσιμα στη μεταβλητή `$flashes` ως αντικείμενα `stdClass`, τα οποία περιέχουν τις ιδιότητες `message` (κείμενο μηνύματος), `type` (τύπος μηνύματος) και μπορούν να περιέχουν τις ήδη αναφερθείσες πληροφορίες χρήστη. Τα αποδίδουμε, για παράδειγμα, ως εξής: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Σφάλμα 404 κ.λπ. -================ - -Αν δεν είναι δυνατό να ικανοποιηθεί το αίτημα, για παράδειγμα, επειδή το άρθρο που θέλουμε να εμφανίσουμε δεν υπάρχει στη βάση δεδομένων, δημιουργούμε σφάλμα 404 με τη μέθοδο `error(?string $message = null, int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -Ο κωδικός HTTP του σφάλματος μπορεί να περαστεί ως δεύτερη παράμετρος, ο προεπιλεγμένος είναι 404. Η μέθοδος λειτουργεί δημιουργώντας την εξαίρεση `Nette\Application\BadRequestException`, οπότε η `Application` παραδίδει τον έλεγχο στον error-presenter. Αυτός είναι ένας presenter του οποίου ο ρόλος είναι να εμφανίσει μια σελίδα που ενημερώνει για το σφάλμα που προέκυψε. Η ρύθμιση του error-presenter γίνεται στη [διαμόρφωση application|configuration]. - - -Αποστολή JSON -============= - -Παράδειγμα μεθόδου action που στέλνει δεδομένα σε μορφή JSON και τερματίζει τον presenter: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Παράμετροι αιτήματος .{data-version:3.1.14} -=========================================== - -Ο presenter και επίσης κάθε component λαμβάνει τις παραμέτρους του από το αίτημα HTTP. Μπορείτε να βρείτε την τιμή τους χρησιμοποιώντας τη μέθοδο `getParameter($name)` ή `getParameters()`. Οι τιμές είναι strings ή arrays από strings, πρόκειται ουσιαστικά για ακατέργαστα δεδομένα που λαμβάνονται απευθείας από το URL. - -Για μεγαλύτερη ευκολία, συνιστούμε να κάνετε τις παραμέτρους προσβάσιμες μέσω ιδιοτήτων. Αρκεί να τις επισημάνετε με το attribute `#[Parameter]`: - -```php -use Nette\Application\Attributes\Parameter; // αυτή η γραμμή είναι σημαντική - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // πρέπει να είναι public -} -``` - -Συνιστούμε να καθορίσετε τον τύπο δεδομένων για την ιδιότητα (π.χ. `string`) και το Nette θα μετατρέψει αυτόματα την τιμή σύμφωνα με αυτόν. Οι τιμές των παραμέτρων μπορούν επίσης να [επικυρωθούν |#Επικύρωση παραμέτρων]. - -Κατά τη δημιουργία ενός συνδέσμου, η τιμή των παραμέτρων μπορεί να οριστεί απευθείας: - -```latte -κάντε κλικ -``` - - -Persistent παράμετροι -===================== - -Οι persistent παράμετροι χρησιμοποιούνται για τη διατήρηση της κατάστασης μεταξύ διαφορετικών αιτήσεων. Η τιμή τους παραμένει η ίδια ακόμη και μετά το κλικ σε έναν σύνδεσμο. Σε αντίθεση με τα δεδομένα στη session, μεταφέρονται στη διεύθυνση URL. Και αυτό γίνεται εντελώς αυτόματα, δεν χρειάζεται δηλαδή να τις καθορίσετε ρητά στο `link()` ή στο `n:href`. - -Παράδειγμα χρήσης; Έχετε μια πολύγλωσση εφαρμογή. Η τρέχουσα γλώσσα είναι μια παράμετρος που πρέπει να είναι συνεχώς μέρος της διεύθυνσης URL. Αλλά θα ήταν απίστευτα κουραστικό να την καθορίζετε σε κάθε σύνδεσμο. Έτσι, την κάνετε μια persistent παράμετρο `lang` και θα μεταφέρεται μόνη της. Υπέροχο! - -Η δημιουργία μιας persistent παραμέτρου στο Nette είναι εξαιρετικά απλή. Αρκεί να δημιουργήσετε μια δημόσια ιδιότητα και να την επισημάνετε με ένα attribute: (παλαιότερα χρησιμοποιούνταν το `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // αυτή η γραμμή είναι σημαντική - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // πρέπει να είναι public -} -``` - -Αν το `$this->lang` έχει την τιμή, για παράδειγμα, `'en'`, τότε και οι σύνδεσμοι που δημιουργούνται χρησιμοποιώντας το `link()` ή το `n:href` θα περιέχουν την παράμετρο `lang=en`. Και μετά το κλικ στον σύνδεσμο, το `$this->lang` θα είναι ξανά `'en'`. - -Συνιστούμε να καθορίσετε τον τύπο δεδομένων για την ιδιότητα (π.χ. `string`) και μπορείτε επίσης να καθορίσετε μια προεπιλεγμένη τιμή. Οι τιμές των παραμέτρων μπορούν να [επικυρωθούν |#Επικύρωση παραμέτρων]. - -Οι persistent παράμετροι μεταφέρονται κανονικά μεταξύ όλων των actions του συγκεκριμένου presenter. Για να μεταφέρονται και μεταξύ πολλών presenters, πρέπει να οριστούν είτε: - -- σε έναν κοινό πρόγονο, από τον οποίο κληρονομούν οι presenters -- σε ένα trait, το οποίο χρησιμοποιούν οι presenters: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Κατά τη δημιουργία ενός συνδέσμου, η τιμή της persistent παραμέτρου μπορεί να αλλάξει: - -```latte -λεπτομέρεια στα Τσέχικα -``` - -Ή μπορεί να *επαναφερθεί*, δηλαδή να αφαιρεθεί από τη διεύθυνση URL. Στη συνέχεια, θα πάρει την προεπιλεγμένη της τιμή: - -```latte -κάντε κλικ -``` - - -Διαδραστικά Components -====================== - -Οι presenters έχουν ενσωματωμένο σύστημα components. Τα components είναι ανεξάρτητες, επαναχρησιμοποιήσιμες μονάδες που ενσωματώνουμε στους presenters. Μπορεί να είναι [φόρμες |forms:in-presenter], datagrids, μενού, στην πραγματικότητα οτιδήποτε έχει νόημα να χρησιμοποιείται επανειλημμένα. - -Πώς ενσωματώνονται και στη συνέχεια χρησιμοποιούνται τα components στον presenter; Αυτό θα το μάθετε στο κεφάλαιο [Components |components]. Θα ανακαλύψετε ακόμη και τι κοινό έχουν με το Hollywood. - -Και πού μπορώ να βρω components; Στη σελίδα [Componette |https://componette.org/search/component] θα βρείτε open-source components και επίσης μια σειρά από άλλα πρόσθετα για το Nette, τα οποία έχουν τοποθετηθεί εδώ από εθελοντές της κοινότητας γύρω από το framework. - - -Πάμε βαθύτερα -============= - -.[tip] -Με όσα έχουμε δείξει μέχρι τώρα σε αυτό το κεφάλαιο, πιθανότατα θα τα βγάλετε πέρα. Οι παρακάτω γραμμές προορίζονται για όσους ενδιαφέρονται για τους presenters σε βάθος και θέλουν να μάθουν τα πάντα. - - -Επικύρωση παραμέτρων --------------------- - -Οι τιμές των [παραμέτρων αιτήματος |#Παράμετροι αιτήματος] και των [persistent παραμέτρων |#Persistent παράμετροι] που λαμβάνονται από τη διεύθυνση URL γράφονται στις ιδιότητες από τη μέθοδο `loadState()`. Αυτή ελέγχει επίσης εάν ο τύπος δεδομένων που καθορίζεται στην ιδιότητα αντιστοιχεί, διαφορετικά απαντά με σφάλμα 404 και η σελίδα δεν εμφανίζεται. - -Ποτέ μην εμπιστεύεστε τυφλά τις παραμέτρους, επειδή μπορούν εύκολα να αντικατασταθούν από τον χρήστη στη διεύθυνση URL. Έτσι, για παράδειγμα, επαληθεύουμε εάν η γλώσσα `$this->lang` είναι μεταξύ των υποστηριζόμενων. Ένας κατάλληλος τρόπος είναι να αντικαταστήσετε την αναφερόμενη μέθοδο `loadState()`: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // εδώ ορίζεται το $this->lang - // ακολουθεί προσαρμοσμένος έλεγχος τιμής: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Αποθήκευση και ανάκτηση αιτήματος ---------------------------------- - -Το αίτημα που διεκπεραιώνει ο presenter είναι ένα αντικείμενο [api:Nette\Application\Request] και επιστρέφεται από τη μέθοδο του presenter `getRequest()`. - -Το τρέχον αίτημα μπορεί να αποθηκευτεί στη session ή, αντίθετα, να ανακτηθεί από αυτήν και να αφεθεί ο presenter να το εκτελέσει ξανά. Αυτό είναι χρήσιμο, για παράδειγμα, σε μια κατάσταση όπου ο χρήστης συμπληρώνει μια φόρμα και η σύνδεσή του λήγει. Για να μην χάσει τα δεδομένα, πριν από την ανακατεύθυνση στη σελίδα σύνδεσης, αποθηκεύουμε το τρέχον αίτημα στη session χρησιμοποιώντας το `$reqId = $this->storeRequest()`, το οποίο επιστρέφει το αναγνωριστικό του με τη μορφή μιας σύντομης συμβολοσειράς και το μεταβιβάζουμε ως παράμετρο στον presenter σύνδεσης. - -Μετά τη σύνδεση, καλούμε τη μέθοδο `$this->restoreRequest($reqId)`, η οποία ανακτά το αίτημα από τη session και προωθεί σε αυτό. Η μέθοδος επαληθεύει ταυτόχρονα ότι το αίτημα δημιουργήθηκε από τον ίδιο χρήστη που συνδέθηκε τώρα. Αν συνδεθεί άλλος χρήστης ή το κλειδί είναι άκυρο, δεν κάνει τίποτα και το πρόγραμμα συνεχίζει. - -Δείτε τον οδηγό [Πώς να επιστρέψετε σε προηγούμενη σελίδα |best-practices:restore-request]. - - -Κανονικοποίηση --------------- - -Οι presenters έχουν ένα πραγματικά εξαιρετικό χαρακτηριστικό που συμβάλλει στο καλύτερο SEO (βελτιστοποίηση για μηχανές αναζήτησης). Αποτρέπουν αυτόματα την ύπαρξη διπλού περιεχομένου σε διαφορετικά URL. Αν υπάρχουν πολλαπλά URL που οδηγούν στον ίδιο στόχο, π.χ. `/index` και `/index?page=1`, το framework καθορίζει ένα από αυτά ως το κύριο (κανονικό) και ανακατευθύνει τα υπόλοιπα σε αυτό χρησιμοποιώντας τον κωδικό HTTP 301. Χάρη σε αυτό, οι μηχανές αναζήτησης δεν ευρετηριάζουν τις σελίδες σας δύο φορές και δεν διασπούν το page rank τους. - -Αυτή η διαδικασία ονομάζεται κανονικοποίηση. Η κανονική διεύθυνση URL είναι αυτή που δημιουργείται από τον [router|routing], συνήθως δηλαδή η πρώτη αντίστοιχη διαδρομή στη συλλογή. - -Η κανονικοποίηση είναι ενεργοποιημένη από προεπιλογή και μπορεί να απενεργοποιηθεί μέσω του `$this->autoCanonicalize = false`. - -Η ανακατεύθυνση δεν πραγματοποιείται κατά τη διάρκεια μιας αίτησης AJAX ή POST, επειδή θα προκαλούσε απώλεια δεδομένων ή δεν θα είχε προστιθέμενη αξία από άποψη SEO. - -Μπορείτε επίσης να καλέσετε την κανονικοποίηση χειροκίνητα χρησιμοποιώντας τη μέθοδο `canonicalize()`, στην οποία, παρόμοια με τη μέθοδο `link()`, περνιέται ο presenter, η action και οι παράμετροι. Δημιουργεί έναν σύνδεσμο και τον συγκρίνει με την τρέχουσα διεύθυνση URL. Αν διαφέρουν, ανακατευθύνει στον δημιουργημένο σύνδεσμο. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // ανακατευθύνει αν το $slug διαφέρει από το $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - - -Γεγονότα --------- - -Εκτός από τις μεθόδους `startup()`, `beforeRender()` και `shutdown()`, οι οποίες καλούνται ως μέρος του κύκλου ζωής του presenter, μπορούν να οριστούν και άλλες συναρτήσεις που θα καλούνται αυτόματα. Ο presenter ορίζει τα λεγόμενα [γεγονότα |nette:glossary#Events], των οποίων τους handlers προσθέτετε στους πίνακες `$onStartup`, `$onRender` και `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Οι handlers στον πίνακα `$onStartup` καλούνται ακριβώς πριν από τη μέθοδο `startup()`, στη συνέχεια το `$onRender` μεταξύ `beforeRender()` και `render()` και τέλος το `$onShutdown` ακριβώς πριν από το `shutdown()`. - - -Απαντήσεις ----------- - -Η response που επιστρέφει ο presenter είναι ένα αντικείμενο που υλοποιεί το interface [api:Nette\Application\Response]. Υπάρχουν διαθέσιμες πολλές έτοιμες responses: - -- [api:Nette\Application\Responses\CallbackResponse] - στέλνει ένα callback -- [api:Nette\Application\Responses\FileResponse] - στέλνει ένα αρχείο -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - στέλνει JSON -- [api:Nette\Application\Responses\RedirectResponse] - ανακατεύθυνση -- [api:Nette\Application\Responses\TextResponse] - στέλνει κείμενο -- [api:Nette\Application\Responses\VoidResponse] - κενή response - -Οι responses στέλνονται με τη μέθοδο `sendResponse()`: - -```php -use Nette\Application\Responses; - -// Απλό κείμενο -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Στέλνει ένα αρχείο -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// Η response θα είναι ένα callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - - -Περιορισμός πρόσβασης με `#[Requires]` .{data-version:3.2.2} ------------------------------------------------------------- - -Το attribute `#[Requires]` παρέχει προηγμένες δυνατότητες για τον περιορισμό της πρόσβασης σε presenters και τις μεθόδους τους. Μπορεί να χρησιμοποιηθεί για τον καθορισμό μεθόδων HTTP, την απαίτηση αίτησης AJAX, τον περιορισμό στην ίδια προέλευση (same origin), και την πρόσβαση μόνο μέσω προώθησης (forwarding). Το attribute μπορεί να εφαρμοστεί τόσο στις κλάσεις των presenters όσο και στις μεμονωμένες μεθόδους `action()`, `render()`, `handle()` και `createComponent()`. - -Μπορείτε να καθορίσετε αυτούς τους περιορισμούς: -- σε μεθόδους HTTP: `#[Requires(methods: ['GET', 'POST'])]` -- απαίτηση αίτησης AJAX: `#[Requires(ajax: true)]` -- πρόσβαση μόνο από την ίδια προέλευση: `#[Requires(sameOrigin: true)]` -- πρόσβαση μόνο μέσω forward: `#[Requires(forward: true)]` -- περιορισμός σε συγκεκριμένες actions: `#[Requires(actions: 'default')]` - -Λεπτομέρειες θα βρείτε στον οδηγό [Πώς να χρησιμοποιήσετε το attribute Requires |best-practices:attribute-requires]. - - -Έλεγχος μεθόδου HTTP --------------------- - -Οι presenters στο Nette επαληθεύουν αυτόματα τη μέθοδο HTTP κάθε εισερχόμενου αιτήματος. Ο λόγος για αυτόν τον έλεγχο είναι κυρίως η ασφάλεια. Από προεπιλογή, επιτρέπονται οι μέθοδοι `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. - -Αν θέλετε να επιτρέψετε επιπλέον, για παράδειγμα, τη μέθοδο `OPTIONS`, χρησιμοποιήστε το attribute `#[Requires]` (από το Nette Application v3.2): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Στην έκδοση 3.1, η επαλήθευση γίνεται στην `checkHttpMethod()`, η οποία ελέγχει εάν η μέθοδος που καθορίζεται στην αίτηση περιλαμβάνεται στον πίνακα `$presenter->allowedMethods`. Η προσθήκη της μεθόδου γίνεται ως εξής: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Είναι σημαντικό να τονιστεί ότι αν επιτρέψετε τη μέθοδο `OPTIONS`, πρέπει στη συνέχεια να την χειριστείτε κατάλληλα εντός του presenter σας. Η μέθοδος χρησιμοποιείται συχνά ως το λεγόμενο preflight request, το οποίο το πρόγραμμα περιήγησης στέλνει αυτόματα πριν από το πραγματικό αίτημα, όταν χρειάζεται να διαπιστωθεί εάν το αίτημα επιτρέπεται από την πολιτική CORS (Cross-Origin Resource Sharing). Αν επιτρέψετε τη μέθοδο, αλλά δεν υλοποιήσετε τη σωστή response, μπορεί να οδηγήσει σε ασυνέπειες και πιθανά προβλήματα ασφάλειας. - - -Περαιτέρω ανάγνωση -================== - -- [Μέθοδοι και attributes inject |best-practices:inject-method-attribute] -- [Σύνθεση presenters από traits |best-practices:presenter-traits] -- [Μεταβίβαση ρυθμίσεων σε presenters |best-practices:passing-settings-to-presenters] -- [Πώς να επιστρέψετε σε προηγούμενη σελίδα |best-practices:restore-request] diff --git a/application/el/routing.texy b/application/el/routing.texy deleted file mode 100644 index 31800ef023..0000000000 --- a/application/el/routing.texy +++ /dev/null @@ -1,721 +0,0 @@ -Δρομολόγηση -*********** - -
    - -Ο Router είναι υπεύθυνος για τα πάντα γύρω από τις διευθύνσεις URL, ώστε να μην χρειάζεται πλέον να τις σκέφτεστε. Θα δείξουμε: - -- πώς να ρυθμίσετε τον router ώστε τα URL να είναι όπως τα φαντάζεστε -- θα μιλήσουμε για SEO και ανακατεύθυνση -- και θα δείξουμε πώς να γράψετε τον δικό σας router - -
    - - -Οι πιο ανθρώπινες διευθύνσεις URL (ή αλλιώς cool ή pretty URL) είναι πιο εύχρηστες, πιο εύκολα απομνημονεύσιμες και συμβάλλουν θετικά στο SEO. Το Nette το λαμβάνει υπόψη και υποστηρίζει πλήρως τους προγραμματιστές. Μπορείτε να σχεδιάσετε για την εφαρμογή σας ακριβώς τη δομή των διευθύνσεων URL που θέλετε. Μπορείτε να τη σχεδιάσετε ακόμη και όταν η εφαρμογή είναι ήδη έτοιμη, επειδή αυτό γίνεται χωρίς παρεμβάσεις στον κώδικα ή τα πρότυπα. Ορίζεται με κομψό τρόπο σε ένα [μόνο σημείο |#Ενσωμάτωση στην εφαρμογή], στον router, και δεν είναι διάσπαρτη με τη μορφή σχολιαστικών παρατηρήσεων σε όλους τους presenters. - -Ο router στο Nette είναι εξαιρετικός στο ότι είναι **αμφίδρομος.** Μπορεί τόσο να αποκωδικοποιεί τα URL σε αιτήματα HTTP, όσο και να δημιουργεί συνδέσμους. Παίζει επομένως καθοριστικό ρόλο στην [Nette Application |how-it-works#Nette Application], επειδή αφενός αποφασίζει ποιος presenter και action θα εκτελέσει το τρέχον αίτημα, αλλά χρησιμοποιείται επίσης για τη [δημιουργία URL |creating-links] στο πρότυπο κ.λπ. - -Ωστόσο, ο router δεν περιορίζεται μόνο σε αυτή τη χρήση, μπορείτε να τον χρησιμοποιήσετε σε εφαρμογές όπου δεν χρησιμοποιούνται καθόλου presenters, για REST API, κ.λπ. Περισσότερα στην ενότητα [#Αυτόνομη χρήση]. - - -Συλλογή διαδρομών -================= - -Ο πιο ευχάριστος τρόπος για να ορίσετε τη μορφή των διευθύνσεων URL στην εφαρμογή είναι η κλάση [api:Nette\Application\Routers\RouteList]. Ο ορισμός αποτελείται από μια λίστα λεγόμενων routes, δηλαδή μασκών διευθύνσεων URL και των σχετικών presenters και actions που συνδέονται με αυτές μέσω ενός απλού API. Δεν χρειάζεται να ονομάσουμε τις routes με κανέναν τρόπο. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -Το παράδειγμα λέει ότι αν ανοίξουμε στο πρόγραμμα περιήγησης το `https://domain.com/rss.xml`, θα εμφανιστεί ο presenter `Feed` με την action `rss`, αν ανοίξουμε το `https://domain.com/article/12`, θα εμφανιστεί ο presenter `Article` με την action `view` κ.λπ. Σε περίπτωση που δεν βρεθεί κατάλληλη route, η Nette Application αντιδρά δημιουργώντας την εξαίρεση [BadRequestException |api:Nette\Application\BadRequestException], η οποία εμφανίζεται στον χρήστη ως σελίδα σφάλματος 404 Not Found. - - -Σειρά διαδρομών ---------------- - -Η **σειρά** με την οποία αναφέρονται οι επιμέρους routes είναι **απολύτως κρίσιμη**, επειδή αξιολογούνται διαδοχικά από πάνω προς τα κάτω. Ισχύει ο κανόνας ότι δηλώνουμε τις routes **από τις πιο συγκεκριμένες προς τις πιο γενικές**: - -```php -// ΛΑΘΟΣ: το 'rss.xml' πιάνεται από την πρώτη route και κατανοεί αυτή τη συμβολοσειρά ως -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// ΣΩΣΤΟ -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Οι routes αξιολογούνται από πάνω προς τα κάτω και κατά τη δημιουργία συνδέσμων: - -```php -// ΛΑΘΟΣ: ο σύνδεσμος προς 'Feed:rss' δημιουργείται ως 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// ΣΩΣΤΟ -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Δεν θα κρύψουμε από εσάς ότι η σωστή σύνθεση των routes απαιτεί κάποια δεξιότητα. Μέχρι να την αποκτήσετε, θα σας φανεί χρήσιμος ο [πίνακας δρομολόγησης |#Αποσφαλμάτωση του router]. - - -Μάσκα και παράμετροι --------------------- - -Η μάσκα περιγράφει τη σχετική διαδρομή από τον ριζικό κατάλογο του ιστότοπου. Η απλούστερη μάσκα είναι ένα στατικό URL: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Συχνά οι μάσκες περιέχουν τις λεγόμενες **παραμέτρους**. Αυτές αναφέρονται σε αιχμηρές αγκύλες (π.χ. ``) και μεταβιβάζονται στον presenter προορισμού, για παράδειγμα στη μέθοδο `renderShow(int $year)` ή στην persistent παράμετρο `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Το παράδειγμα λέει ότι αν ανοίξουμε στο πρόγραμμα περιήγησης το `https://example.com/chronicle/2020`, θα εμφανιστεί ο presenter `History` με την action `show` και την παράμετρο `year: 2020`. - -Μπορούμε να ορίσουμε μια προεπιλεγμένη τιμή για τις παραμέτρους απευθείας στη μάσκα και έτσι γίνονται προαιρετικές: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Η route θα δέχεται τώρα και το URL `https://example.com/chronicle/`, το οποίο θα εμφανίσει ξανά το `History:show` με την παράμετρο `year: 2020`. - -Παράμετρος μπορεί φυσικά να είναι και το όνομα του presenter και της action. Για παράδειγμα, έτσι: - -```php -$router->addRoute('/', 'Home:default'); -``` - -Η αναφερόμενη route δέχεται, για παράδειγμα, URL της μορφής `/article/edit` ή επίσης `/catalog/list` και τα κατανοεί ως presenters και actions `Article:edit` και `Catalog:list`. - -Ταυτόχρονα, δίνει στις παραμέτρους `presenter` και `action` τις προεπιλεγμένες τιμές `Home` και `default` και είναι επομένως επίσης προαιρετικές. Έτσι, η route δέχεται και URL της μορφής `/article` και το κατανοεί ως `Article:default`. Ή αντίστροφα, ένας σύνδεσμος προς το `Product:default` θα δημιουργήσει τη διαδρομή `/product`, ένας σύνδεσμος προς το προεπιλεγμένο `Home:default` τη διαδρομή `/`. - -Η μάσκα μπορεί να περιγράφει όχι μόνο τη σχετική διαδρομή από τον ριζικό κατάλογο του ιστότοπου, αλλά και την απόλυτη διαδρομή, αν ξεκινά με κάθετο, ή ακόμα και ολόκληρο το απόλυτο URL, αν ξεκινά με δύο κάθετους: - -```php -// σχετικά με το document root -$router->addRoute('/', /* ... */); - -// απόλυτη διαδρομή (σχετικά με τον τομέα) -$router->addRoute('//', /* ... */); - -// απόλυτο URL συμπεριλαμβανομένου του τομέα (σχετικά με το σχήμα) -$router->addRoute('//.example.com//', /* ... */); - -// απόλυτο URL συμπεριλαμβανομένου του σχήματος -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Εκφράσεις επικύρωσης --------------------- - -Για κάθε παράμετρο, μπορεί να οριστεί μια συνθήκη επικύρωσης χρησιμοποιώντας μια [κανονική έκφραση|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Για παράδειγμα, για την παράμετρο `id`, καθορίζουμε ότι μπορεί να πάρει μόνο αριθμητικές τιμές χρησιμοποιώντας την κανονική έκφραση `\d+`: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -Η προεπιλεγμένη κανονική έκφραση για όλες τις παραμέτρους είναι `[^/]+`, δηλαδή οτιδήποτε εκτός από κάθετο. Αν μια παράμετρος πρέπει να δέχεται και κάθετους, καθορίζουμε την έκφραση `.+`: - -```php -// δέχεται https://example.com/a/b/c, η διαδρομή θα είναι 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Προαιρετικές ακολουθίες ------------------------ - -Στη μάσκα, μπορείτε να επισημάνετε προαιρετικά τμήματα χρησιμοποιώντας αγκύλες. Οποιοδήποτε τμήμα της μάσκας μπορεί να είναι προαιρετικό, μπορεί να περιέχει και παραμέτρους: - -```php -$router->addRoute('[/]', /* ... */); - -// Δέχεται διαδρομές: -// /cs/download => lang => cs, name => download -// /download => lang => null, name => download -``` - -Όταν μια παράμετρος είναι μέρος μιας προαιρετικής ακολουθίας, γίνεται φυσικά επίσης προαιρετική. Αν δεν έχει καθορισμένη προεπιλεγμένη τιμή, θα είναι null. - -Προαιρετικά τμήματα μπορούν να υπάρχουν και στον τομέα: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Οι ακολουθίες μπορούν να ενσωματωθούν και να συνδυαστούν ελεύθερα: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Δέχεται διαδρομές: -// /cs/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -Κατά τη δημιουργία URL, επιδιώκεται η συντομότερη παραλλαγή, οπότε οτιδήποτε μπορεί να παραλειφθεί, παραλείπεται. Γι' αυτό, για παράδειγμα, η route `index[.html]` δημιουργεί τη διαδρομή `/index`. Η αναστροφή της συμπεριφοράς είναι δυνατή με την προσθήκη ενός θαυμαστικού μετά την αριστερή αγκύλη: - -```php -// δέχεται /hello και /hello.html, δημιουργεί /hello -$router->addRoute('[.html]', /* ... */); - -// δέχεται /hello και /hello.html, δημιουργεί /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Οι προαιρετικές παράμετροι (δηλαδή οι παράμετροι που έχουν προεπιλεγμένη τιμή) χωρίς αγκύλες συμπεριφέρονται ουσιαστικά σαν να ήταν περικλεισμένες με τον ακόλουθο τρόπο: - -```php -$router->addRoute('//', /* ... */); - -// αντιστοιχεί σε αυτό: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Αν θέλαμε να επηρεάσουμε τη συμπεριφορά της τελικής κάθετου, ώστε για παράδειγμα αντί για `/home/` να δημιουργείται μόνο `/home`, αυτό μπορεί να επιτευχθεί ως εξής: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Χαρακτήρες μπαλαντέρ --------------------- - -Στη μάσκα μιας απόλυτης διαδρομής, μπορούμε να χρησιμοποιήσουμε τους ακόλουθους χαρακτήρες μπαλαντέρ και να αποφύγουμε έτσι, για παράδειγμα, την ανάγκη να γράψουμε στη μάσκα τον τομέα, ο οποίος μπορεί να διαφέρει στο περιβάλλον ανάπτυξης και παραγωγής: - -- `%tld%` = top level domain, π.χ. `com` ή `org` -- `%sld%` = second level domain, π.χ. `example` -- `%domain%` = τομέας χωρίς υποτομείς, π.χ. `example.com` -- `%host%` = ολόκληρος ο host, π.χ. `www.example.com` -- `%basePath%` = διαδρομή προς τον ριζικό κατάλογο - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Για πιο λεπτομερή προδιαγραφή, μπορεί να χρησιμοποιηθεί μια ακόμη πιο εκτεταμένη μορφή, όπου εκτός από τις προεπιλεγμένες τιμές, μπορούμε να ορίσουμε και άλλες ιδιότητες των παραμέτρων, όπως για παράδειγμα την κανονική έκφραση επικύρωσης (βλ. παράμετρο `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Είναι σημαντικό να σημειωθεί ότι αν οι παράμετροι που ορίζονται στον πίνακα δεν αναφέρονται στη μάσκα της διαδρομής, οι τιμές τους δεν μπορούν να αλλάξουν, ούτε με παραμέτρους query που αναφέρονται μετά το ερωτηματικό στο URL. - - -Φίλτρα και μεταφράσεις ----------------------- - -Γράφουμε τον πηγαίο κώδικα της εφαρμογής στα Αγγλικά, αλλά αν ο ιστότοπος πρέπει να έχει ελληνικά URL, τότε η απλή δρομολόγηση του τύπου: - -```php -$router->addRoute('/', 'Home:default'); -``` - -θα δημιουργήσει αγγλικά URL, όπως `/product/123` ή `/cart`. Αν θέλουμε οι presenters και οι actions στο URL να αντιπροσωπεύονται από ελληνικές λέξεις (π.χ. `/produkt/123` ή `/kosik`), μπορούμε να χρησιμοποιήσουμε ένα λεξικό μετάφρασης. Για τη σύνταξή του, χρειαζόμαστε ήδη την "πιο ομιλητική" παραλλαγή της δεύτερης παραμέτρου: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // συμβολοσειρά στο URL => presenter - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Πολλά κλειδιά του λεξικού μετάφρασης μπορούν να οδηγούν στον ίδιο presenter. Έτσι, δημιουργούνται διάφορα ψευδώνυμα γι' αυτόν. Ως κανονική παραλλαγή (δηλαδή αυτή που θα βρίσκεται στο δημιουργημένο URL) θεωρείται το τελευταίο κλειδί. - -Ο πίνακας μετάφρασης μπορεί να χρησιμοποιηθεί με αυτόν τον τρόπο για οποιαδήποτε παράμετρο. Ενώ αν η μετάφραση δεν υπάρχει, λαμβάνεται η αρχική τιμή. Αυτή η συμπεριφορά μπορεί να αλλάξει συμπληρώνοντας `Route::FilterStrict => true` και η route θα απορρίψει τότε το URL αν η τιμή δεν βρίσκεται στο λεξικό. - -Εκτός από το λεξικό μετάφρασης με τη μορφή πίνακα, μπορούν να εφαρμοστούν και προσαρμοσμένες συναρτήσεις μετάφρασης. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -Η συνάρτηση `Route::FilterIn` μετατρέπει μεταξύ της παραμέτρου στο URL και της συμβολοσειράς που στη συνέχεια μεταβιβάζεται στον presenter, η συνάρτηση `FilterOut` εξασφαλίζει τη μετατροπή προς την αντίθετη κατεύθυνση. - -Οι παράμετροι `presenter`, `action` και `module` έχουν ήδη προκαθορισμένα φίλτρα που μετατρέπουν μεταξύ του στυλ PascalCase ή camelCase και του kebab-case που χρησιμοποιείται στα URL. Η προεπιλεγμένη τιμή των παραμέτρων γράφεται ήδη στη μετασχηματισμένη μορφή, οπότε για παράδειγμα στην περίπτωση του presenter γράφουμε ``, όχι ``. - - -Γενικά φίλτρα -------------- - -Εκτός από τα φίλτρα που προορίζονται για συγκεκριμένες παραμέτρους, μπορούμε επίσης να ορίσουμε γενικά φίλτρα που λαμβάνουν έναν συσχετιστικό πίνακα όλων των παραμέτρων, τα οποία μπορούν να τροποποιήσουν με οποιονδήποτε τρόπο και στη συνέχεια να τα επιστρέψουν. Ορίζουμε τα γενικά φίλτρα κάτω από το κλειδί `null`. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Τα γενικά φίλτρα δίνουν τη δυνατότητα να τροποποιήσετε τη συμπεριφορά της route με απολύτως οποιονδήποτε τρόπο. Μπορούμε να τα χρησιμοποιήσουμε, για παράδειγμα, για την τροποποίηση παραμέτρων με βάση άλλες παραμέτρους. Για παράδειγμα, τη μετάφραση των `` και `` με βάση την τρέχουσα τιμή της παραμέτρου ``. - -Αν μια παράμετρος έχει ορισμένο δικό της φίλτρο και ταυτόχρονα υπάρχει ένα γενικό φίλτρο, εκτελείται το δικό της `FilterIn` πριν από το γενικό και αντίστροφα το γενικό `FilterOut` πριν από το δικό της. Επομένως, μέσα στο γενικό φίλτρο, οι τιμές των παραμέτρων `presenter` ή `action` είναι γραμμένες σε στυλ PascalCase ή camelCase. - - -Μονόδρομες OneWay ------------------ - -Οι μονόδρομες routes χρησιμοποιούνται για τη διατήρηση της λειτουργικότητας παλιών URL, τα οποία η εφαρμογή δεν δημιουργεί πλέον, αλλά εξακολουθεί να δέχεται. Τις επισημαίνουμε με τη σημαία `OneWay`: - -```php -// παλιό URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); -// νέο URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -Κατά την πρόσβαση στο παλιό URL, ο presenter ανακατευθύνει αυτόματα στο νέο URL, οπότε οι μηχανές αναζήτησης δεν θα ευρετηριάσουν αυτές τις σελίδες δύο φορές (βλ. [#SEO και κανονικοποίηση]). - - -Δυναμική δρομολόγηση με callbacks ---------------------------------- - -Η δυναμική δρομολόγηση με callbacks σας επιτρέπει να αντιστοιχίσετε απευθείας συναρτήσεις (callbacks) στις routes, οι οποίες εκτελούνται όταν επισκέπτεστε τη συγκεκριμένη διαδρομή. Αυτή η ευέλικτη λειτουργικότητα σας επιτρέπει να δημιουργείτε γρήγορα και αποτελεσματικά διάφορα τελικά σημεία (endpoints) για την εφαρμογή σας: - -```php -$router->addRoute('test', function () { - echo 'βρίσκεστε στη διεύθυνση /test'; -}); -``` - -Μπορείτε επίσης να ορίσετε παραμέτρους στη μάσκα, οι οποίες θα μεταβιβαστούν αυτόματα στο callback σας: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Καλώς ήρθατε στην τσέχικη έκδοση του ιστότοπού μας!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - - -Modules -------- - -Αν έχουμε πολλαπλές routes που ανήκουν σε ένα κοινό [module |directory-structure#Presenters και Πρότυπα], χρησιμοποιούμε το `withModule()`: - -```php -$router = new RouteList; -$router->withModule('Forum') // οι ακόλουθες routes είναι μέρος του module Forum - ->addRoute('rss', 'Feed:rss') // ο presenter θα είναι Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // οι ακόλουθες routes είναι μέρος του module Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Μια εναλλακτική είναι η χρήση της παραμέτρου `module`: - -```php -// Το URL manage/dashboard/default αντιστοιχεί στον presenter Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - - -Υποτομείς ---------- - -Μπορούμε να χωρίσουμε τις συλλογές routes ανάλογα με τους υποτομείς: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -Στο όνομα του τομέα, μπορείτε επίσης να χρησιμοποιήσετε [#Χαρακτήρες μπαλαντέρ]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Πρόθεμα διαδρομής ------------------ - -Μπορούμε να χωρίσουμε τις συλλογές routes ανάλογα με τη διαδρομή στο URL: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // πιάνει το URL /eshop/rss - ->addRoute('/'); // πιάνει το URL /eshop// -``` - - -Συνδυασμός ----------- - -Μπορούμε να συνδυάσουμε τις παραπάνω διαρθρώσεις μεταξύ τους: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Παράμετροι Query ----------------- - -Οι μάσκες μπορούν επίσης να περιέχουν παραμέτρους query (παραμέτρους μετά το ερωτηματικό στο URL). Δεν μπορεί να οριστεί γι' αυτές κανονική έκφραση επικύρωσης, αλλά μπορεί να αλλάξει το όνομα με το οποίο μεταβιβάζονται στον presenter: - -```php -// θέλουμε να χρησιμοποιήσουμε την παράμετρο query 'cat' στην εφαρμογή με το όνομα 'categoryId' -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Παράμετροι Foo --------------- - -Τώρα πηγαίνουμε βαθύτερα. Οι παράμετροι Foo είναι ουσιαστικά ανώνυμες παράμετροι που επιτρέπουν την αντιστοίχιση μιας κανονικής έκφρασης. Παράδειγμα είναι μια route που δέχεται `/index`, `/index.html`, `/index.htm` και `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -Μπορείτε επίσης να ορίσετε ρητά τη συμβολοσειρά που θα χρησιμοποιηθεί κατά τη δημιουργία του URL. Η συμβολοσειρά πρέπει να τοποθετηθεί αμέσως μετά το ερωτηματικό. Η ακόλουθη route είναι παρόμοια με την προηγούμενη, αλλά δημιουργεί `/index.html` αντί για `/index`, επειδή η συμβολοσειρά `.html` έχει οριστεί ως τιμή δημιουργίας: - -```php -$router->addRoute('index', /* ... */); -``` - - -Ενσωμάτωση στην εφαρμογή -======================== - -Για να ενσωματώσουμε τον δημιουργημένο router στην εφαρμογή, πρέπει να ενημερώσουμε το DI container γι' αυτόν. Ο ευκολότερος τρόπος είναι να προετοιμάσουμε ένα factory που θα παράγει το αντικείμενο του router και να πούμε στη διαμόρφωση του container ότι πρέπει να το χρησιμοποιήσει. Ας υποθέσουμε ότι γι' αυτόν τον σκοπό γράφουμε τη μέθοδο `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Στη [διαμόρφωση |dependency-injection:services] στη συνέχεια γράφουμε: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Οποιεσδήποτε εξαρτήσεις, για παράδειγμα από τη βάση δεδομένων κ.λπ., μεταβιβάζονται στην factory μέθοδο ως παράμετροί της χρησιμοποιώντας το [autowiring|dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Ένας πολύ απλούστερος router από τη συλλογή routes είναι ο [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Τον χρησιμοποιούμε όταν δεν έχουμε ιδιαίτερες απαιτήσεις για τη μορφή των URL, όταν δεν είναι διαθέσιμο το `mod_rewrite` (ή οι εναλλακτικές του) ή όταν δεν θέλουμε ακόμα να ασχοληθούμε με όμορφα URL. - -Δημιουργεί διευθύνσεις περίπου σε αυτή τη μορφή: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Η παράμετρος του κατασκευαστή του SimpleRouter είναι ο προεπιλεγμένος presenter & η action, στην οποία πρέπει να κατευθυνθεί, αν ανοίξουμε τη σελίδα χωρίς παραμέτρους, π.χ. `http://example.com/`. - -```php -// ο προεπιλεγμένος presenter θα είναι 'Home' και η action 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Συνιστούμε να ορίσετε τον SimpleRouter απευθείας στη [διαμόρφωση |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO και κανονικοποίηση -====================== - -Το framework συμβάλλει στο SEO (βελτιστοποίηση για μηχανές αναζήτησης) αποτρέποντας τη διπλή εμφάνιση περιεχομένου σε διαφορετικά URL. Αν υπάρχουν πολλαπλές διευθύνσεις που οδηγούν στον ίδιο στόχο, π.χ. `/index` και `/index.html`, το framework καθορίζει την πρώτη από αυτές ως την κύρια (κανονική) και ανακατευθύνει τις υπόλοιπες σε αυτήν χρησιμοποιώντας τον κωδικό HTTP 301. Χάρη σε αυτό, οι μηχανές αναζήτησης δεν ευρετηριάζουν τις σελίδες σας δύο φορές και δεν διασπούν το page rank τους. - -Αυτή η διαδικασία ονομάζεται κανονικοποίηση. Η κανονική διεύθυνση URL είναι αυτή που δημιουργείται από τον router, δηλαδή η πρώτη κατάλληλη route στη συλλογή χωρίς τη σημαία OneWay. Γι' αυτό στη συλλογή αναφέρουμε τις **κύριες routes πρώτες**. - -Η κανονικοποίηση εκτελείται από τον presenter, περισσότερα στο κεφάλαιο [κανονικοποίηση |presenters#Κανονικοποίηση]. - - -HTTPS -===== - -Για να μπορούμε να χρησιμοποιούμε το πρωτόκολλο HTTPS, είναι απαραίτητο να το ενεργοποιήσουμε στο hosting και να διαμορφώσουμε σωστά τον διακομιστή. - -Η ανακατεύθυνση ολόκληρου του ιστότοπου σε HTTPS πρέπει να ρυθμιστεί σε επίπεδο διακομιστή, για παράδειγμα, χρησιμοποιώντας το αρχείο .htaccess στον ριζικό κατάλογο της εφαρμογής μας, και αυτό με τον κωδικό HTTP 301. Η ρύθμιση μπορεί να διαφέρει ανάλογα με το hosting και μοιάζει περίπου έτσι: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Ο router δημιουργεί URL με το ίδιο πρωτόκολλο με το οποίο φορτώθηκε η σελίδα, οπότε τίποτα περισσότερο δεν χρειάζεται να ρυθμιστεί. - -Αν όμως εξαιρετικά χρειαζόμαστε διαφορετικές routes να εκτελούνται με διαφορετικά πρωτόκολλα, το αναφέρουμε στη μάσκα της route: - -```php -// Θα δημιουργήσει διεύθυνση με HTTP -$router->addRoute('http://%host%//', /* ... */); - -// Θα δημιουργήσει διεύθυνση με HTTPS -$router->addRoute('https://%host%//', /* ... */); -``` - - -Αποσφαλμάτωση του router -======================== - -Ο πίνακας δρομολόγησης που εμφανίζεται στη [Tracy Bar |tracy:] είναι ένα χρήσιμο βοήθημα που εμφανίζει τη λίστα των routes και επίσης τις παραμέτρους που απέκτησε ο router από το URL. - -Η πράσινη γραμμή με το σύμβολο ✓ αντιπροσωπεύει τη route που επεξεργάστηκε το τρέχον URL, με μπλε χρώμα και το σύμβολο ≈ επισημαίνονται οι routes που θα επεξεργάζονταν επίσης το URL αν η πράσινη δεν τις είχε προλάβει. Παρακάτω βλέπουμε τον τρέχοντα presenter & την action. - -[* routing-debugger.webp *] - -Ταυτόχρονα, αν συμβεί μια μη αναμενόμενη ανακατεύθυνση λόγω [κανονικοποίησης |#SEO και κανονικοποίηση], είναι χρήσιμο να κοιτάξετε τον πίνακα στη γραμμή *redirect*, όπου θα μάθετε πώς ο router κατανόησε αρχικά το URL και γιατί ανακατεύθυνε. - -.[note] -Κατά την αποσφαλμάτωση του router, συνιστούμε να ανοίξετε τα Developer Tools στο πρόγραμμα περιήγησης (Ctrl+Shift+I ή Cmd+Option+I) και στον πίνακα Network να απενεργοποιήσετε την cache, ώστε να μην αποθηκεύονται σε αυτήν οι ανακατευθύνσεις. - - -Απόδοση -======= - -Ο αριθμός των routes επηρεάζει την ταχύτητα του router. Ο αριθμός τους σίγουρα δεν θα πρέπει να υπερβαίνει μερικές δεκάδες. Αν ο ιστότοπός σας έχει πολύπλοκη δομή URL, μπορείτε να γράψετε έναν προσαρμοσμένο [#Προσαρμοσμένος router]. - -Αν ο router δεν έχει εξαρτήσεις, για παράδειγμα από τη βάση δεδομένων, και το factory του δεν δέχεται ορίσματα, μπορούμε να σειριοποιήσουμε τη συναρμολογημένη του μορφή απευθείας στο DI container και έτσι να επιταχύνουμε ελαφρώς την εφαρμογή. - -```neon -routing: - cache: true -``` - - -Προσαρμοσμένος router -===================== - -Οι παρακάτω γραμμές προορίζονται για πολύ προχωρημένους χρήστες. Μπορείτε να δημιουργήσετε τον δικό σας router και να τον ενσωματώσετε εντελώς φυσικά στη συλλογή των routes. Ο router είναι μια υλοποίηση του interface [api:Nette\Routing\Router] με δύο μεθόδους: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -Η μέθοδος `match` επεξεργάζεται το τρέχον αίτημα [$httpRequest |http:request], από το οποίο μπορείτε να λάβετε όχι μόνο το URL, αλλά και τις κεφαλίδες κ.λπ., σε έναν πίνακα που περιέχει το όνομα του presenter και τις παραμέτρους του. Αν δεν μπορεί να επεξεργαστεί το αίτημα, επιστρέφει null. Κατά την επεξεργασία του αιτήματος, πρέπει να επιστρέψουμε τουλάχιστον τον presenter και την action. Το όνομα του presenter είναι πλήρες και περιέχει και τυχόν modules: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -Η μέθοδος `constructUrl` αντίθετα συναρμολογεί από τον πίνακα παραμέτρων το τελικό απόλυτο URL. Γι' αυτό μπορεί να χρησιμοποιήσει πληροφορίες από την παράμετρο [`$refUrl`|api:Nette\Http\UrlScript], που είναι το τρέχον URL. - -Τον προσθέτετε στη συλλογή των routes χρησιμοποιώντας το `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Αυτόνομη χρήση -============== - -Με την αυτόνομη χρήση εννοούμε τη χρήση των δυνατοτήτων του router σε μια εφαρμογή που δεν χρησιμοποιεί το Nette Application και τους presenters. Ισχύουν γι' αυτόν σχεδόν όλα όσα δείξαμε σε αυτό το κεφάλαιο, με τις εξής διαφορές: - -- για συλλογές routes χρησιμοποιούμε την κλάση [api:Nette\Routing\RouteList] -- ως simple router την κλάση [api:Nette\Routing\SimpleRouter] -- επειδή δεν υπάρχει το ζεύγος `Presenter:action`, χρησιμοποιούμε την [#Εκτεταμένη σημειογραφία] - -Έτσι, ξανά δημιουργούμε μια μέθοδο που θα μας συναρμολογήσει τον router, π.χ.: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Αν χρησιμοποιείτε DI container, το οποίο συνιστούμε, προσθέτουμε ξανά τη μέθοδο στη διαμόρφωση και στη συνέχεια λαμβάνουμε τον router μαζί με το αίτημα HTTP από το container: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Ή δημιουργούμε τα αντικείμενα απευθείας: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Τώρα μένει μόνο να αφήσουμε τον router να δουλέψει: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // δεν βρέθηκε αντίστοιχη route, αποστολή σφάλματος 404 - exit; -} - -// επεξεργασία των ληφθέντων παραμέτρων -$controller = $params['controller']; -// ... -``` - -Και αντίστροφα, χρησιμοποιούμε τον router για να συναρμολογήσουμε έναν σύνδεσμο: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/router}} diff --git a/application/el/templates.texy b/application/el/templates.texy deleted file mode 100644 index 41c922b8a1..0000000000 --- a/application/el/templates.texy +++ /dev/null @@ -1,323 +0,0 @@ -Πρότυπα -******* - -.[perex] -Το Nette χρησιμοποιεί το σύστημα προτύπων [Latte |latte:]. Αφενός επειδή είναι το πιο ασφαλές σύστημα προτύπων για PHP, και αφετέρου το πιο διαισθητικό σύστημα. Δεν χρειάζεται να μάθετε πολλά νέα πράγματα, αρκεί η γνώση της PHP και μερικών ετικετών. - -Είναι σύνηθες μια σελίδα να αποτελείται από ένα πρότυπο διάταξης + το πρότυπο της συγκεκριμένης action. Έτσι μπορεί να μοιάζει ένα πρότυπο διάταξης, παρατηρήστε τα μπλοκ `{block}` και την ετικέτα `{include}`: - -```latte - - - - {block title}My App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -Και αυτό θα είναι το πρότυπο της action: - -```latte -{block title}Homepage{/block} - -{block content} -

    Homepage

    -... -{/block} -``` - -Αυτό ορίζει το μπλοκ `content`, το οποίο εισάγεται στη θέση του `{include content}` στη διάταξη, και επίσης επαναπροσδιορίζει το μπλοκ `title`, το οποίο αντικαθιστά το `{block title}` στη διάταξη. Προσπαθήστε να φανταστείτε το αποτέλεσμα. - - -Αναζήτηση προτύπου ------------------- - -Δεν χρειάζεται να καθορίσετε στους presenters ποιο πρότυπο πρέπει να αποδοθεί, το framework θα βρει τη διαδρομή μόνο του και θα σας γλιτώσει από το γράψιμο. - -Αν χρησιμοποιείτε μια δομή καταλόγων όπου κάθε presenter έχει τον δικό του κατάλογο, απλά τοποθετήστε το πρότυπο σε αυτόν τον κατάλογο με το όνομα της action (ή του view), δηλαδή για την action `default` χρησιμοποιήστε το πρότυπο `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Αν χρησιμοποιείτε μια δομή όπου οι presenters βρίσκονται μαζί σε έναν κατάλογο και τα πρότυπα στον φάκελο `templates`, αποθηκεύστε το είτε στο αρχείο `..latte` είτε στο `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1η παραλλαγή - └── Home/ - └── default.latte ← 2η παραλλαγή -\-- - -Ο κατάλογος `templates` μπορεί επίσης να βρίσκεται ένα επίπεδο πιο πάνω, δηλαδή στο ίδιο επίπεδο με τον κατάλογο με τις κλάσεις των presenters. - -Αν το πρότυπο δεν βρεθεί, ο presenter απαντά με [σφάλμα 404 - η σελίδα δεν βρέθηκε |presenters#Σφάλμα 404 κ.λπ]. - -Μπορείτε να αλλάξετε το view χρησιμοποιώντας το `$this->setView('jineView')`. Μπορείτε επίσης να καθορίσετε απευθείας το αρχείο με το πρότυπο χρησιμοποιώντας το `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Τα αρχεία όπου αναζητούνται τα πρότυπα μπορούν να αλλάξουν αντικαθιστώντας τη μέθοδο [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], η οποία επιστρέφει έναν πίνακα πιθανών ονομάτων αρχείων. - - -Αναζήτηση προτύπου διάταξης ---------------------------- - -Το Nette αναζητά επίσης αυτόματα το αρχείο με τη διάταξη. - -Αν χρησιμοποιείτε μια δομή καταλόγων όπου κάθε presenter έχει τον δικό του κατάλογο, τοποθετήστε τη διάταξη είτε στον φάκελο με τον presenter, αν είναι συγκεκριμένη μόνο γι' αυτόν, είτε ένα επίπεδο πιο πάνω, αν είναι κοινή για πολλούς presenters: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← κοινή διάταξη - └── Home/ - ├── @layout.latte ← μόνο για τον presenter Home - ├── HomePresenter.php - └── default.latte -\-- - -Αν χρησιμοποιείτε μια δομή όπου οι presenters βρίσκονται μαζί σε έναν κατάλογο και τα πρότυπα στον φάκελο `templates`, η διάταξη θα αναμένεται σε αυτές τις θέσεις: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← κοινή διάταξη - ├── Home.@layout.latte ← μόνο για Home, 1η παραλλαγή - └── Home/ - └── @layout.latte ← μόνο για Home, 2η παραλλαγή -\-- - -Αν ο presenter βρίσκεται σε ένα module, η αναζήτηση θα γίνει και σε περαιτέρω επίπεδα καταλόγων, ανάλογα με την ένθεση του module. - -Το όνομα της διάταξης μπορεί να αλλάξει χρησιμοποιώντας το `$this->setLayout('layoutAdmin')` και τότε θα αναμένεται στο αρχείο `@layoutAdmin.latte`. Μπορείτε επίσης να καθορίσετε απευθείας το αρχείο με το πρότυπο διάταξης χρησιμοποιώντας το `$this->setLayout('/path/to/template.latte')`. - -Χρησιμοποιώντας το `$this->setLayout(false)` ή την ετικέτα `{layout none}` μέσα στο πρότυπο, η αναζήτηση διάταξης απενεργοποιείται. - -.[note] -Τα αρχεία όπου αναζητούνται τα πρότυπα διάταξης μπορούν να αλλάξουν αντικαθιστώντας τη μέθοδο [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], η οποία επιστρέφει έναν πίνακα πιθανών ονομάτων αρχείων. - - -Μεταβλητές στο πρότυπο ----------------------- - -Μεταβιβάζουμε μεταβλητές στο πρότυπο γράφοντάς τες στο `$this->template` και στη συνέχεια τις έχουμε διαθέσιμες στο πρότυπο ως τοπικές μεταβλητές: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Με αυτόν τον απλό τρόπο, μπορούμε να μεταβιβάσουμε οποιεσδήποτε μεταβλητές στα πρότυπα. Ωστόσο, κατά την ανάπτυξη στιβαρών εφαρμογών, είναι συνήθως πιο χρήσιμο να περιοριστούμε. Για παράδειγμα, ορίζοντας ρητά μια λίστα μεταβλητών που αναμένει το πρότυπο και τους τύπους τους. Χάρη σε αυτό, η PHP θα μπορεί να ελέγχει τους τύπους, το IDE να προτείνει σωστά και η στατική ανάλυση να εντοπίζει σφάλματα. - -Και πώς ορίζουμε μια τέτοια λίστα; Απλά με τη μορφή μιας κλάσης και των ιδιοτήτων της. Την ονομάζουμε παρόμοια με τον presenter, απλώς με το `Template` στο τέλος: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // και άλλες μεταβλητές -} -``` - -Το αντικείμενο `$this->template` στον presenter θα είναι τώρα μια παρουσία της κλάσης `ArticleTemplate`. Έτσι, η PHP θα ελέγχει τους δηλωμένους τύπους κατά την εγγραφή. Και από την έκδοση PHP 8.2, θα προειδοποιεί και για εγγραφή σε ανύπαρκτη μεταβλητή, σε προηγούμενες εκδόσεις το ίδιο μπορεί να επιτευχθεί χρησιμοποιώντας το trait [Nette\SmartObject |utils:smartobject]. - -Η σχολιαστική παρατήρηση `@property-read` προορίζεται για το IDE και τη στατική ανάλυση, χάρη σε αυτήν θα λειτουργεί η αυτόματη συμπλήρωση, βλ. "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Μπορείτε να απολαύσετε την πολυτέλεια της αυτόματης συμπλήρωσης και στα πρότυπα, αρκεί να εγκαταστήσετε το plugin για το Latte στο PhpStorm και να αναφέρετε στην αρχή του προτύπου το όνομα της κλάσης, περισσότερα στο άρθρο "Latte: πώς να χρησιμοποιήσετε το σύστημα τύπων":https://blog.nette.org/el/latte-how-to-use-type-system: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Έτσι λειτουργούν και τα πρότυπα στα components, αρκεί απλώς να τηρήσετε τη σύμβαση ονοματοδοσίας και για ένα component π.χ. `FifteenControl` να δημιουργήσετε μια κλάση προτύπου `FifteenTemplate`. - -Αν χρειαστεί να δημιουργήσετε το `$template` ως παρουσία μιας άλλης κλάσης, χρησιμοποιήστε τη μέθοδο `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Προεπιλεγμένες μεταβλητές -------------------------- - -Οι presenters και τα components μεταβιβάζουν αυτόματα αρκετές χρήσιμες μεταβλητές στα πρότυπα: - -- `$basePath` είναι η απόλυτη διαδρομή URL προς τον ριζικό κατάλογο (π.χ. `/eshop`) -- `$baseUrl` είναι η απόλυτη URL προς τον ριζικό κατάλογο (π.χ. `http://localhost/eshop`) -- `$user` είναι το αντικείμενο [που αντιπροσωπεύει τον χρήστη |security:authentication] -- `$presenter` είναι ο τρέχων presenter -- `$control` είναι το τρέχον component ή presenter -- `$flashes` πίνακας [μηνυμάτων |presenters#Flash μηνύματα] που στάλθηκαν από τη συνάρτηση `flashMessage()` - -Αν χρησιμοποιείτε τη δική σας κλάση προτύπου, αυτές οι μεταβλητές μεταβιβάζονται αν δημιουργήσετε μια ιδιότητα γι' αυτές. - - -Δημιουργία συνδέσμων --------------------- - -Στο πρότυπο, οι σύνδεσμοι προς άλλους presenters & actions δημιουργούνται με αυτόν τον τρόπο: - -```latte -λεπτομέρεια προϊόντος -``` - -Το attribute `n:href` είναι πολύ χρήσιμο για τις ετικέτες HTML ``. Αν θέλουμε να εμφανίσουμε έναν σύνδεσμο αλλού, για παράδειγμα σε κείμενο, χρησιμοποιούμε το `{link}`: - -```latte -Η διεύθυνση είναι: {link Home:default} -``` - -Περισσότερες πληροφορίες θα βρείτε στο κεφάλαιο [Δημιουργία συνδέσμων URL|creating-links]. - - -Προσαρμοσμένα φίλτρα, ετικέτες κ.λπ. ------------------------------------- - -Το σύστημα προτύπων Latte μπορεί να επεκταθεί με προσαρμοσμένα φίλτρα, συναρτήσεις, ετικέτες κ.λπ. Αυτό μπορεί να γίνει απευθείας στη μέθοδο `render` ή `beforeRender()`: - -```php -public function beforeRender(): void -{ - // προσθήκη φίλτρου - $this->template->addFilter('foo', /* ... */); - - // ή διαμορφώνουμε απευθείας το αντικείμενο Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -Το Latte στην έκδοση 3 προσφέρει έναν πιο προηγμένο τρόπο, δημιουργώντας μια [extension |latte:extending-latte#Latte Extension] για κάθε διαδικτυακό έργο. Ένα αποσπασματικό παράδειγμα μιας τέτοιας κλάσης: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Την καταχωρούμε χρησιμοποιώντας τη [διαμόρφωση |configuration#Templates Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Μετάφραση ---------- - -Αν προγραμματίζετε μια πολύγλωσση εφαρμογή, πιθανότατα θα χρειαστεί να εμφανίσετε ορισμένα κείμενα στο πρότυπο σε διαφορετικές γλώσσες. Το Nette Framework ορίζει γι' αυτόν τον σκοπό ένα interface για τη μετάφραση [api:Nette\Localization\Translator], το οποίο έχει μία μόνο μέθοδο `translate()`. Αυτή δέχεται το μήνυμα `$message`, το οποίο συνήθως είναι μια συμβολοσειρά, και οποιεσδήποτε άλλες παραμέτρους. Ο στόχος είναι να επιστρέψει τη μεταφρασμένη συμβολοσειρά. Στο Nette δεν υπάρχει προεπιλεγμένη υλοποίηση, μπορείτε να επιλέξετε ανάλογα με τις ανάγκες σας από πολλές έτοιμες λύσεις που θα βρείτε στο [Componette |https://componette.org/search/localization]. Στην τεκμηρίωσή τους θα μάθετε πώς να διαμορφώσετε τον translator. - -Στα πρότυπα μπορεί να οριστεί ένας μεταφραστής, τον οποίο [ζητάμε να μας περάσει |dependency-injection:passing-dependencies], με τη μέθοδο `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Ο Translator μπορεί εναλλακτικά να οριστεί χρησιμοποιώντας τη [διαμόρφωση |configuration#Templates Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Στη συνέχεια, ο μεταφραστής μπορεί να χρησιμοποιηθεί, για παράδειγμα, ως φίλτρο `|translate`, συμπεριλαμβανομένων των συμπληρωματικών παραμέτρων που μεταβιβάζονται στη μέθοδο `translate()` (βλ. `foo, bar`): - -```latte -{='Καλάθι'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Ή ως ετικέτα με κάτω παύλα: - -```latte -{_'Καλάθι'} -{_$item} -{_$item, foo, bar} -``` - -Για τη μετάφραση ενός τμήματος του προτύπου, υπάρχει η ζευγαρωτή ετικέτα `{translate}` (από το Latte 2.11, παλαιότερα χρησιμοποιούνταν η ετικέτα `{_}`): - -```latte -{translate}Παραγγελία{/translate} -{translate foo, bar}Παραγγελία{/translate} -``` - -Ο Translator καλείται κανονικά κατά το χρόνο εκτέλεσης κατά την απόδοση του προτύπου. Ωστόσο, το Latte έκδοση 3 μπορεί να μεταφράσει όλα τα στατικά κείμενα ήδη κατά τη μεταγλώττιση του προτύπου. Αυτό εξοικονομεί απόδοση, επειδή κάθε συμβολοσειρά μεταφράζεται μόνο μία φορά και η τελική μετάφραση γράφεται στη μεταγλωττισμένη μορφή. Έτσι, στον κατάλογο cache δημιουργούνται πολλαπλές μεταγλωττισμένες εκδόσεις του προτύπου, μία για κάθε γλώσσα. Γι' αυτό, αρκεί απλώς να αναφέρετε τη γλώσσα ως δεύτερη παράμετρο: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Στατικό κείμενο σημαίνει, για παράδειγμα, `{_'hello'}` ή `{translate}hello{/translate}`. Μη στατικά κείμενα, όπως για παράδειγμα `{_$foo}`, θα συνεχίσουν να μεταφράζονται κατά το χρόνο εκτέλεσης. diff --git a/application/hu/@home.texy b/application/hu/@home.texy deleted file mode 100644 index b9bc4c1a11..0000000000 --- a/application/hu/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -A Nette Application a Nette keretrendszer magja, amely hatékony eszközöket kínál modern webalkalmazások létrehozásához. Számos kivételes tulajdonságot kínál, amelyek jelentősen megkönnyítik a fejlesztést, és javítják a kód biztonságát és karbantarthatóságát. - - -Telepítés ---------- - -A könyvtárat a [Composer|best-practices:composer] eszközzel töltheti le és telepítheti: - -```shell -composer require nette/application -``` - - -Miért válassza a Nette Applicationt? ------------------------------------- - -A Nette mindig is úttörő volt a webes technológiák területén. - -**Kétirányú router:** A Nette fejlett router rendszerrel rendelkezik, amely kétirányúsága miatt egyedülálló - nemcsak az URL-eket fordítja le az alkalmazás akcióira, hanem visszafelé is képes URL-címeket generálni. Ez azt jelenti, hogy: -- Bármikor megváltoztathatja az egész alkalmazás URL-struktúráját anélkül, hogy a sablonokat módosítania kellene -- Az URL-ek automatikusan kanonizálódnak, ami javítja a SEO-t -- Az útválasztás egy helyen van definiálva, nem pedig szétszórva az annotációkban - -**Komponensek és szignálok:** A Delphi és a React.js által inspirált beépített komponensrendszer teljesen egyedülálló a PHP keretrendszerek között: -- Lehetővé teszi újrafelhasználható UI elemek létrehozását -- Támogatja a komponensek hierarchikus összeállítását -- Elegáns AJAX kérések kezelését kínálja szignálok segítségével -- Kész komponensek gazdag könyvtára a [Componette](https://componette.org) oldalon - -**AJAX és snippettek:** A Nette már 2009-ben forradalmi módszert vezetett be az AJAX-szal való munkára, jóval megelőzve az olyan hasonló megoldásokat, mint a Hotwire a Ruby on Railshez vagy a Symfony UX Turbo: -- A snippettek lehetővé teszik az oldal csak egyes részeinek frissítését JavaScript írása nélkül -- Automatikus integráció a komponensrendszerrel -- Oldalrészek intelligens érvénytelenítése -- Minimális mennyiségű továbbított adat - -**Intuitív [Latte|latte:] sablonok:** A legbiztonságosabb sablonrendszer PHP-hoz fejlett funkciókkal: -- Automatikus védelem XSS ellen kontextusérzékeny escapeléssel -- Bővíthetőség saját szűrőkkel, függvényekkel és tagekkel -- Sablon öröklődés és snippettek AJAX-hoz -- Kiváló PHP 8.x támogatás típusrendszerrel - -**Dependency Injection:** A Nette teljes mértékben kihasználja a Dependency Injectiont: -- Függőségek automatikus átadása (autowiring) -- Konfiguráció áttekinthető NEON formátumban -- Komponens factory-k támogatása - - -Fő előnyök ----------- - -- **Biztonság**: Automatikus védelem a [sebezhetőségekkel|nette:vulnerability-protection] szemben, mint az XSS, CSRF stb. -- **Termelékenység**: Kevesebb írás, több funkció az intelligens tervezésnek köszönhetően -- **Debuggolás**: [Tracy debugger|tracy:] útválasztó panellel -- **Teljesítmény**: Intelligens cache, komponensek lusta betöltése (lazy loading) -- **Rugalmasság**: Az URL-ek egyszerű módosítása az alkalmazás befejezése után is -- **Komponensek**: Egyedülálló újrafelhasználható UI elemek rendszere -- **Modern**: Teljes PHP 8.4+ és típusrendszer támogatás - - -Első lépések ------------- - -1. [Hogyan működnek az alkalmazások? |how-it-works] - Az alapvető architektúra megértése -2. [Presenterek |presenters] - Munka presenterekkel és akciókkal -3. [Sablonok |templates] - Sablonok készítése Latte-ban -4. [Route-ok |routing] - URL címek konfigurálása -5. [Interaktív komponensek |components] - A komponensrendszer kihasználása - - -PHP kompatibilitás ------------------- - -| verzió | kompatibilis PHP-vel -|-----------|------------------- -| 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 - -Az utolsó patch verzióra érvényes. diff --git a/application/hu/@left-menu.texy b/application/hu/@left-menu.texy deleted file mode 100644 index 1c42a160dd..0000000000 --- a/application/hu/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Hogyan működnek az alkalmazások? |how-it-works] -- [Bootstrapping] -- [Presenterek |presenters] -- [Sablonok |templates] -- [Könyvtárstruktúra |directory-structure] -- [Route-ok |routing] -- [URL linkek létrehozása |creating-links] -- [Interaktív komponensek |components] -- [AJAX & snippettek |ajax] -- [Multiplier |multiplier] -- [Konfiguráció |configuration] - - -További olvasmányok -******************* -- [Miért használjuk a Nette-t? |www:10-reasons-why-nette] -- [Telepítés |nette:installation] -- [Írjuk meg az első alkalmazásunkat! |quickstart:] -- [Útmutatók és eljárások |best-practices:] -- [Problémamegoldás |nette:troubleshooting] diff --git a/application/hu/@meta.texy b/application/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/application/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/application/hu/ajax.texy b/application/hu/ajax.texy deleted file mode 100644 index 27f9403885..0000000000 --- a/application/hu/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & Snippetek -**************** - -
    - -A modern webalkalmazások korában, ahol a funkcionalitás gyakran megoszlik a szerver és a böngésző között, az AJAX elengedhetetlen összekötő elem. Milyen lehetőségeket kínál nekünk a Nette Framework ezen a területen? -- sablonrészek, úgynevezett snippetek küldése -- változók átadása PHP és JavaScript között -- eszközök AJAX kérések debuggolásához - -
    - - -AJAX kérés -========== - -Az AJAX kérés alapvetően nem különbözik a klasszikus HTTP kéréstől. Meghív egy presentert bizonyos paraméterekkel. És a presenteren múlik, hogyan reagál a kérésre - visszaadhat adatokat JSON formátumban, küldhet HTML kód egy részét, XML dokumentumot stb. - -A böngésző oldalán az AJAX kérést a `fetch()` függvénnyel inicializáljuk: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // válasz feldolgozása -}); -``` - -Szerveroldalon az AJAX kérést a [HTTP kérést becsomagoló |http:request] szolgáltatás `$httpRequest->isAjax()` metódusával ismerjük fel. Az észleléshez a `X-Requested-With` HTTP fejlécet használja, ezért fontos elküldeni. A presenterben a `$this->isAjax()` metódus használható. - -Ha adatokat szeretne küldeni JSON formátumban, használja a [`sendJson()` |presenters#Válasz küldése] metódust. A metódus szintén befejezi a presenter működését. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Ha egy speciális, AJAX-hoz szánt sablonnal tervez válaszolni, a következőképpen teheti meg: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippetek -========= - -A Nette által kínált legerősebb eszköz a szerver és a kliens összekapcsolására a snippetek. Ezeknek köszönhetően egy átlagos alkalmazást minimális erőfeszítéssel és néhány sor kóddal AJAX-alapúvá alakíthat. Hogy mindez hogyan működik, azt a Fifteen példa demonstrálja, amelynek kódját a [GitHubon |https://github.com/nette-examples/fifteen] találja meg. - -A snippetek, vagyis kódrészletek, lehetővé teszik az oldal csak bizonyos részeinek frissítését, ahelyett, hogy az egész oldalt újra kellene tölteni. Ez nemcsak gyorsabb és hatékonyabb, hanem kényelmesebb felhasználói élményt is nyújt. A snippetek emlékeztethetnek a Hotwire for Ruby on Rails vagy a Symfony UX Turbo megoldásokra. Érdekesség, hogy a Nette már 14 évvel korábban bemutatta a snippeteket. - -Hogyan működnek a snippetek? Az oldal első betöltésekor (nem AJAX kérés esetén) az egész oldal betöltődik, beleértve az összes snippetet is. Amikor a felhasználó interakcióba lép az oldallal (pl. gombra kattint, űrlapot küld stb.), az egész oldal betöltése helyett egy AJAX kérés indul. A presenterben lévő kód végrehajtja a műveletet, és eldönti, mely snippeteket kell frissíteni. A Nette ezeket a snippeteket rendereli és JSON formátumú tömbként küldi el. A böngészőben lévő kezelő kód a kapott snippeteket visszailleszti az oldalba. Így csak a megváltozott snippetek kódja kerül átvitelre, ami sávszélességet takarít meg és gyorsítja a betöltést az egész oldal tartalmának átvitelével szemben. - - -Naja ----- - -A snippetek böngészőoldali kezelésére a [Naja könyvtár |https://naja.js.org] szolgál. Ezt [telepítse |https://naja.js.org/#/guide/01-install-setup-naja] node.js csomagként (Webpack, Rollup, Vite, Parcel és más alkalmazásokkal való használathoz): - -```shell -npm install naja -``` - -…vagy közvetlenül illessze be az oldal sablonjába: - -```latte - -``` - -Először is [inicializálni |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] kell a könyvtárat: - -```js -naja.initialize(); -``` - -Ahhoz, hogy egy egyszerű linkből (signal) vagy űrlapküldésből AJAX kérés legyen, elegendő a megfelelő linket, űrlapot vagy gombot `ajax` osztállyal megjelölni: - -```latte -Go - -
    - -
    - -vagy - -
    - -
    -``` - - -Snippetek újrarajzolása ------------------------ - -Minden [Control |components] osztályú objektum (beleértve magát a Presentert is) nyilvántartja, hogy történt-e olyan változás, amely az újrarajzolását igényli. Erre szolgál a `redrawControl()` metódus: - -```php -public function handleLogin(string $user): void -{ - // bejelentkezés után újra kell rajzolni a releváns részt - $this->redrawControl(); - // ... -} -``` - -A Nette még finomabb vezérlést tesz lehetővé afölött, hogy mit kell újrarajzolni. Az említett metódus ugyanis argumentumként fogadhatja a snippet nevét. Így lehet invalidálni (értsd: újrarajzolást kényszeríteni) a sablon részei szintjén. Ha az egész komponenst invalidáljuk, akkor annak minden snippetje is újrarajzolódik: - -```php -// invalidálja a 'header' snippetet -$this->redrawControl('header'); -``` - - -Snippetek a Latte-ban ---------------------- - -A snippetek használata a Latte-ban rendkívül egyszerű. Ha egy sablonrészt snippetként szeretne definiálni, egyszerűen csomagolja be `{snippet}` és `{/snippet}` tag-ekkel: - -```latte -{snippet header} -

    Hello ...

    -{/snippet} -``` - -A snippet létrehoz egy `
    ` elemet a HTML oldalon egy speciális, generált `id`-val. A snippet újrarajzolásakor ennek az elemnek a tartalma frissül. Ezért szükséges, hogy az oldal első renderelésekor az összes snippet is renderelődjön, még akkor is, ha esetleg kezdetben üresek. - -Létrehozhat snippetet `
    `-től eltérő elemmel is egy n:attribútum segítségével: - -```latte -
    -

    Hello ...

    -
    -``` - - -Snippet területek ------------------ - -A snippetek nevei kifejezések is lehetnek: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Így több snippet jön létre: `item-0`, `item-1` stb. Ha közvetlenül invalidálnánk egy dinamikus snippetet (például `item-1`), semmi sem rajzolódna újra. Ennek oka az, hogy a snippetek valóban kódrészletekként működnek, és csak közvetlenül önmaguk renderelődnek. Azonban a sablonban valójában nincs `item-1` nevű snippet. Az csak a snippet körüli kód, azaz a foreach ciklus végrehajtásakor jön létre. Ezért megjelöljük a sablon azon részét, amelyet végre kell hajtani a `{snippetArea}` tag segítségével: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -És újrarajzoltatjuk mind a snippetet magát, mind a teljes szülő területet: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Ugyanakkor célszerű biztosítani, hogy az `$items` tömb csak azokat az elemeket tartalmazza, amelyeket újra kell rajzolni. - -Ha a sablonba a `{include}` tag segítségével egy másik sablont illesztünk be, amely snippeteket tartalmaz, a sablon beillesztését ismét `snippetArea`-ba kell foglalni, és azt a snippettel együtt kell invalidálni: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippetek a komponensekben --------------------------- - -Snippeteket [komponensekben|components] is létrehozhat, és a Nette automatikusan újrarajzolja őket. De van egy korlátozás: a snippetek újrarajzolásához a `render()` metódust paraméterek nélkül hívja meg. Tehát a paraméterek átadása a sablonban nem fog működni: - -```latte -OK -{control productGrid} - -nem fog működni: -{control productGrid $arg, $arg} -{control productGrid:paginator} -``` - - -Felhasználói adatok küldése ---------------------------- - -A snippetekkel együtt tetszőleges további adatokat is küldhet a kliensnek. Egyszerűen írja be őket a `payload` objektumba: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Sikeres'; - } -} -``` - - -Paraméterek átadása -=================== - -Ha egy komponensnek AJAX kéréssel paramétereket küldünk, legyenek azok signal paraméterek vagy perzisztens paraméterek, a kérésnél meg kell adnunk a globális nevüket, amely tartalmazza a komponens nevét is. A paraméter teljes nevét a `getParameterId()` metódus adja vissza. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -És a handle metódus a megfelelő paraméterekkel a komponensben: - -```php -public function handleFoo(int $bar): void -{ -} -``` diff --git a/application/hu/bootstrapping.texy b/application/hu/bootstrapping.texy deleted file mode 100644 index 0d955175df..0000000000 --- a/application/hu/bootstrapping.texy +++ /dev/null @@ -1,297 +0,0 @@ -Bootstrapping -************* - -
    - -A bootstrapping az alkalmazás környezetének inicializálása, egy dependency injection (DI) konténer létrehozása és az alkalmazás elindítása. A következőkről fogunk beszélni: - -- hogyan inicializálja a Bootstrap osztály a környezetet -- hogyan konfigurálhatók az alkalmazások NEON fájlok használatával -- hogyan különböztessük meg a produkciós és fejlesztői módot -- hogyan hozzuk létre és konfiguráljuk a DI konténert - -
    - - -Az alkalmazások, legyenek azok webesek vagy parancssorból futtatott szkriptek, működésüket valamilyen környezet inicializálási formával kezdik. Régen ezt egy `include.inc.php` nevű fájl intézte, amelyet az elsődleges fájl inkludált. A modern Nette alkalmazásokban ezt a `Bootstrap` osztály váltotta fel, amelyet az alkalmazás részeként az `app/Bootstrap.php` fájlban találhat meg. Például így nézhet ki: - -```php -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // A Configurator felelős az alkalmazás környezetének és szolgáltatásainak beállításáért. - $this->configurator = new Configurator; - // Beállítja a Nette által generált ideiglenes fájlok (pl. fordított sablonok) könyvtárát - $this->configurator->setTempDirectory($this->rootDir . '/temp'); - } - - public function bootWebApplication(): Nette\DI\Container - { - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); - } - - private function initializeEnvironment(): void - { - // A Nette okos, és a fejlesztői mód automatikusan bekapcsolódik, - // vagy engedélyezheti egy adott IP-címre a következő sor kommentjének eltávolításával: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Aktiválja a Tracy-t: a végső "svájci bicska" a debuggoláshoz. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: automatikusan betölti az összes osztályt a kiválasztott könyvtárban - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Betölti a konfigurációs fájlokat - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -A webalkalmazások esetében az elsődleges fájl az `index.php`, amely a [nyilvános könyvtárban |directory-structure#Nyilvános könyvtár www] (`www/`) található. Ez a Bootstrap osztálytól kéri a környezet inicializálását és a DI konténer létrehozását. Ezután ebből szerzi be az `Application` szolgáltatást, amely elindítja a webalkalmazást: - -```php -$bootstrap = new App\Bootstrap; -// Környezet inicializálása + DI konténer létrehozása -$container = $bootstrap->bootWebApplication(); -// A DI konténer létrehozza a Nette\Application\Application objektumot -$application = $container->getByType(Nette\Application\Application::class); -// A Nette alkalmazás elindítása és a bejövő kérés feldolgozása -$application->run(); -``` - -Mint látható, a környezet beállításában és a dependency injection (DI) konténer létrehozásában a [api:Nette\Bootstrap\Configurator] osztály segít, amelyet most részletesebben bemutatunk. - - -Fejlesztői vs éles mód -====================== - -A Nette eltérően viselkedik attól függően, hogy fejlesztői vagy éles szerveren fut: - -🛠️ Fejlesztői mód (Development): - - Megjeleníti a Tracy debugbart hasznos információkkal (SQL lekérdezések, végrehajtási idő, felhasznált memória) - - Hiba esetén részletes hibaoldalt jelenít meg a függvényhívásokkal és a változók tartalmával - - Automatikusan frissíti a cache-t a Latte sablonok módosításakor, a konfigurációs fájlok szerkesztésekor stb. - - -🚀 Éles mód (Production): - - Nem jelenít meg semmilyen debuggolási információt, minden hibát a logba ír - - Hiba esetén az ErrorPresentert vagy egy általános "Server Error" oldalt jelenít meg - - A cache soha nem frissül automatikusan! - - Optimalizálva a sebességre és a biztonságra - - -A mód kiválasztása automatikus felismeréssel történik, így általában nincs szükség semmit konfigurálni vagy manuálisan átváltani: - -- fejlesztői mód: localhoston (IP-cím `127.0.0.1` vagy `::1`), ha nincs proxy (azaz annak HTTP fejléce) -- éles mód: mindenhol máshol - -Ha a fejlesztői módot más esetekben is engedélyezni szeretnénk, például egy adott IP-címről hozzáférő programozók számára, használjuk a `setDebugMode()` metódust: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // IP-címek tömbje is megadható -``` - -Határozottan javasoljuk az IP-cím és a cookie kombinálását. A `nette-debug` cookie-ba mentsünk el egy titkos tokent, pl. `secret1234`, és így aktiváljuk a fejlesztői módot az adott IP-címről hozzáférő és a cookie-ban említett tokennel rendelkező programozók számára: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -A fejlesztői módot teljesen ki is kapcsolhatjuk, még localhostra is: - -```php -$this->configurator->setDebugMode(false); -``` - -Figyelem, a `true` érték véglegesen bekapcsolja a fejlesztői módot, ami soha nem történhet meg éles szerveren. - - -Tracy debuggoló eszköz -====================== - -A könnyű debuggolás érdekében kapcsoljuk be a nagyszerű [Tracy |tracy:] eszközt. Fejlesztői módban vizualizálja a hibákat, éles módban pedig a hibákat a megadott könyvtárba logolja: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Ideiglenes fájlok -================= - -A Nette cache-t használ a DI konténerhez, a RobotLoaderhez, a sablonokhoz stb. Ezért szükséges beállítani annak a könyvtárnak az elérési útját, ahová a cache mentésre kerül: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Linuxon vagy macOS-en állítsa be a `log/` és `temp/` könyvtáraknak az [írási jogokat |nette:troubleshooting#Könyvtárjogosultságok beállítása]. - - -RobotLoader -=========== - -Általában szeretnénk automatikusan betölteni az osztályokat a [RobotLoader |robot-loader:] segítségével, ezért el kell indítanunk, és hagynunk kell, hogy betöltse az osztályokat abból a könyvtárból, ahol a `Bootstrap.php` található (azaz `__DIR__`), és az összes alkönyvtárából: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Alternatív megközelítés az osztályok betöltésének kizárólag a [Composer |best-practices:composer] segítségével történő engedélyezése a PSR-4 betartása mellett. - - -Időzóna -======= - -A konfigurátoron keresztül beállíthatja az alapértelmezett időzónát. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -DI konténer konfigurálása -========================= - -Az indítási folyamat része a DI konténer, vagyis az objektumgyár létrehozása, amely az egész alkalmazás szíve. Ez valójában egy PHP osztály, amelyet a Nette generál és a cache könyvtárba ment. A gyár gyártja az alkalmazás kulcsfontosságú objektumait, és a konfigurációs fájlok segítségével utasítjuk, hogyan hozza létre és állítsa be őket, ezzel befolyásolva az egész alkalmazás viselkedését. - -A konfigurációs fájlokat általában [NEON |neon:format] formátumban írják. Egy külön fejezetben olvashat arról, [mit lehet konfigurálni |nette:configuring]. - -.[tip] -Fejlesztői módban a konténer automatikusan frissül minden kód- vagy konfigurációs fájl módosításakor. Éles módban csak egyszer generálódik, és a változások a maximális teljesítmény érdekében nem kerülnek ellenőrzésre. - -A konfigurációs fájlokat a `addConfig()` segítségével töltjük be: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Ha több konfigurációs fájlt szeretnénk hozzáadni, többször is meghívhatjuk az `addConfig()` függvényt. - -```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'); -} -``` - -A `cli.php` név nem elírás, a konfiguráció PHP fájlban is megadható, amely tömbként adja vissza. - -További konfigurációs fájlokat is hozzáadhatunk az [`includes` szekcióban |dependency-injection:configuration#Fájlok beillesztése]. - -Ha a konfigurációs fájlokban azonos kulcsokkal rendelkező elemek jelennek meg, azok felülíródnak, vagy [tömbök esetén egyesülnek |dependency-injection:configuration#Összefésülés]. A később beillesztett fájlnak magasabb prioritása van, mint az előzőnek. Annak a fájlnak, amelyben az `includes` szekció szerepel, magasabb prioritása van, mint a benne inkludált fájloknak. - - -Statikus paraméterek --------------------- - -A konfigurációs fájlokban használt paramétereket definiálhatjuk [a `parameters` szekcióban |dependency-injection:configuration#Paraméterek], és átadhatjuk (vagy felülírhatjuk) az `addStaticParameters()` metódussal (van `addParameters()` aliasa is). Fontos, hogy a paraméterek különböző értékei további DI konténerek, azaz további osztályok generálását eredményezik. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -A `projectId` paraméterre a konfigurációban a szokásos `%projectId%` jelöléssel lehet hivatkozni. - - -Dinamikus paraméterek ---------------------- - -A konténerhez dinamikus paramétereket is hozzáadhatunk, amelyek különböző értékei, a statikus paraméterekkel ellentétben, nem okozzák új DI konténerek generálását. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Így egyszerűen hozzáadhatunk pl. környezeti változókat, amelyekre aztán a konfigurációban a `%env.variable%` jelöléssel lehet hivatkozni. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Alapértelmezett paraméterek ---------------------------- - -A konfigurációs fájlokban használhatja ezeket a statikus paramétereket: - -- `%appDir%` az abszolút elérési út a `Bootstrap.php` fájlt tartalmazó könyvtárhoz -- `%wwwDir%` az abszolút elérési út a `index.php` bemeneti fájlt tartalmazó könyvtárhoz -- `%tempDir%` az abszolút elérési út az ideiglenes fájlok könyvtárához -- `%vendorDir%` az abszolút elérési út ahhoz a könyvtárhoz, ahová a Composer telepíti a könyvtárakat -- `%rootDir%` az abszolút elérési út a projekt gyökérkönyvtárához -- `%debugMode%` jelzi, hogy az alkalmazás debug módban van-e -- `%consoleMode%` jelzi, hogy a kérés parancssorból érkezett-e - - -Importált szolgáltatások ------------------------- - -Most mélyebbre megyünk. Bár a DI konténer célja az objektumok gyártása, kivételesen szükség lehet egy meglévő objektum beillesztésére a konténerbe. Ezt úgy tehetjük meg, hogy a szolgáltatást `imported: true` jelzővel definiáljuk. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -És a bootstrapban beillesztjük az objektumot a konténerbe: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Eltérő környezet -================ - -Ne féljen módosítani a Bootstrap osztályt saját igényei szerint. A `bootWebApplication()` metódushoz hozzáadhat paramétereket a webprojektek megkülönböztetésére. Vagy kiegészíthetjük további metódusokkal, például `bootTestEnvironment()`, amely inicializálja a környezetet az egységtesztekhez, `bootConsoleApplication()` a parancssorból hívott szkriptekhez stb. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // Nette Tester inicializálása - $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/hu/components.texy b/application/hu/components.texy deleted file mode 100644 index 2b99e2c440..0000000000 --- a/application/hu/components.texy +++ /dev/null @@ -1,485 +0,0 @@ -Interaktív komponensek -********************** - -
    - -A komponensek önálló, újrafelhasználható objektumok, amelyeket oldalakba illesztünk be. Lehetnek űrlapok, datagrid-ek, szavazások, valójában bármi, amit érdemes ismételten használni. Megmutatjuk: - -- hogyan használjuk a komponenseket? -- hogyan írjunk komponenseket? -- mik azok a signálok? - -
    - -A Nette beépített komponensrendszerrel rendelkezik. Valami hasonlót a Delphi vagy az ASP.NET Web Forms ismerői ismerhetnek, valami távolról hasonlóra épül a React vagy a Vue.js is. Azonban a PHP keretrendszerek világában ez egyedülálló dolog. - -Eközben a komponensek alapvetően befolyásolják az alkalmazásfejlesztési megközelítést. Az oldalakat előre elkészített egységekből állíthatja össze. Szüksége van egy datagridre az adminisztrációban? Megtalálja a [Componette |https://componette.org/search/component] oldalon, amely a Nette nyílt forráskódú kiegészítőinek (tehát nem csak komponenseknek) a tárolója, és egyszerűen beillesztheti a presenterbe. - -A presenterbe tetszőleges számú komponenst beépíthet. És néhány komponensbe további komponenseket is beilleszthet. Így egy komponensfa jön létre, amelynek gyökere a presenter. - - -Factory metódusok -================= - -Hogyan illesztjük be és használjuk a komponenseket a presenterben? Általában factory metódusok segítségével. - -A komponens factory elegáns módja annak, hogy a komponenseket csak akkor hozzuk létre, amikor valóban szükség van rájuk (lazy / on demand). Az egész varázslat egy `createComponent()` nevű metódus implementálásában rejlik, ahol `` a létrehozandó komponens neve, és amely létrehozza és visszaadja a komponenst. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->item; - return $poll; - } -} -``` - -Annak köszönhetően, hogy minden komponens külön metódusban jön létre, a kód áttekinthetőbbé válik. - -.[note] -A komponensek nevei mindig kisbetűvel kezdődnek, annak ellenére, hogy a metódus nevében nagybetűvel íródnak. - -A factory-kat soha nem hívjuk meg közvetlenül, maguktól hívódnak meg, amikor először használjuk a komponenst. Ennek köszönhetően a komponens a megfelelő pillanatban jön létre, és csak akkor, ha valóban szükség van rá. Ha nem használjuk a komponenst (például egy AJAX kérésnél, amikor csak az oldal egy része kerül átvitelre, vagy a sablon cache-elésekor), egyáltalán nem jön létre, és megspóroljuk a szerver teljesítményét. - -```php .{file:DefaultPresenter.php} -// hozzáférünk a komponenshez, és ha ez volt az első alkalom, -// meghívódik a createComponentPoll(), amely létrehozza -$poll = $this->getComponent('poll'); -// alternatív szintaxis: $poll = $this['poll']; -``` - -A sablonban a komponenst a [{control} |#Renderelés] tag segítségével lehet renderelni. Ezért nincs szükség a komponensek manuális átadására a sablonnak. - -```latte -

    Szavazzon

    - -{control poll} -``` - - -Hollywood style -=============== - -A komponensek általában egy friss technikát használnak, amit szeretünk Hollywood style-nak nevezni. Biztosan ismeri a szállóigévé vált mondatot, amit a filmes meghallgatások résztvevői oly gyakran hallanak: „Ne hívjon minket, mi majd hívjuk önt”. És pontosan erről van szó. - -A Nette-ben ugyanis ahelyett, hogy állandóan kérdezgetnie kellene („elküldték az űrlapot?”, „érvényes volt?” vagy „megnyomta a felhasználó ezt a gombot?”), azt mondja a keretrendszernek, „amikor ez megtörténik, hívd meg ezt a metódust”, és a további munkát ráhagyja. Ha JavaScriptben programozik, ezt a programozási stílust jól ismeri. Olyan függvényeket ír, amelyek akkor hívódnak meg, amikor egy bizonyos esemény bekövetkezik. És a nyelv átadja nekik a megfelelő paramétereket. - -Ez teljesen megváltoztatja az alkalmazások írásáról alkotott képet. Minél több feladatot bízhat a keretrendszerre, annál kevesebb munkája van Önnek. És annál kevesebb dolgot hagyhat ki esetleg. - - -Komponens írása -=============== - -Komponens alatt általában a [api:Nette\Application\UI\Control] osztály leszármazottját értjük. (Pontosabb lenne tehát a „controls” kifejezést használni, de a „kontrolloknak” a magyarban teljesen más jelentése van, és inkább a „komponensek” terjedtek el.) Maga a presenter [api:Nette\Application\UI\Presenter] egyébként szintén a `Control` osztály leszármazottja. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Renderelés -========== - -Már tudjuk, hogy a komponens renderelésére a `{control componentName}` tag szolgál. Ez valójában a komponens `render()` metódusát hívja meg, amelyben gondoskodunk a renderelésről. Rendelkezésünkre áll, ugyanúgy, mint a presenterben, egy [Latte sablon|templates] a `$this->template` változóban, amelynek paramétereket adunk át. A presentertől eltérően itt meg kell adnunk a sablonfájlt, és hagynunk kell, hogy renderelje: - -```php .{file:PollControl.php} -public function render(): void -{ - // beillesztünk néhány paramétert a sablonba - $this->template->param = $value; - // és rendereljük - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -A `{control}` tag lehetővé teszi paraméterek átadását a `render()` metódusnak: - -```latte -{control poll $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Néha egy komponens több részből állhat, amelyeket külön szeretnénk renderelni. Mindegyikhez létrehozunk egy saját renderelő metódust, itt a példában például `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -És a sablonban ezt a következőképpen hívjuk meg: - -```latte -{control poll:paginator} -``` - -A jobb megértés érdekében jó tudni, hogyan fordítódik le ez a tag PHP-ra. - -```latte -{control poll} -{control poll:paginator 123, 'hello'} -``` - -lefordítva: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -A `getComponent()` metódus visszaadja a `poll` komponenst, és ezen a komponensen hívja meg a `render()` metódust, illetve a `renderPaginator()` metódust, ha a tag-ben a kettőspont után más renderelési mód van megadva. - -.[caution] -Figyelem, ha bárhol a paraméterek között **`=>`** jelenik meg, az összes paraméter egy tömbbe lesz csomagolva és az első argumentumként kerül átadásra: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -lefordítva: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Alkomponens renderelése: - -```latte -{control cartControl-someForm} -``` - -lefordítva: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -A komponensek, akárcsak a presenterek, automatikusan átadnak néhány hasznos változót a sablonoknak: - -- `$basePath` az abszolút URL elérési út a gyökérkönyvtárhoz (pl. `/eshop`) -- `$baseUrl` az abszolút URL a gyökérkönyvtárhoz (pl. `http://localhost/eshop`) -- `$user` a [felhasználót reprezentáló |security:authentication] objektum -- `$presenter` az aktuális presenter -- `$control` az aktuális komponens -- `$flashes` a `flashMessage()` függvénnyel küldött [üzenetek |#Flash üzenetek] tömbje - - -Signal -====== - -Már tudjuk, hogy a Nette alkalmazásban a navigáció linkekre vagy átirányításokra épül `Presenter:action` párokra. De mi van akkor, ha csak egy műveletet szeretnénk végrehajtani az **aktuális oldalon**? Például megváltoztatni az oszlopok sorrendjét egy táblázatban; törölni egy elemet; váltani világos/sötét mód között; elküldeni egy űrlapot; szavazni egy szavazáson; stb. - -Az ilyen típusú kéréseket signáloknak nevezzük. És ahogy az akciók a `action()` vagy `render()` metódusokat hívják meg, a signálok a `handle()` metódusokat hívják meg. Míg az akció (vagy view) fogalma tisztán csak a presenterekhez kapcsolódik, a signálok minden komponensre vonatkoznak. És így a presenterekre is, mivel az `UI\Presenter` az `UI\Control` leszármazottja. - -```php -public function handleClick(int $x, int $y): void -{ - // ... signál feldolgozása ... -} -``` - -A signált meghívó linket a szokásos módon hozzuk létre, azaz a sablonban az `n:href` attribútummal vagy a `{link}` taggel, a kódban pedig a `link()` metódussal. További információk az [URL linkek létrehozása |creating-links#Linkek signálhoz] fejezetben. - -```latte -kattints ide -``` - -A signál mindig az aktuális presenteren és action-ön hívódik meg, nem lehet másik presenteren vagy másik action-ön meghívni. - -A signál tehát az oldal újratöltését okozza, ugyanúgy, mint az eredeti kérésnél, csak emellett meghívja a signál kezelő metódusát a megfelelő paraméterekkel. Ha a metódus nem létezik, [api:Nette\Application\UI\BadSignalException] kivétel dobódik, amely a felhasználónak 403 Forbidden hibaoldalként jelenik meg. - - -Snippetek és AJAX -================= - -A signálok talán egy kicsit emlékeztetnek az AJAX-ra: handlerek, amelyek az aktuális oldalon hívódnak meg. És igaza van, a signálokat valóban gyakran AJAX segítségével hívják meg, és utána csak az oldal megváltozott részeit továbbítjuk a böngészőbe. Vagyis az ún. snippeteket. További információkat talál az [AJAX-nak szentelt oldalon |ajax]. - - -Flash üzenetek -============== - -A komponensnek saját flash üzenet tárolója van, amely független a presentertől. Ezek olyan üzenetek, amelyek pl. egy művelet eredményéről tájékoztatnak. A flash üzenetek fontos jellemzője, hogy a sablonban átirányítás után is elérhetők. Megjelenítésük után még további 30 másodpercig élnek – például arra az esetre, ha a felhasználó hibás átvitel miatt frissítené az oldalt - az üzenet tehát nem tűnik el azonnal. - -A küldést a [flashMessage |api:Nette\Application\UI\Control::flashMessage()] metódus végzi. Az első paraméter az üzenet szövege vagy egy `stdClass` objektum, amely az üzenetet reprezentálja. A nem kötelező második paraméter a típusa (error, warning, info stb.). A `flashMessage()` metódus visszaadja a flash üzenet példányát `stdClass` objektumként, amelyhez további információkat lehet hozzáadni. - -```php -$this->flashMessage('Az elem törölve lett.'); -$this->redirect(/* ... */); // és átirányítunk -``` - -A sablonban ezek az üzenetek a `$flashes` változóban érhetők el `stdClass` objektumokként, amelyek tartalmazzák a `message` (üzenet szövege), `type` (üzenet típusa) tulajdonságokat, és tartalmazhatják a már említett felhasználói információkat is. Például így rendereljük őket: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Átirányítás signál után -======================= - -A komponensek signáljának feldolgozása után gyakran átirányítás következik. Ez hasonló helyzet, mint az űrlapoknál - elküldésük után is átirányítunk, hogy a böngészőben az oldal frissítésekor ne küldődjenek újra az adatok. - -```php -$this->redirect('this') // átirányít az aktuális presenter-re és action-re -``` - -Mivel a komponens egy újrafelhasználható elem, és általában nem kellene, hogy közvetlen kapcsolata legyen konkrét presenterekkel, a `redirect()` és `link()` metódusok automatikusan komponens signálként értelmezik a paramétert: - -```php -$this->redirect('click') // átirányít ugyanazon komponens 'click' signáljára -``` - -Ha másik presenter-re vagy akcióra kell átirányítani, ezt a presenteren keresztül teheti meg: - -```php -$this->getPresenter()->redirect('Product:show'); // átirányít másik presenter/action-re -``` - - -Perzisztens paraméterek -======================= - -A perzisztens paraméterek a komponensek állapotának megőrzésére szolgálnak a különböző kérések között. Értékük ugyanaz marad a linkre kattintás után is. A session adatokkal ellentétben az URL-ben kerülnek átvitelre. És ez teljesen automatikusan történik, beleértve az ugyanazon az oldalon lévő más komponensekben létrehozott linkeket is. - -Például van egy komponensünk a tartalom lapozásához. Ilyen komponensekből több is lehet az oldalon. És azt szeretnénk, hogy egy linkre kattintás után minden komponens az aktuális oldalán maradjon. Ezért az oldalszámból (`page`) perzisztens paramétert csinálunk. - -Perzisztens paraméter létrehozása a Nette-ben rendkívül egyszerű. Csak létre kell hozni egy public property-t és megjelölni egy attribútummal: (korábban a `/** @persistent */` volt használatos) - -```php -use Nette\Application\Attributes\Persistent; // ez a sor fontos - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // public-nak kell lennie -} -``` - -A property-nél javasoljuk az adattípus megadását (pl. `int`), és megadhat alapértelmezett értéket is. A paraméterek értékeit lehet [validálni |#Perzisztens paraméterek validálása]. - -Link létrehozásakor a perzisztens paraméter értékét meg lehet változtatni: - -```latte -következő -``` - -Vagy *resetelhető*, azaz eltávolítható az URL-ből. Ekkor az alapértelmezett értékét veszi fel: - -```latte -reset -``` - - -Perzisztens komponensek -======================= - -Nemcsak a paraméterek, hanem a komponensek is lehetnek perzisztensek. Egy ilyen komponens perzisztens paraméterei átkerülnek a presenter különböző akciói között vagy több presenter között is. A perzisztens komponenseket annotációval jelöljük a presenter osztályánál. Például így jelöljük a `calendar` és `poll` komponenseket: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Az ezekben a komponensekben lévő alkomponenseket nem kell jelölni, azok is perzisztensekké válnak. - -PHP 8-ban attribútumokat is használhat a perzisztens komponensek jelölésére: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Komponensek függőségekkel -========================= - -Hogyan hozzunk létre komponenseket függőségekkel anélkül, hogy „beszennyeznénk” azokat a presentereket, amelyek használni fogják őket? A Nette DI konténerének okos tulajdonságainak köszönhetően, ugyanúgy, mint a klasszikus szolgáltatások használatakor, a munka nagy részét a keretrendszerre bízhatjuk. - -Vegyünk példaként egy komponenst, amelynek függősége van a `PollFacade` szolgáltatásra: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // Annak a szavazásnak az ID-ja, amelyhez komponenst hozunk létre - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Ha klasszikus szolgáltatást írnánk, nem lenne mit megoldani. Az összes függőség átadásáról láthatatlanul gondoskodna a DI konténer. De a komponensekkel általában úgy bánunk, hogy új példányukat közvetlenül a presenterben hozzuk létre a [factory metódusokban |#Factory metódusok] `createComponent…()`. De az összes komponens összes függőségét átadni a presenternek, hogy aztán átadjuk a komponenseknek, nehézkes. És mennyi írott kód… - -A logikus kérdés az, hogy miért nem regisztráljuk egyszerűen a komponenst klasszikus szolgáltatásként, adjuk át a presenternek, majd a `createComponent…()` metódusban adjuk vissza? Ez a megközelítés azonban nem megfelelő, mert a komponenst akár többször is szeretnénk létrehozni. - -A helyes megoldás egy factory írása a komponenshez, azaz egy osztály, amely létrehozza nekünk a komponenst: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Így regisztráljuk a factory-t a konténerünkbe a konfigurációban: - -```neon -services: - - PollControlFactory -``` - -és végül használjuk a presenterünkben: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // átadhatjuk a paraméterünket - return $this->pollControlFactory->create($pollId); - } -} -``` - -Nagyszerű, hogy a Nette DI ilyen egyszerű factory-kat tud [generálni |dependency-injection:factory], így a teljes kód helyett elegendő csak az interfészét megírni: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -És ez minden. A Nette belsőleg implementálja ezt az interfészt és átadja a presenternek, ahol már használhatjuk is. Mágikusan hozzáadja a komponensünkhöz az `$id` paramétert és a `PollFacade` osztály példányát is. - - -Komponensek mélységében -======================= - -A Nette Application komponensei újrafelhasználható részei a webalkalmazásnak, amelyeket oldalakba illesztünk, és amelyekkel egyébként ez az egész fejezet foglalkozik. Milyen képességekkel rendelkezik pontosan egy ilyen komponens? - -1) renderelhető a sablonban -2) tudja, [melyik részét |ajax#Snippetek] kell renderelni AJAX kérés esetén (snippetek) -3) képes az állapotát az URL-ben tárolni (perzisztens paraméterek) -4) képes reagálni a felhasználói műveletekre (signálok) -5) hierarchikus struktúrát hoz létre (ahol a gyökér a presenter) - -Ezeknek a funkcióknak mindegyikét az öröklési lánc valamelyik osztálya látja el. A renderelést (1 + 2) a [api:Nette\Application\UI\Control] osztály intézi, az [életciklusba |presenters#Presenter életciklusa] való beilleszkedést (3, 4) a [api:Nette\Application\UI\Component] osztály, a hierachikus struktúra létrehozását (5) pedig a [Container és Component |component-model:] osztályok: - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Komponens életciklusa ---------------------- - -[* lifecycle-component.svg *] *** *Komponens életciklusa* .<> - - -Perzisztens paraméterek validálása ----------------------------------- - -Az URL-ből kapott [#perzisztens paraméterek] értékeit a `loadState()` metódus írja be a property-kbe. Ez ellenőrzi azt is, hogy megfelelnek-e a property-nél megadott adattípusnak, különben 404-es hibával válaszol, és az oldal nem jelenik meg. - -Soha ne bízzon vakon a perzisztens paraméterekben, mert azokat a felhasználó könnyen felülírhatja az URL-ben. Így például ellenőrizzük, hogy az oldalszám `$this->page` nagyobb-e 0-nál. Megfelelő módszer az említett `loadState()` metódus felülírása: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // itt állítódik be a $this->page - // következik a saját értékellenőrzés: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Az ellenkező folyamatot, azaz az értékek összegyűjtését a perzisztens property-kből, a `saveState()` metódus végzi. - - -Signálok mélységében --------------------- - -A signál az oldal újratöltését okozza, ugyanúgy, mint az eredeti kérésnél (kivéve, ha AJAX-szal hívják), és meghívja a `signalReceived($signal)` metódust, amelynek alapértelmezett implementációja a `Nette\Application\UI\Component` osztályban megpróbál meghívni egy `handle{signal}` szavakból összetett metódust. A további feldolgozás az adott objektumon múlik. A `Component`-től öröklődő objektumok (azaz a `Control` és a `Presenter`) úgy reagálnak, hogy megpróbálják meghívni a `handle{signal}` metódust a megfelelő paraméterekkel. - -Más szavakkal: veszi a `handle{signal}` függvény definícióját és az összes paramétert, amely a kéréssel érkezett, és az argumentumokhoz név szerint hozzárendeli az URL paramétereit, majd megpróbálja meghívni az adott metódust. Például az `$id` paraméterként az URL `id` paraméterének értékét adja át, a `$something` paraméterként az URL `something` értékét adja át, stb. És ha a metódus nem létezik, a `signalReceived` metódus [kivételt |api:Nette\Application\UI\BadSignalException] dob. - -Signált bármely komponens, presenter vagy objektum fogadhat, amely implementálja a `SignalReceiver` interfészt és csatlakozik a komponensfához. - -A signálok fő fogadói a `Presenterek` és a `Control`-tól öröklődő vizuális komponensek lesznek. A signál jelzésként szolgál az objektum számára, hogy tegyen valamit – a szavazás számolja be a felhasználó szavazatát, a hírek blokkja bontakozzon ki és jelenítsen meg kétszer annyi hírt, az űrlap elküldésre került és dolgozza fel az adatokat, és így tovább. - -A signál URL-jét a [Component::link() |api:Nette\Application\UI\Component::link()] metódussal hozzuk létre. A `$destination` paraméterként adjuk át a `{signal}!` stringet, a `$args` paraméterként pedig az argumentumok tömbjét, amelyeket a signálnak szeretnénk átadni. A signál mindig az aktuális presenteren és action-ön hívódik meg az aktuális paraméterekkel, a signál paraméterei csak hozzáadódnak. Ezenkívül rögtön az elején hozzáadódik a **`?do` paraméter, amely meghatározza a signált**. - -Formátuma vagy `{signal}`, vagy `{signalReceiver}-{signal}`. A `{signalReceiver}` a komponens neve a presenterben. Ezért nem lehet kötőjel a komponens nevében – a komponens nevének és a signálnak az elválasztására szolgál, azonban így több komponenst is be lehet ágyazni. - -A [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] metódus ellenőrzi, hogy a komponens (első argumentum) a signál (második argumentum) fogadója-e. A második argumentumot elhagyhatjuk – ekkor azt vizsgálja, hogy a komponens bármilyen signál fogadója-e. Második paraméterként megadhatunk `true`-t, és ezzel ellenőrizhetjük, hogy nemcsak a megadott komponens a fogadó, hanem bármelyik leszármazottja is. - -Bármely, a `handle{signal}` előtti fázisban manuálisan végrehajthatjuk a signált a [processSignal()|api:Nette\Application\UI\Presenter::processSignal()] metódus meghívásával, amely gondoskodik a signál elintézéséről – veszi a signál fogadójaként meghatározott komponenst (ha nincs megadva signál fogadó, akkor maga a presenter az) és elküldi neki a signált. - -Példa: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Ezzel a signál idő előtt végrehajtódik, és nem fog újra meghívódni. diff --git a/application/hu/configuration.texy b/application/hu/configuration.texy deleted file mode 100644 index 7ac8952ebd..0000000000 --- a/application/hu/configuration.texy +++ /dev/null @@ -1,191 +0,0 @@ -Alkalmazások konfigurálása -************************** - -.[perex] -A Nette alkalmazások konfigurációs lehetőségeinek áttekintése. - - -Application -=========== - -```neon -application: - # megjelenjen a "Nette Application" panel a Tracy BlueScreen-en? - debugger: ... # (bool) alapértelmezett: true - - # hiba esetén meghívódjon az error-presenter? - # csak fejlesztői módban van hatása - catchExceptions: ... # (bool) alapértelmezett: true - - # az error-presenter neve - errorPresenter: Error # (string|array) alapértelmezett: 'Nette:Error' - - # aliasokat definiál presenterekhez és akciókhoz - aliases: ... - - # szabályokat definiál a presenter nevének osztályra való fordításához - mapping: ... - - # a hibás linkek nem generálnak figyelmeztetést? - # csak fejlesztői módban van hatása - silentLinks: ... # (bool) alapértelmezett: false -``` - -A `nette/application` 3.2-es verziójától kezdve definiálható egy error-presenter pár: - -```neon -application: - errorPresenter: - 4xx: Error4xx # Nette\Application\BadRequestException kivételhez - 5xx: Error5xx # egyéb kivételekhez -``` - -A `silentLinks` opció meghatározza, hogyan viselkedik a Nette fejlesztői módban, ha a link generálása sikertelen (például mert nem létezik a presenter stb.). Az alapértelmezett `false` érték azt jelenti, hogy a Nette `E_USER_WARNING` hibát dob. `true`-ra állítva ez a hibaüzenet elnyomásra kerül. Éles környezetben az `E_USER_WARNING` mindig kiváltódik. Ezt a viselkedést a presenter [$invalidLinkMode |creating-links#Érvénytelen linkek] változójának beállításával is befolyásolhatjuk. - -Az [Aliasok egyszerűsítik a hivatkozást |creating-links#Aliasok] a gyakran használt presenterekre. - -A [Mapping definiálja a szabályokat |directory-structure#Presenterek map-elése], amelyek alapján a presenter nevéből levezetődik az osztály neve. - - -Presenterek automatikus regisztrációja --------------------------------------- - -A Nette automatikusan hozzáadja a presentereket szolgáltatásként a DI konténerhez, ami jelentősen felgyorsítja azok létrehozását. A Nette presenterek felkutatásának módja konfigurálható: - -```neon -application: - # keresse a presentereket a Composer class map-ben? - scanComposer: ... # (bool) alapértelmezett: true - - # maszk, amelynek meg kell felelnie az osztály és a fájl nevének - scanFilter: ... # (string) alapértelmezett: '*Presenter' - - # mely könyvtárakban keresse a presentereket? - scanDirs: # (string[]|false) alapértelmezett: '%appDir%' - - %vendorDir%/mymodule -``` - -A `scanDirs`-ben megadott könyvtárak nem írják felül az alapértelmezett `%appDir%` értéket, hanem kiegészítik azt, így a `scanDirs` mindkét utat tartalmazni fogja: `%appDir%` és `%vendorDir%/mymodule`. Ha az alapértelmezett könyvtárat ki szeretnénk hagyni, használjuk a [felkiáltójelet |dependency-injection:configuration#Összefésülés], amely felülírja az értéket: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -A könyvtárak szkennelése kikapcsolható a false érték megadásával. Nem javasoljuk a presenterek automatikus hozzáadásának teljes elnyomását, mert ez csökkenti az alkalmazás teljesítményét. - - -Latte sablonok -============== - -Ezzel a beállítással globálisan befolyásolható a Latte viselkedése a komponensekben és presenterekben. - -```neon -latte: - # megjelenjen a Latte panel a Tracy Bar-ban a fő sablonhoz (true) vagy az összes komponenshez (all)? - debugger: ... # (true|false|'all') alapértelmezett: true - - # generál sablonokat declare(strict_types=1) fejléccel - strictTypes: ... # (bool) alapértelmezett: false - - # bekapcsolja a [szigorú parser |latte:develop#striktní režim] módot - strictParsing: ... # (bool) alapértelmezett: false - - # aktiválja a [generált kód ellenőrzését |latte:develop#Kontrola vygenerovaného kódu] - phpLinter: ... # (string) alapértelmezett: null - - # beállítja a locale-t - locale: cs_CZ # (string) alapértelmezett: null - - # a $this->template objektum osztálya - templateClass: App\MyTemplateClass # alapértelmezett: Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Ha a Latte 3-as verzióját használja, új [bővítményeket |latte:extending-latte#Latte Extension] adhat hozzá a következőkkel: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Ha a Latte 2-es verzióját használja, új tag-eket regisztrálhat akár az osztálynév megadásával, akár egy szolgáltatásra való hivatkozással. Alapértelmezés szerint az `install()` metódus hívódik meg, de ezt meg lehet változtatni egy másik metódus nevének megadásával: - -```neon -latte: - # egyéni Latte tag-ek regisztrálása - macros: - - App\MyLatteMacros::register # statikus metódus, classname vagy callable - - @App\MyLatteMacrosFactory # szolgáltatás install() metódussal - - @App\MyLatteMacrosFactory::register # szolgáltatás register() metódussal - -services: - - App\MyLatteMacrosFactory -``` - - -Routing -======= - -Alapbeállítások: - -```neon -routing: - # megjelenjen a routing panel a Tracy Bar-ban? - debugger: ... # (bool) alapértelmezett: true - - # szerializálja a routert a DI konténerbe - cache: ... # (bool) alapértelmezett: false -``` - -A routingot általában a [RouterFactory |routing#Route gyűjtemény] osztályban definiáljuk. Alternatívaként a route-okat a konfigurációban is definiálhatjuk `maszk: akció` párokkal, de ez a módszer nem kínál olyan széleskörű beállítási lehetőségeket: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Konstansok -========== - -PHP konstansok létrehozása. - -```neon -constants: - Foobar: 'baz' -``` - -Az alkalmazás indítása után létrejön a `Foobar` konstans. - -.[note] -A konstansok nem szolgálhatnak valamiféle globálisan elérhető változóként. Értékek objektumokba való átadásához használja a [dependency injectiont |dependency-injection:passing-dependencies]. - - -PHP -=== - -PHP direktívák beállítása. Az összes direktíva áttekintése megtalálható a [php.net |https://www.php.net/manual/en/ini.list.php] oldalon. - -```neon -php: - date.timezone: Europe/Prague -``` - - -DI szolgáltatások -================= - -Ezek a szolgáltatások kerülnek hozzáadásra a DI konténerhez: - -| Név | Típus | Leírás -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [az egész alkalmazás indítója |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] | egyes presenterek -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | `Latte\Engine` objektum factory-ja -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | factory a [`$this->template` |templates] számára diff --git a/application/hu/creating-links.texy b/application/hu/creating-links.texy deleted file mode 100644 index 9aedf6052c..0000000000 --- a/application/hu/creating-links.texy +++ /dev/null @@ -1,286 +0,0 @@ -URL linkek létrehozása -********************** - -
    - -Linkek létrehozása a Nette-ben egyszerű, mint az ujjal mutogatás. Csak rá kell mutatni, és a keretrendszer elvégzi az összes munkát Ön helyett. Megmutatjuk: - -- hogyan hozzunk létre linkeket sablonokban és máshol -- hogyan különböztessük meg az aktuális oldalra mutató linket -- mit tegyünk az érvénytelen linkekkel - -
    - - -Az [kétirányú routingnak |routing] köszönhetően soha nem kell majd keményen beírnia az alkalmazás URL-címeit a sablonokba vagy a kódba, amelyek később megváltozhatnak, vagy bonyolultan összeállítani őket. A linkben elegendő megadni a presentert és az akciót, átadni az esetleges paramétereket, és a keretrendszer maga generálja az URL-t. Valójában nagyon hasonlít egy függvényhívásra. Ez tetszeni fog Önnek. - - -A presenter sablonjában -======================= - -Leggyakrabban sablonokban hozunk létre linkeket, és nagyszerű segítő az `n:href` attribútum: - -```latte -részletek -``` - -Figyelje meg, hogy a HTML `href` attribútum helyett az [n:attribútumot |latte:syntax#n:attribútumok] `n:href` használtuk. Ennek értéke nem URL, ahogy az `href` attribútum esetében lenne, hanem a presenter és az akció neve. - -Egy linkre kattintás, leegyszerűsítve, olyan, mintha a `ProductPresenter::renderShow()` metódust hívnánk meg. És ha annak szignatúrájában paraméterek vannak, argumentumokkal hívhatjuk meg: - -```latte -termék részletei -``` - -Lehetőség van elnevezett paraméterek átadására is. A következő link a `lang` paramétert adja át `cs` értékkel: - -```latte -termék részletei -``` - -Ha a `ProductPresenter::renderShow()` metódusnak nincs `$lang` a szignatúrájában, a paraméter értékét a `$lang = $this->getParameter('lang')` segítségével vagy a [property-ből |presenters#Kérés paraméterei] tudhatja meg. - -Ha a paraméterek tömbben vannak tárolva, kibonthatók a `...` operátorral (Latte 2.x-ben az `(expand)` operátorral): - -```latte -{var $args = [$product->id, lang => cs]} -termék részletei -``` - -A linkekben automatikusan átadódnak az ún. [perzisztens paraméterek |presenters#Perzisztens paraméterek] is. - -Az `n:href` attribútum nagyon praktikus a HTML `` tag-ekhez. Ha máshol szeretnénk kiírni a linket, például szövegben, használjuk a `{link}`-et: - -```latte -A cím: {link Home:default} -``` - - -A kódban -======== - -Link létrehozásához a presenterben a `link()` metódus szolgál: - -```php -$url = $this->link('Product:show', $product->id); -``` - -A paramétereket tömb segítségével is át lehet adni, ahol elnevezett paramétereket is meg lehet adni: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); -``` - -Linkeket presenter nélkül is lehet létrehozni, erre való a [#LinkGenerator] és annak `link()` metódusa. - - -Linkek presenterhez -=================== - -Ha a link célja egy presenter és egy akció, akkor a szintaxisa a következő: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -A formátumot minden Latte tag és minden presenter metódus támogatja, amely linkekkel dolgozik, tehát `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` és a [#LinkGenerator] is. Tehát még ha a példákban `n:href` szerepel is, bármelyik függvény lehetne ott. - -Az alapforma tehát `Presenter:action`: - -```latte -kezdőlap -``` - -Ha az aktuális presenter akciójára hivatkozunk, kihagyhatjuk a nevét: - -```latte -kezdőlap -``` - -Ha a cél a `default` akció, kihagyhatjuk, de a kettőspontnak maradnia kell: - -```latte -kezdőlap -``` - -A linkek más [modulokba |directory-structure#Presenterek és sablonok] is mutathatnak. Itt a linkeket megkülönböztetjük relatívakra egy beágyazott almodulba, vagy abszolútakra. Az elv analóg a lemezen lévő elérési utakkal, csak perjelek helyett kettőspontok vannak. Tegyük fel, hogy az aktuális presenter a `Front` modul része, akkor így írjuk: - -```latte -link a Front:Shop:Product:show-ra -link az Admin:Product:show-ra -``` - -Speciális eset a [saját magára mutató |#Link az aktuális oldalra] link, amikor célként a `this`-t adjuk meg. - -```latte -frissítés -``` - -Hivatkozhatunk az oldal egy bizonyos részére az ún. fragment segítségével a kettőskereszt `#` jel után: - -```latte -link a Home:default-ra és a #main fragmentre -``` - - -Abszolút utak -============= - -A `link()` vagy `n:href` segítségével generált linkek mindig abszolút utak (azaz `/` jellel kezdődnek), de nem abszolút URL-ek protokollal és domainnel, mint `https://domain`. - -Abszolút URL generálásához adjon hozzá két perjelet az elejére (pl. `n:href="//Home:"`). Vagy átkapcsolhatja a presentert, hogy csak abszolút linkeket generáljon a `$this->absoluteUrls = true` beállításával. - - -Link az aktuális oldalra -======================== - -A `this` cél linket hoz létre az aktuális oldalra: - -```latte -frissítés -``` - -Ugyanakkor átadódnak az összes paraméter, amelyek a `action()` vagy `render()` metódus szignatúrájában szerepelnek, ha az `action()` nincs definiálva. Tehát ha a `Product:show` oldalon vagyunk és `id: 123`, a `this`-re mutató link ezt a paramétert is átadja. - -Természetesen a paramétereket közvetlenül is meg lehet adni: - -```latte -frissítés -``` - -Az `isLinkCurrent()` függvény ellenőrzi, hogy a link célja megegyezik-e az aktuális oldallal. Ezt például a sablonban lehet használni a linkek megkülönböztetésére stb. - -A paraméterek ugyanazok, mint a `link()` metódusnál, de ezen felül lehetőség van egy konkrét akció helyett a `*` helyettesítő karakter megadására, amely az adott presenter bármely akcióját jelenti. - -```latte -{if !isLinkCurrent('Admin:login')} - Jelentkezzen be -{/if} - -
  • - ... -
  • -``` - -Az `n:href`-fel kombinálva egy elemen belül használható a rövidített forma: - -```latte -... -``` - -A `*` helyettesítő karakter csak az akció helyett használható, a presenter helyett nem. - -Annak megállapítására, hogy egy adott modulban vagy annak almoduljában vagyunk-e, használjuk az `isModuleCurrent(moduleName)` metódust. - -```latte -
  • - ... -
  • -``` - - -Linkek signálhoz -================ - -A link célja nemcsak presenter és akció lehet, hanem [signál |components#Signal] is (ezek a `handle()` metódust hívják). Ekkor a szintaxis a következő: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -A signált tehát a felkiáltójel különbözteti meg: - -```latte -signál -``` - -Lehet linket létrehozni egy alkomponens (vagy al-alkomponens) signáljára is: - -```latte -signál -``` - - -Linkek a komponensben -===================== - -Mivel a [komponensek|components] önálló, újrafelhasználható egységek, amelyeknek nem kellene semmilyen kapcsolatban állniuk a környező presenterekkel, a linkek itt egy kicsit másképp működnek. A Latte `n:href` attribútuma és a `{link}` tag, valamint a komponens metódusai, mint a `link()` és mások, a link célját **mindig signál névként** kezelik. Ezért még a felkiáltójelet sem kell megadni: - -```latte -signál, nem akció -``` - -Ha a komponens sablonjában presenterekre szeretnénk hivatkozni, használjuk a `{plink}` taget: - -```latte -kezdőlap -``` - -vagy a kódban - -```php -$this->getPresenter()->link('Home:default') -``` - - -Aliasok .{data-version:v3.2.2} -============================== - -Néha hasznos lehet egy könnyen megjegyezhető aliast rendelni egy Presenter:akció párhoz. Például a `Front:Home:default` kezdőlapot egyszerűen `home`-nak nevezni, vagy az `Admin:Dashboard:default`-ot `admin`-nak. - -Az aliasokat a [konfigurációban|configuration] definiáljuk az `application › aliases` kulcs alatt: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -A linkekben ezután a kukac jellel írjuk őket, például: - -```latte -adminisztráció -``` - -Támogatottak minden olyan metódusban is, amely linkekkel dolgozik, mint a `redirect()` és hasonlók. - - -Érvénytelen linkek -================== - -Előfordulhat, hogy érvénytelen linket hozunk létre - vagy azért, mert nem létező presenterhez vezet, vagy azért, mert több paramétert ad át, mint amennyit a célmetódus a szignatúrájában elfogad, vagy ha a célakcióhoz nem lehet URL-t generálni. Az érvénytelen linkek kezelését a `Presenter::$invalidLinkMode` statikus változó határozza meg. Ez a következő értékek kombinációját veheti fel (konstansok): - -- `Presenter::InvalidLinkSilent` - csendes mód, URL-ként a # karaktert adja vissza -- `Presenter::InvalidLinkWarning` - E_USER_WARNING figyelmeztetést dob, amely éles módban logolásra kerül, de nem szakítja meg a szkript futását -- `Presenter::InvalidLinkTextual` - vizuális figyelmeztetés, a hibát közvetlenül a linkbe írja -- `Presenter::InvalidLinkException` - InvalidLinkException kivételt dob - -Az alapértelmezett beállítás `InvalidLinkWarning` éles módban és `InvalidLinkWarning | InvalidLinkTextual` fejlesztői módban. Az `InvalidLinkWarning` éles környezetben nem szakítja meg a szkript futását, de a figyelmeztetés logolásra kerül. Fejlesztői környezetben a [Tracy |tracy:] elfogja és bluescreen-t jelenít meg. Az `InvalidLinkTextual` úgy működik, hogy URL-ként egy hibaüzenetet ad vissza, amely `#error:` karakterekkel kezdődik. Hogy az ilyen linkek első pillantásra észrevehetők legyenek, adjunk hozzá a CSS-hez: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Ha nem szeretnénk, hogy fejlesztői környezetben figyelmeztetések keletkezzenek, beállíthatjuk a csendes módot közvetlenül a [konfigurációban|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Hogyan hozzunk létre linkeket hasonló kényelemmel, mint a `link()` metódus, de presenter jelenléte nélkül? Erre való a [api:Nette\Application\LinkGenerator]. - -A LinkGenerator egy szolgáltatás, amelyet a konstruktoron keresztül kérhetünk, majd a `link()` metódusával hozhatunk létre linkeket. - -A presenterekkel szemben itt van egy különbség. A LinkGenerator minden linket rögtön abszolút URL-ként hoz létre. Továbbá nincs "aktuális presenter", így nem lehet célként csak az akció nevét megadni (`link('default')`) vagy relatív utakat megadni a modulokhoz. - -Az érvénytelen linkek mindig `Nette\Application\UI\InvalidLinkException`-t dobnak. diff --git a/application/hu/directory-structure.texy b/application/hu/directory-structure.texy deleted file mode 100644 index 4ee6a6ada3..0000000000 --- a/application/hu/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Alkalmazás könyvtárstruktúrája -****************************** - -
    - -Hogyan tervezzünk áttekinthető és skálázható könyvtárstruktúrát Nette Framework projektekhez? Megmutatjuk a bevált gyakorlatokat, amelyek segítenek a kód szervezésében. Megtudhatja: - -- hogyan **logikusan tagoljuk** az alkalmazást könyvtárakba -- hogyan tervezzük meg a struktúrát úgy, hogy **jól skálázódjon** a projekt növekedésével -- mik a **lehetséges alternatívák** és azok előnyei vagy hátrányai - -
    - - -Fontos megemlíteni, hogy maga a Nette Framework nem ragaszkodik semmilyen konkrét struktúrához. Úgy tervezték, hogy könnyen alkalmazkodjon bármilyen igényhez és preferenciához. - - -A projekt alapstruktúrája -========================= - -Bár a Nette Framework nem diktál semmilyen merev könyvtárstruktúrát, létezik egy bevált alapértelmezett elrendezés a [Web Project|https://github.com/nette/web-project] formájában: - -/--pre -web-project/ -├── app/ ← alkalmazás könyvtára -├── assets/ ← SCSS, JS fájlok, képek..., alternatívaként resources/ -├── bin/ ← parancssori szkriptek -├── config/ ← konfiguráció -├── log/ ← logolt hibák -├── temp/ ← ideiglenes fájlok, cache -├── tests/ ← tesztek -├── vendor/ ← Composer által telepített könyvtárak -└── www/ ← nyilvános könyvtár (document-root) -\-- - -Ezt a struktúrát tetszés szerint módosíthatja igényei szerint - a mappákat átnevezheti vagy áthelyezheti. Ezután csak a relatív elérési utakat kell módosítani a `Bootstrap.php` fájlban és esetleg a `composer.json`-ban. Semmi másra nincs szükség, nincs bonyolult újrakonfigurálás, nincs konstansok módosítása. A Nette okos automatikus felismeréssel rendelkezik, és automatikusan felismeri az alkalmazás helyét, beleértve annak URL alapját is. - - -Kódszervezési elvek -=================== - -Amikor először vizsgál meg egy új projektet, gyorsan eligazodnia kell benne. Képzelje el, hogy kibontja az `app/Model/` könyvtárat, és ezt a struktúrát látja: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -Ebből csak azt olvashatja ki, hogy a projekt valamilyen szolgáltatásokat, repository-kat és entitásokat használ. Az alkalmazás valódi céljáról semmit sem tud meg. - -Nézzünk meg egy másik megközelítést - **szervezés domainek szerint**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Itt más a helyzet - első pillantásra világos, hogy egy webáruházról van szó. Már maguk a könyvtárnevek is elárulják, mit tud az alkalmazás - fizetésekkel, rendelésekkel és termékekkel dolgozik. - -Az első megközelítés (szervezés osztálytípus szerint) a gyakorlatban számos problémát okoz: a logikailag összetartozó kód különböző mappákba van szétszórva, és ugrálnia kell közöttük. Ezért domainek szerint fogunk szervezni. - - -Névterek --------- - -Szokás, hogy a könyvtárstruktúra megfelel az alkalmazás névtereinek. Ez azt jelenti, hogy a fájlok fizikai elhelyezkedése megfelel a namespace-üknek. Például az `app/Model/Product/ProductRepository.php`-ban elhelyezett osztálynak `App\Model\Product` namespace-szel kellene rendelkeznie. Ez az elv segít a kódban való tájékozódásban és egyszerűsíti az autoloadingot. - - -Egyes vs többes szám a nevekben -------------------------------- - -Figyelje meg, hogy az alkalmazás fő könyvtárainál egyes számot használunk: `app`, `config`, `log`, `temp`, `www`. Ugyanígy az alkalmazáson belül is: `Model`, `Core`, `Presentation`. Ez azért van, mert mindegyik egy-egy összefüggő koncepciót képvisel. - -Hasonlóképpen például az `app/Model/Product` mindent reprezentál a termékekkel kapcsolatban. Nem nevezzük `Products`-nak, mert nem egy termékekkel teli mappa (akkor `nokia.php`, `samsung.php` fájlok lennének benne). Ez egy namespace, amely osztályokat tartalmaz a termékekkel való munkához - `ProductRepository.php`, `ProductService.php`. - -Az `app/Tasks` mappa többes számban van, mert önálló futtatható szkriptek készletét tartalmazza - `CleanupTask.php`, `ImportTask.php`. Mindegyik önálló egység. - -A következetesség érdekében javasoljuk a következők használatát: -- Egyes szám egy funkcionális egységet reprezentáló namespace-hez (még ha több entitással is dolgozik) -- Többes szám önálló egységek gyűjteményeihez -- Bizonytalanság esetén, vagy ha nem akar ezen gondolkodni, válassza az egyes számot - - -Nyilvános könyvtár `www/` -========================= - -Ez a könyvtár az egyetlen, amely a webről elérhető (ún. document-root). Gyakran találkozhat a `public/` névvel is a `www/` helyett - ez csak konvenció kérdése, és nincs hatással a funkcionalitásra. A könyvtár tartalmazza: -- Az alkalmazás [belépési pontját |bootstrapping#index.php] `index.php` -- A `.htaccess` fájlt mod_rewrite szabályokkal (Apache esetén) -- Statikus fájlokat (CSS, JavaScript, képek) -- Feltöltött fájlokat - -Az alkalmazás megfelelő biztonsága érdekében elengedhetetlen a [helyesen konfigurált document-root |nette:troubleshooting#Hogyan lehet megváltoztatni vagy eltávolítani a www könyvtárat az URL-ből]. - -.[note] -Soha ne helyezze ebbe a könyvtárba a `node_modules/` mappát - ez több ezer fájlt tartalmaz, amelyek futtathatók lehetnek, és nem kellene nyilvánosan elérhetőnek lenniük. - - -Alkalmazás könyvtára `app/` -=========================== - -Ez az alkalmazás kódjának fő könyvtára. Alapstruktúra: - -/--pre -app/ -├── Core/ ← infrastrukturális ügyek -├── Model/ ← üzleti logika -├── Presentation/ ← presenterek és sablonok -├── Tasks/ ← parancssori szkriptek -└── Bootstrap.php ← az alkalmazás indító osztálya -\-- - -A `Bootstrap.php` az [alkalmazás indító osztálya|bootstrapping], amely inicializálja a környezetet, betölti a konfigurációt és létrehozza a DI konténert. - -Most nézzük meg részletesebben az egyes alkönyvtárakat. - - -Presenterek és sablonok -======================= - -Az alkalmazás prezentációs része az `app/Presentation` könyvtárban található. Alternatíva a rövid `app/UI`. Ez a hely minden presenter, azok sablonjai és esetleges segédosztályai számára. - -Ezt a réteget domainek szerint szervezzük. Egy komplex projektben, amely kombinálja a webáruházat, a blogot és az API-t, a struktúra így nézne ki: - -/--pre -app/Presentation/ -├── Shop/ ← webáruház frontend -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← adminisztráció -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← API végpontok - └── V1/ -\-- - -Ezzel szemben egy egyszerű blog esetében a következő tagolást használnánk: - -/--pre -app/Presentation/ -├── Front/ ← web frontend -│ ├── Home/ -│ └── Post/ -├── Admin/ ← adminisztráció -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, sitemap-ek stb. -\-- - -A `Home/` vagy `Dashboard/` mappák presentereket és sablonokat tartalmaznak. A `Front/`, `Admin/` vagy `Api/` mappákat **moduloknak** nevezzük. Technikailag ezek átlagos könyvtárak, amelyek az alkalmazás logikai tagolására szolgálnak. - -Minden presenter mappa tartalmaz egy azonos nevű presentert és annak sablonjait. Például a `Dashboard/` mappa tartalmazza: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← sablon -\-- - -Ez a könyvtárstruktúra tükröződik az osztályok névtereiben. Például a `DashboardPresenter` az `App\Presentation\Admin\Dashboard` névtérben található (lásd [#Presenterek map-elése]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Az `Admin` modulon belüli `Dashboard` presenterére az alkalmazásban kettőspontos jelöléssel hivatkozunk, mint `Admin:Dashboard`. Annak `default` akciójára pedig mint `Admin:Dashboard:default`. Beágyazott modulok esetén több kettőspontot használunk, például `Shop:Order:Detail:default`. - - -A struktúra rugalmas fejlesztése --------------------------------- - -Ennek a struktúrának az egyik nagy előnye, hogy milyen elegánsan alkalmazkodik a projekt növekvő igényeihez. Vegyük példaként az XML feedeket generáló részt. Kezdetben egyszerű formában van: - -/--pre -Export/ -├── ExportPresenter.php ← egy presenter minden exportáláshoz -├── sitemap.latte ← sablon a sitemaphoz -└── feed.latte ← sablon az RSS feedhez -\-- - -Idővel újabb feed típusok jelennek meg, és több logikára van szükségünk hozzájuk... Semmi probléma! Az `Export/` mappa egyszerűen modullá válik: - -/--pre -Export/ -├── Sitemap/ -│ ├── SitemapPresenter.php -│ └── sitemap.latte -└── Feed/ - ├── FeedPresenter.php - ├── zbozi.latte ← feed a Zboží.cz-hez - └── heureka.latte ← feed a Heureka.cz-hez -\-- - -Ez az átalakulás teljesen zökkenőmentes - csak új almappákat kell létrehozni, szétosztani bennük a kódot és frissíteni a linkeket (pl. `Export:feed`-ről `Export:Feed:zbozi`-ra). Ennek köszönhetően a struktúrát fokozatosan bővíthetjük igény szerint, a beágyazási szint nincs korlátozva. - -Ha például az adminisztrációban sok presenter van a rendelések kezelésével kapcsolatban, mint például `OrderDetail`, `OrderEdit`, `OrderDispatch` stb., akkor a jobb szervezettség érdekében ezen a ponton létrehozhat egy `Order` modult (mappát), amelyben a `Detail`, `Edit`, `Dispatch` és további presenterek (mappái) lesznek. - - -Sablonok elhelyezése --------------------- - -Az előző példákban láttuk, hogy a sablonok közvetlenül a presenter mappájában helyezkednek el: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← opcionális osztály a sablonhoz -└── default.latte ← sablon -\-- - -Ez az elhelyezés a gyakorlatban a legkényelmesebbnek bizonyul - minden kapcsolódó fájl kéznél van. - -Alternatívaként a sablonokat elhelyezheti a `templates/` almappába. A Nette mindkét változatot támogatja. Sőt, a sablonokat akár teljesen a `Presentation/` mappán kívül is elhelyezheti. A sablonok elhelyezési lehetőségeiről mindent megtalál a [Sablonok keresése |templates#Sablonok keresése] fejezetben. - - -Segédosztályok és komponensek ------------------------------ - -A presenterekhez és sablonokhoz gyakran tartoznak további segédfájlok is. Ezeket logikusan a hatókörük szerint helyezzük el: - -1. **Közvetlenül a presenter mellett**, ha az adott presenterhez specifikus komponensekről van szó: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← komponens a termékek listázásához -└── FilterForm.php ← űrlap a szűréshez -\-- - -2. **A modulhoz** - javasoljuk az `Accessory` mappa használatát, amely áttekinthetően az ábécé elején helyezkedik el: - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← komponensek a frontendhez -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **Az egész alkalmazáshoz** - a `Presentation/Accessory/`-ban: -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Vagy elhelyezheti a segédosztályokat, mint a `LatteExtension.php` vagy `TemplateFilters.php`, az infrastrukturális `app/Core/Latte/` mappába. És a komponenseket az `app/Components`-be. A választás a csapat szokásaitól függ. - - -Model - az alkalmazás szíve -=========================== - -A modell tartalmazza az alkalmazás összes üzleti logikáját. Szervezésére ismét az a szabály érvényes - domainek szerint strukturálunk: - -/--pre -app/Model/ -├── Payment/ ← minden a fizetésekkel kapcsolatban -│ ├── PaymentFacade.php ← fő belépési pont -│ ├── PaymentRepository.php -│ ├── Payment.php ← entitás -├── Order/ ← minden a rendelésekkel kapcsolatban -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← minden a szállítással kapcsolatban -\-- - -A modellben tipikusan ezekkel az osztálytípusokkal találkozhat: - -**Fasádok (Facades)**: az alkalmazás egy adott domainjének fő belépési pontját képviselik. Orchestrátorként működnek, amely koordinálja a különböző szolgáltatások közötti együttműködést a teljes use-case-ek (mint a "rendelés létrehozása" vagy "fizetés feldolgozása") implementálása érdekében. Az orchestrációs rétege alatt a fasád elrejti az implementációs részleteket az alkalmazás többi része elől, ezáltal tiszta interfészt biztosítva az adott domainnel való munkához. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // validáció - // rendelés létrehozása - // e-mail küldése - // statisztikákba írás - } -} -``` - -**Szolgáltatások (Services)**: egy specifikus üzleti műveletre összpontosítanak a domainen belül. Ellentétben a fasáddal, amely teljes use-case-eket orchestrál, a szolgáltatás egy konkrét üzleti logikát implementál (mint az árkalkulációk vagy a fizetések feldolgozása). A szolgáltatások tipikusan állapotmentesek, és használhatók akár fasádok által építőelemekként komplexebb műveletekhez, akár közvetlenül az alkalmazás más részei által egyszerűbb feladatokhoz. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // árkalkuláció - } -} -``` - -**Repository-k**: biztosítják az összes kommunikációt az adattárolóval, tipikusan adatbázissal. Feladata az entitások betöltése és mentése, valamint metódusok implementálása azok kereséséhez. A repository elszigeteli az alkalmazás többi részét az adatbázis implementációs részleteitől, és objektumorientált interfészt biztosít az adatokkal való munkához. - -```php -class OrderRepository -{ - public function find(int $id): ?Order - { - } - - public function findByCustomer(int $customerId): array - { - } -} -``` - -**Entitások**: objektumok, amelyek az alkalmazás fő üzleti koncepcióit reprezentálják, saját identitással rendelkeznek és idővel változnak. Tipikusan olyan osztályokról van szó, amelyeket adatbázis táblákra map-elnek ORM segítségével (mint a Nette Database Explorer vagy a Doctrine). Az entitások tartalmazhatnak üzleti szabályokat az adataikra vonatkozóan és validációs logikát. - -```php -// Az orders adatbázis táblára map-elt entitás -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 objektumok**: megváltoztathatatlan objektumok, amelyek értékeket reprezentálnak saját identitás nélkül - például pénzösszeg vagy e-mail cím. Két azonos értékű value objektum példány azonosnak tekintendő. - - -Infrastrukturális kód -===================== - -A `Core/` (vagy `Infrastructure/`) mappa az alkalmazás technikai alapjának otthona. Az infrastrukturális kód tipikusan tartalmazza: - -/--pre -app/Core/ -├── Router/ ← routing és URL menedzsment -│ └── RouterFactory.php -├── Security/ ← authentikáció és autorizáció -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← logolás és monitoring -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← cachovací réteg -│ └── FullPageCache.php -└── Integration/ ← integráció külső szolgáltatásokkal - ├── Slack/ - └── Stripe/ -\-- - -Kisebb projekteknél természetesen elegendő a lapos tagolás: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -Olyan kódról van szó, amely: - -- Technikai infrastruktúrát old meg (routing, logolás, cacholás) -- Külső szolgáltatásokat integrál (Sentry, Elasticsearch, Redis) -- Alapszolgáltatásokat nyújt az egész alkalmazás számára (mail, adatbázis) -- Többnyire független a konkrét domaintól - a cache vagy a logger ugyanúgy működik egy webáruház vagy egy blog esetében. - -Bizonytalan, hogy egy adott osztály ide vagy a modellbe tartozik-e? A kulcsfontosságú különbség az, hogy a `Core/`-ban lévő kód: - -- Nem tud semmit a domainről (termékek, rendelések, cikkek) -- Többnyire átvihető egy másik projektbe -- Azt oldja meg, "hogyan működik" (hogyan küldjön e-mailt), nem pedig azt, "mit csinál" (milyen e-mailt küldjön) - -Példa a jobb megértéshez: - -- `App\Core\MailerFactory` - létrehozza az e-mailek küldésére szolgáló osztály példányait, kezeli az SMTP beállításokat -- `App\Model\OrderMailer` - használja a `MailerFactory`-t a rendelésekkel kapcsolatos e-mailek küldésére, ismeri azok sablonjait és tudja, mikor kell elküldeni őket - - -Parancssori szkriptek -===================== - -Az alkalmazásoknak gyakran kell tevékenységeket végezniük a szokásos HTTP kéréseken kívül - legyen szó akár háttérbeli adatfeldolgozásról, karbantartásról, vagy időszakos feladatokról. Futtatásukra egyszerű szkriptek szolgálnak a `bin/` könyvtárban, magát az implementációs logikát pedig az `app/Tasks/` (esetleg `app/Commands/`) mappába helyezzük. - -Példa: - -/--pre -app/Tasks/ -├── Maintenance/ ← karbantartó szkriptek -│ ├── CleanupCommand.php ← régi adatok törlése -│ └── DbOptimizeCommand.php ← adatbázis optimalizálása -├── Integration/ ← integráció külső rendszerekkel -│ ├── ImportProducts.php ← import a beszállítói rendszerből -│ └── SyncOrders.php ← rendelések szinkronizálása -└── Scheduled/ ← rendszeres feladatok - ├── NewsletterCommand.php ← hírlevelek kiküldése - └── ReminderCommand.php ← értesítések az ügyfeleknek -\-- - -Mi tartozik a modellbe és mi a parancssori szkriptekbe? Például egyetlen e-mail elküldésének logikája a modell része, több ezer e-mail tömeges kiküldése már a `Tasks/`-ba tartozik. - -A feladatokat általában [parancssorból |https://blog.nette.org/en/cli-scripts-in-nette-application] vagy cron segítségével futtatjuk. HTTP kérésen keresztül is futtathatók, de gondolni kell a biztonságra. A feladatot elindító presentert védeni kell, például csak bejelentkezett felhasználók számára, vagy erős tokennel és hozzáféréssel engedélyezett IP-címekről. Hosszú feladatok esetén növelni kell a szkript időkorlátját és használni kell a `session_write_close()`-t, hogy ne záródjon le a session. - - -További lehetséges könyvtárak -============================= - -Az említett alapkönyvtárakon kívül a projekt igényei szerint további specializált mappákat is hozzáadhat. Nézzük meg a leggyakoribbakat és azok használatát: - -/--pre -app/ -├── Api/ ← API logika, amely független a prezentációs rétegtől -├── Database/ ← migrációs szkriptek és seederek tesztadatokhoz -├── Components/ ← megosztott vizuális komponensek az egész alkalmazásban -├── Event/ ← hasznos, ha event-driven architektúrát használ -├── Mail/ ← e-mail sablonok és kapcsolódó logika -└── Utils/ ← segédosztályok -\-- - -Az alkalmazásban használt megosztott vizuális komponensekhez használható az `app/Components` vagy `app/Controls` mappa: - -/--pre -app/Components/ -├── Form/ ← megosztott űrlap komponensek -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← komponensek adatlistázáshoz -│ └── DataGrid.php -└── Navigation/ ← navigációs elemek - ├── Breadcrumbs.php - └── Menu.php -\-- - -Ide tartoznak azok a komponensek, amelyek komplexebb logikával rendelkeznek. Ha komponenseket szeretne megosztani több projekt között, célszerű őket külön composer csomagba kivonni. - -Az `app/Mail` könyvtárba helyezheti az e-mail kommunikáció kezelését: - -/--pre -app/Mail/ -├── templates/ ← e-mail sablonok -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Presenterek map-elése -===================== - -A map-elés definiálja a szabályokat az osztály nevének levezetésére a presenter nevéből. Ezeket a [konfigurációban|configuration] adjuk meg az `application › mapping` kulcs alatt. - -Ezen az oldalon megmutattuk, hogy a presentereket az `app/Presentation` (esetleg `app/UI`) mappába helyezzük. Ezt a konvenciót közölnünk kell a Nette-vel a konfigurációs fájlban. Egyetlen sor elegendő: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Hogyan működik a map-elés? A jobb megértés érdekében először képzeljünk el egy alkalmazást modulok nélkül. Azt szeretnénk, hogy a presenter osztályok az `App\Presentation` névtérbe essenek, hogy a `Home` presenter az `App\Presentation\HomePresenter` osztályra map-eljen. Ezt ezzel a konfigurációval érjük el: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -A map-elés úgy működik, hogy a `Home` presenter neve helyettesíti a csillagot az `App\Presentation\*Presenter` maszkban, így kapjuk meg az `App\Presentation\HomePresenter` végső osztálynevet. Egyszerű! - -Ahogy azonban a példákban ebben és más fejezetekben látható, a presenter osztályokat azonos nevű alkönyvtárakba helyezzük, például a `Home` presenter az `App\Presentation\Home\HomePresenter` osztályra map-el. Ezt a kettőspont megduplázásával érjük el (Nette Application 3.2-t igényel): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Most térjünk át a presenterek modulokba való map-elésére. Minden modulhoz definiálhatunk specifikus map-elést: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -Ezen konfiguráció szerint a `Front:Home` presenter az `App\Presentation\Front\Home\HomePresenter` osztályra map-el, míg az `Api:OAuth` presenter az `App\Api\OAuthPresenter` osztályra. - -Mivel a `Front` és `Admin` modulok hasonló map-elési móddal rendelkeznek, és valószínűleg több ilyen modul lesz, létrehozható egy általános szabály, amely helyettesíti őket. Az osztály maszkjába így bekerül egy új csillag a modulhoz: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Ez mélyebben beágyazott könyvtárstruktúrák esetén is működik, mint például a `Admin:User:Edit` presenter, a csillaggal jelölt szegmens minden szinten megismétlődik, és az eredmény az `App\Presentation\Admin\User\Edit\EditPresenter` osztály. - -Alternatív jelölésként string helyett használhatunk egy három szegmensből álló tömböt. Ez a jelölés egyenértékű az előzővel: - -```neon -application: - mapping: - *: [App\Presentation, *, **Presenter] - Api: [App\Api, '', *Presenter] -``` diff --git a/application/hu/how-it-works.texy b/application/hu/how-it-works.texy deleted file mode 100644 index 83bb986af3..0000000000 --- a/application/hu/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Hogyan működnek az alkalmazások? -******************************** - -
    - -Éppen a Nette dokumentáció alapdokumentumát olvassa. Megtudhatja a webalkalmazások működésének teljes elvét. Szépen A-tól Z-ig, a születés pillanatától a PHP szkript utolsó lélegzetvételéig. Az olvasás után tudni fogja: - -- hogyan működik az egész -- mi az a Bootstrap, Presenter és DI konténer -- hogyan néz ki a könyvtárstruktúra - -
    - - -Könyvtárstruktúra -================= - -Nyissa meg a [WebProject|https://github.com/nette/web-project] nevű webalkalmazás skeleton példáját, és olvasás közben nézheti azokat a fájlokat, amelyekről szó van. - -A könyvtárstruktúra valahogy így néz ki: - -/--pre -web-project/ -├── app/ ← alkalmazás könyvtára -│ ├── Core/ ← a működéshez szükséges alaposztályok -│ │ └── RouterFactory.php ← URL címek konfigurációja -│ ├── Presentation/ ← presenterek, sablonok & társai -│ │ ├── @layout.latte ← layout sablon -│ │ └── Home/ ← Home presenter könyvtára -│ │ ├── HomePresenter.php ← Home presenter osztálya -│ │ └── default.latte ← default akció sablonja -│ └── Bootstrap.php ← Bootstrap indító osztály -├─ assets/ ← erőforrások (SCSS, TypeScript, forrásképek) -├── bin/ ← parancssorból futtatott szkriptek -├── config/ ← konfigurációs fájlok -│ ├── common.neon -│ └── services.neon -├── log/ ← naplózott hibák -├── temp/ ← ideiglenes fájlok, cache, … -├── vendor/ ← Composer által telepített könyvtárak -│ ├── ... -│ └── autoload.php ← az összes telepített csomag autoloadingja -├── www/ ← nyilvános könyvtár vagy a projekt document-rootja -│ ├──assets/ ← összeállított statikus fájlok (CSS, JS, képek, ...) -│ ├── .htaccess ← mod_rewrite szabályok -│ └── index.php ← elsődleges fájl, amellyel az alkalmazás elindul -└── .htaccess ← tiltja a hozzáférést minden könyvtárhoz a www kivételével -\-- - -A könyvtárstruktúrát tetszés szerint módosíthatja, a mappákat átnevezheti vagy áthelyezheti, teljesen rugalmas. A Nette ráadásul okos automatikus felismeréssel rendelkezik, és automatikusan felismeri az alkalmazás helyét, beleértve annak URL alapját is. - -Kicsit nagyobb alkalmazásoknál a presenterek és sablonok mappáit [alkönyvtárakba tagolhatjuk |directory-structure#Presenterek és sablonok], az osztályokat pedig névterekbe, amelyeket moduloknak nevezünk. - -A `www/` könyvtár az ún. nyilvános könyvtár vagy a projekt document-rootja. Átnevezheti anélkül, hogy bármit is be kellene állítania az alkalmazás oldalán. Csak a [hostingot kell konfigurálni |nette:troubleshooting#Hogyan lehet megváltoztatni vagy eltávolítani a www könyvtárat az URL-ből] úgy, hogy a document-root erre a könyvtárra mutasson. - -A WebProjectet közvetlenül is letöltheti a Nette-vel együtt a [Composer |best-practices:composer] segítségével: - -```shell -composer create-project nette/web-project -``` - -Linuxon vagy macOS-en állítsa be a `log/` és `temp/` könyvtáraknak az [írási jogokat |nette:troubleshooting#Könyvtárjogosultságok beállítása]. - -A WebProject alkalmazás készen áll a futtatásra, egyáltalán semmit nem kell konfigurálni, és azonnal megjelenítheti a böngészőben a `www/` mappához való hozzáféréssel. - - -HTTP kérés -========== - -Minden akkor kezdődik, amikor a felhasználó megnyit egy oldalt a böngészőben. Tehát amikor a böngésző bekopogtat a szerverhez egy HTTP kéréssel. A kérés egyetlen PHP fájlra irányul, amely a `www/` nyilvános könyvtárban található, és ez az `index.php`. Tegyük fel, hogy a kérés a `https://example.com/product/123` címre vonatkozik. A megfelelő [szerverbeállításnak |nette:troubleshooting#Hogyan állítsuk be a szervert a szép URL-ekhez] köszönhetően ez az URL is az `index.php` fájlra map-elődik, és az végrehajtódik. - -Feladata: - -1) inicializálni a környezetet -2) megszerezni a factory-t -3) elindítani a Nette alkalmazást, amely kezeli a kérést - -Milyen factory-t? Hiszen nem traktorokat gyártunk, hanem weboldalakat! Várjon, mindjárt megmagyarázzuk. - -A „környezet inicializálása” alatt például azt értjük, hogy aktiválódik a [Tracy|tracy:], ami egy csodálatos eszköz a naplózáshoz vagy a hibák vizualizálásához. Éles szerveren naplózza a hibákat, fejlesztői szerveren pedig rögtön megjeleníti. Tehát az inicializáláshoz tartozik annak eldöntése is, hogy a web éles vagy fejlesztői módban fut-e. Ehhez a Nette [okos automatikus felismerést |bootstrapping#Fejlesztői vs éles mód] használ: ha a webet localhoston futtatja, fejlesztői módban fut. Így semmit sem kell konfigurálnia, és az alkalmazás rögtön készen áll mind a fejlesztésre, mind az éles bevetésre. Ezek a lépések végrehajtódnak és részletesen le vannak írva a [Bootstrap osztályról|bootstrapping] szóló fejezetben. - -A harmadik pont (igen, a másodikat kihagytuk, de visszatérünk rá) az alkalmazás elindítása. A HTTP kérések kezelését a Nette-ben a `Nette\Application\Application` osztály (továbbiakban `Application`) végzi, tehát amikor azt mondjuk, hogy elindítjuk az alkalmazást, konkrétan ennek az osztálynak az objektumán hívjuk meg a találó nevű `run()` metódust. - -A Nette egy mentor, amely a tiszta alkalmazások írására vezeti Önt a bevált módszertanok szerint. És az egyik leginkább bevált módszertan a **dependency injection**, röviden DI. Ebben a pillanatban nem akarjuk Önt a DI magyarázatával terhelni, erre van egy [külön fejezet|dependency-injection:introduction], a lényeges következmény az, hogy a kulcsfontosságú objektumokat általában egy objektumgyár hozza létre nekünk, amelyet **DI konténernek** (röviden DIC) neveznek. Igen, ez az a factory, amelyről az előbb szó volt. És ez gyártja nekünk az `Application` objektumot is, ezért először a konténerre van szükségünk. A `Configurator` osztály segítségével szerezzük meg, és hagyjuk, hogy létrehozza az `Application` objektumot, meghívjuk rajta a `run()` metódust, és ezzel elindul a Nette alkalmazás. Pontosan ez történik az [index.php |bootstrapping#index.php] fájlban. - - -Nette Application -================= - -Az Application osztálynak egyetlen feladata van: válaszolni a HTTP kérésre. - -A Nette-ben írt alkalmazások sok ún. presenter-re tagolódnak (más keretrendszerekben találkozhat a controller kifejezéssel, ez ugyanaz), amelyek olyan osztályok, amelyek mindegyike egy konkrét weboldalt képvisel: pl. a kezdőlapot; egy terméket a webáruházban; a bejelentkezési űrlapot; a sitemap feedet stb. Az alkalmazásnak egytől több ezer presenterig terjedhet a száma. - -Az Application azzal kezdi, hogy megkéri az ún. routert, hogy döntse el, melyik presenternek adja át az aktuális kérést feldolgozásra. A router eldönti, kié a felelősség. Megnézi a bemeneti URL-t `https://example.com/product/123`, és attól függően, hogyan van beállítva, eldönti, hogy ez például a `Product` **presenter** munkája, amelytől **akcióként** a termék megjelenítését (`show`) kéri `id: 123`-mal. A presenter + akció párt jó szokás kettősponttal elválasztva írni, mint `Product:show`. - -Tehát a router átalakította az URL-t egy `Presenter:action` párra + paraméterekre, esetünkben `Product:show` + `id: 123`. Hogy néz ki egy ilyen router, megnézheti az `app/Core/RouterFactory.php` fájlban, és részletesen leírjuk a [Routing | Routing] fejezetben. - -Menjünk tovább. Az Application már ismeri a presenter nevét, és folytathatja. Azzal, hogy létrehozza a `ProductPresenter` osztály objektumát, ami a `Product` presenter kódja. Pontosabban szólva, megkéri a DI konténert, hogy hozza létre a presentert, mert a gyártás az ő feladata. - -A presenter például így nézhet ki: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // adatokat szerzünk a modellből és átadjuk a sablonnak - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -A kérés feldolgozását a presenter veszi át. És a feladat világos: hajtsa végre a `show` akciót `id: 123`-mal. Ami a presenterek nyelvén azt jelenti, hogy meghívódik a `renderShow()` metódus, és a `$id` paraméterben megkapja a `123`-at. - -A presenter több akciót is kezelhet, tehát több `render()` metódusa lehet. De javasoljuk olyan presenterek tervezését, amelyeknek egy vagy a lehető legkevesebb akciója van. - -Tehát meghívódott a `renderShow(123)` metódus, amelynek kódja ugyan kitalált példa, de láthatja rajta, hogyan adunk át adatokat a sablonnak, azaz a `$this->template`-be írással. - -Ezután a presenter visszaadja a választ. Ez lehet egy HTML oldal, egy kép, egy XML dokumentum, egy fájl elküldése a lemezről, JSON, vagy akár átirányítás egy másik oldalra. Fontos, hogy ha explicit módon nem mondjuk meg, hogyan válaszoljon (ami a `ProductPresenter` esete), akkor a válasz egy HTML oldalt tartalmazó sablon renderelése lesz. Miért? Mert az esetek 99%-ában sablont szeretnénk renderelni, ezért a presenter ezt a viselkedést veszi alapértelmezettnek, és meg akarja könnyíteni a munkánkat. Ez a Nette lényege. - -Még azt sem kell megadnunk, hogy melyik sablont renderelje, az útvonalat maga vezeti le. A `show` akció esetében egyszerűen megpróbálja betölteni a `show.latte` sablont a `ProductPresenter` osztályt tartalmazó könyvtárban. Ugyanígy megpróbálja megtalálni a layoutot az `@layout.latte` fájlban (részletesebben a [sablonok kereséséről |templates#Sablonok keresése]). - -És ezután rendereli a sablonokat. Ezzel a presenter és az egész alkalmazás feladata befejeződött, és a mű elkészült. Ha a sablon nem létezne, 404-es hibaoldal jelenne meg. Többet a presenterekről a [Presenterek|presenters] oldalon olvashat. - -[* request-flow.svg *] - -Biztonság kedvéért próbáljuk meg összefoglalni az egész folyamatot egy kicsit más URL-lel: - -1) Az URL `https://example.com` lesz -2) Indítjuk az alkalmazást, létrejön a konténer és elindul az `Application::run()` -3) A router dekódolja az URL-t `Home:default` párként -4) Létrejön a `HomePresenter` osztály objektuma -5) Meghívódik a `renderDefault()` metódus (ha létezik) -6) Renderelődik a sablon, pl. `default.latte` a layouttal, pl. `@layout.latte` - - -Talán most sok új fogalommal találkozott, de reméljük, hogy van értelmük. Alkalmazások fejlesztése a Nette-ben óriási kényelem. - - -Sablonok -======== - -Ha már szóba kerültek a sablonok, a Nette a [Latte |latte:] sablonrendszert használja. Ezért is vannak a `.latte` kiterjesztések a sablonoknál. A Latte-t egyrészt azért használják, mert ez a legbiztonságosabb sablonrendszer PHP-hoz, másrészt pedig a legintuitívabb rendszer. Nem kell sok újat tanulnia, elegendő a PHP ismerete és néhány tag. Mindent megtudhat [a dokumentációban |templates]. - -A sablonban [linkeket hozunk létre |creating-links] más presenterekhez és akciókhoz így: - -```latte -termék részletei -``` - -Egyszerűen a valós URL helyett írja be az ismert `Presenter:action` párt, és adja meg az esetleges paramétereket. A trükk az `n:href`-ben van, amely azt mondja, hogy ezt az attribútumot a Nette dolgozza fel. És generálja: - -```latte -termék részletei -``` - -Az URL generálását a már korábban említett router végzi. Ugyanis a Nette routerei kivételesek abban, hogy nemcsak az URL-ből tudnak átalakítást végezni presenter:action párra, hanem fordítva is, azaz a presenter nevéből + akcióból + paraméterekből URL-t generálni. Ennek köszönhetően a Nette-ben teljesen megváltoztathatja az URL-ek formáját egy kész alkalmazásban anélkül, hogy egyetlen karaktert is megváltoztatna a sablonban vagy a presenterben. Csak a router módosításával. Ennek köszönhetően működik az ún. kanonizáció is, ami a Nette egy másik egyedülálló tulajdonsága, amely hozzájárul a jobb SEO-hoz (keresőoptimalizálás) azáltal, hogy automatikusan megakadályozza a duplikált tartalom létezését különböző URL-eken. Sok programozó ezt lenyűgözőnek tartja. - - -Interaktív komponensek -====================== - -A presenterekről még egy dolgot el kell árulnunk: beépített komponensrendszerük van. Valami hasonlót a Delphi vagy az ASP.NET Web Forms ismerői ismerhetnek, valami távolról hasonlóra épül a React vagy a Vue.js is. A PHP keretrendszerek világában ez teljesen egyedülálló dolog. - -A komponensek önálló, újrafelhasználható egységek, amelyeket oldalakba (azaz presenterekbe) illesztünk be. Lehetnek [űrlapok |forms:in-presenter], [datagrid-ek |https://componette.org/contributte/datagrid/], menük, szavazófelületek, valójában bármi, amit érdemes ismételten használni. Létrehozhatunk saját komponenseket, vagy használhatunk néhányat a [hatalmas kínálatból |https://componette.org] származó nyílt forráskódú komponensek közül. - -A komponensek alapvetően befolyásolják az alkalmazásfejlesztési megközelítést. Új lehetőségeket nyitnak meg az oldalak előre elkészített egységekből való összeállítására. És ráadásul van valami közös bennük a [Hollywooddal |components#Hollywood style]. - - -DI konténer és konfiguráció -=========================== - -A DI konténer vagy objektumgyár az egész alkalmazás szíve. - -Ne aggódjon, ez nem egy varázslatos fekete doboz, ahogy talán az előző sorokból tűnhetett. Valójában ez egy meglehetősen unalmas PHP osztály, amelyet a Nette generál és a cache könyvtárba ment. Rengeteg `createServiceAbcd()` nevű metódusa van, és mindegyik tud létrehozni és visszaadni valamilyen objektumot. Igen, van ott egy `createServiceApplication()` metódus is, amely létrehozza a `Nette\Application\Application`-t, amire szükségünk volt az `index.php` fájlban az alkalmazás elindításához. És vannak metódusok, amelyek az egyes presentereket gyártják. És így tovább. - -Azokat az objektumokat, amelyeket a DI konténer létrehoz, valamilyen okból szolgáltatásoknak nevezik. - -Ami ebben az osztályban igazán különleges, az az, hogy nem Ön programozza, hanem a keretrendszer. Valóban PHP kódot generál és elmenti a lemezre. Ön csak utasításokat ad, hogy milyen objektumokat tudjon a konténer gyártani és pontosan hogyan. És ezek az utasítások a [konfigurációs fájlokban |bootstrapping#DI konténer konfigurálása] vannak leírva, amelyekhez a [NEON|neon:format] formátumot használják, és ezért `.neon` kiterjesztésük van. - -A konfigurációs fájlok tisztán a DI konténer instruálására szolgálnak. Tehát ha például a [session |http:configuration#Session] szekcióban megadom az `expiration: 14 days` opciót, akkor a DI konténer a sessiont reprezentáló `Nette\Http\Session` objektum létrehozásakor meghívja annak `setExpiration('14 days')` metódusát, és ezzel a konfiguráció valósággá válik. - -Van itt Önnek egy egész fejezet, amely leírja, mit lehet [konfigurálni |nette:configuring] és hogyan lehet [saját szolgáltatásokat definiálni |dependency-injection:services]. - -Amint egy kicsit belemerül a szolgáltatások létrehozásába, találkozni fog az [autowiring |dependency-injection:autowiring] szóval. Ez egy olyan trükk, amely hihetetlenül leegyszerűsíti az életét. Képes automatikusan átadni az objektumokat oda, ahol szüksége van rájuk (például az osztályai konstruktoraiban), anélkül, hogy bármit is tennie kellene. Rájön majd, hogy a Nette DI konténere egy kis csoda. - - -Merre tovább? -============= - -Áttekintettük a Nette alkalmazások alapelveit. Eddig nagyon felületesen, de hamarosan mélyebbre hatol, és idővel csodálatos webalkalmazásokat fog létrehozni. Merre tovább? Kipróbálta már az [Első alkalmazás írása|quickstart:] tutorialt? - -A fent leírtakon kívül a Nette egész arzenáljával rendelkezik [hasznos osztályoknak|utils:], [adatbázis rétegnek|database:], stb. Próbálja meg csak úgy átkattintgatni a dokumentációt. Vagy a [blogot|https://blog.nette.org]. Sok érdekes dolgot fog felfedezni. - -Hozzon a keretrendszer sok örömet Önnek 💙 diff --git a/application/hu/multiplier.texy b/application/hu/multiplier.texy deleted file mode 100644 index 4844ec74b2..0000000000 --- a/application/hu/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: dinamikus komponensek -********************************* - -.[perex] -Eszköz interaktív komponensek dinamikus létrehozásához - -Induljunk ki egy tipikus példából: van egy terméklistánk egy webáruházban, és mindegyiknél szeretnénk kiírni egy űrlapot a termék kosárba helyezéséhez. Az egyik lehetséges változat az egész listát egyetlen űrlapba csomagolni. Sokkal kényelmesebb módszert kínál azonban a [api:Nette\Application\UI\Multiplier]. - -A Multiplier lehetővé teszi több komponenshez tartozó factory kényelmes definiálását. Az beágyazott komponensek elvén működik - minden [api:Nette\ComponentModel\Container]-től öröklődő komponens tartalmazhat további komponenseket. - -.[tip] -Lásd a [komponens modellről |components#Komponensek mélységében] szóló fejezetet a dokumentációban vagy [Honza Tvrdík előadását|https://www.youtube.com/watch?v=8y3LLexWu-I]. - -A Multiplier lényege, hogy szülőként lép fel, aki a leszármazottait dinamikusan tudja létrehozni a konstruktorban átadott callback segítségével. Lásd a példát: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Termékek száma:') - ->setRequired(); - $form->addSubmit('send', 'Kosárba'); - return $form; - }); -} -``` - -Most a sablonban egyszerűen minden terméknél megjeleníthetjük az űrlapot - és mindegyik valóban egyedi komponens lesz. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -A `{control}` tagben átadott argumentum formátuma a következőt mondja: - -1. szerezd meg a `shopForm` komponenst -2. és abból szerezd meg a `$item->id` leszármazottat - -Az **1.** pont első hívásakor a `shopForm` még nem létezik, ezért meghívódik a `createComponentShopForm` factory-ja. A megszerzett komponensen (a Multiplier példányán) ezután meghívódik a konkrét űrlap factory-ja - ami az az anonim függvény, amelyet a Multipliernek a konstruktorban átadtunk. - -A foreach következő iterációjában a `createComponentShopForm` metódus már nem hívódik meg (a komponens létezik), de mivel egy másik leszármazottját keressük (`$item->id` minden iterációban más lesz), újra meghívódik az anonim függvény, és visszaad nekünk egy új űrlapot. - -Az egyetlen dolog, ami hátra van, az annak biztosítása, hogy az űrlap valóban azt a terméket adja hozzá a kosárhoz, amelyet kell - jelenleg az űrlap minden terméknél teljesen azonos. Ebben segít a Multiplier (és általában minden komponens factory a Nette Frameworkben) tulajdonsága, mégpedig az, hogy minden factory első argumentumként megkapja a létrehozott komponens nevét. Esetünkben ez `$item->id` lesz, ami pontosan az az adat, amire szükségünk van. Tehát csak kissé módosítani kell az űrlap létrehozását: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function ($itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Termékek száma:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Kosárba'); - return $form; - }); -} -``` diff --git a/application/hu/presenters.texy b/application/hu/presenters.texy deleted file mode 100644 index b9708a6c70..0000000000 --- a/application/hu/presenters.texy +++ /dev/null @@ -1,500 +0,0 @@ -Presenterek -*********** - -
    - -Megismerkedünk azzal, hogyan írjunk presentereket és sablonokat a Nette-ben. Az olvasás után tudni fogja: - -- hogyan működik a presenter -- mik azok a perzisztens paraméterek -- hogyan renderelődnek a sablonok - -
    - -[Már tudjuk |how-it-works#Nette Application], hogy a presenter egy olyan osztály, amely egy webalkalmazás egy konkrét oldalát képviseli, pl. a kezdőlapot; egy terméket a webáruházban; a bejelentkezési űrlapot; a sitemap feedet stb. Az alkalmazásnak egytől több ezer presenterig terjedhet a száma. Más keretrendszerekben kontrollereknek is nevezik őket. - -Általában presenter alatt a [api:Nette\Application\UI\Presenter] osztály leszármazottját értjük, amely alkalmas webes felületek generálására, és amelynek a továbbiakban ebben a fejezetben szenteljük a figyelmet. Általános értelemben a presenter bármely objektum, amely implementálja a [api:Nette\Application\IPresenter] interfészt. - - -Presenter életciklusa -===================== - -A presenter feladata a kérés feldolgozása és a válasz visszaadása (ami lehet HTML oldal, kép, átirányítás stb.). - -Tehát az elején átadódik neki a kérés. Ez nem közvetlenül HTTP kérés, hanem egy [api:Nette\Application\Request] objektum, amelybe a HTTP kérés a router segítségével átalakításra került. Ezzel az objektummal általában nem találkozunk, mivel a presenter a kérés feldolgozását okosan delegálja további metódusokba, amelyeket most megmutatunk. - -[* lifecycle.svg *] *** *Presenter életciklusa* .<> - -A kép felsorolja azokat a metódusokat, amelyek sorban fentről lefelé hívódnak meg, ha léteznek. Egyiknek sem kell léteznie, lehet teljesen üres presenterünk egyetlen metódus nélkül, és építhetünk rá egy egyszerű statikus weboldalt. - - -`__construct()` ---------------- - -A konstruktor nem igazán tartozik a presenter életciklusához, mert az objektum létrehozásának pillanatában hívódik meg. De a fontossága miatt említjük. A konstruktor (a [inject metódussal|best-practices:inject-method-attribute] együtt) a függőségek átadására szolgál. - -A presenternek nem kellene az alkalmazás üzleti logikáját intéznie, adatbázisból írni és olvasni, számításokat végezni stb. Erre valók a modellnek nevezett réteg osztályai. Például az `ArticleRepository` osztály felelhet a cikkek betöltéséért és mentéséért. Hogy a presenter dolgozhasson vele, [dependency injection |dependency-injection:passing-dependencies] segítségével kéri át: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -A kérés kézhezvétele után azonnal meghívódik a `startup()` metódus. Használhatja property-k inicializálására, felhasználói jogosultságok ellenőrzésére stb. Kötelező, hogy a metódus mindig meghívja az ős `parent::startup()` metódusát. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -A `render()` metódus megfelelője. Míg a `render()` arra szolgál, hogy előkészítse az adatokat egy konkrét sablonhoz, amely aztán renderelődik, addig az `action()` a kérést dolgozza fel a sablon renderelésétől függetlenül. Például feldolgozza az adatokat, bejelentkezteti vagy kijelentkezteti a felhasználót, és így tovább, majd [átirányít máshová |#Átirányítás]. - -Fontos, hogy az `action()` korábban hívódik meg, mint a `render()`, így benne esetleg megváltoztathatjuk a további történéseket, azaz megváltoztathatjuk a renderelendő sablont, és a meghívandó `render()` metódust is. Ezt a `setView('jineView')` segítségével tehetjük meg. - -A metódusnak a kérésből származó paraméterek adódnak át. Lehetséges és ajánlott a paraméterek típusának megadása, pl. `actionShow(int $id, ?string $slug = null)` - ha az `id` paraméter hiányzik, vagy ha nem integer, a presenter [404-es hibát |#Hiba 404 és társai] ad vissza és befejezi a működését. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -A metódus az ún. signálokat dolgozza fel, amelyekkel a [komponenseknek |components#Signal] szentelt fejezetben ismerkedünk meg. Ugyanis főként komponensekhez és AJAX kérések feldolgozásához készült. - -A metódusnak a kérésből származó paraméterek adódnak át, mint az `action()` esetében, beleértve a típusellenőrzést is. - - -`beforeRender()` ----------------- - -A `beforeRender` metódus, ahogy a neve is sugallja, minden `render()` metódus előtt hívódik meg. A sablon közös konfigurálására, a layout változóinak átadására és hasonló dolgokra használják. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -Az a hely, ahol előkészítjük a sablont a későbbi renderelésre, adatokat adunk át neki stb. - -A metódusnak a kérésből származó paraméterek adódnak át, mint az `action()` esetében, beleértve a típusellenőrzést is. - -```php -public function renderShow(int $id): void -{ - // adatokat szerzünk a modellből és átadjuk a sablonnak - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -Az `afterRender` metódus, ahogy a neve ismét sugallja, minden `render()` metódus után hívódik meg. Ritkábban használják. - - -`shutdown()` ------------- - -A presenter életciklusának végén hívódik meg. - - -**Jó tanács, mielőtt továbbmennénk**. A presenter, mint látható, több akciót/view-t is kezelhet, tehát több `render()` metódusa lehet. De javasoljuk olyan presenterek tervezését, amelyeknek egy vagy a lehető legkevesebb akciója van. - - -Válasz küldése -============== - -A presenter válasza általában egy [HTML oldalt tartalmazó sablon renderelése|templates], de lehet fájlküldés, JSON, vagy akár átirányítás egy másik oldalra is. - -Az életciklus bármely pontján elküldhetünk választ a következő metódusok valamelyikével, és ezzel egyidejűleg befejezhetjük a presentert: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` és `forward()` [átirányít |#Átirányítás] -- `error()` befejezi a presentert [hiba miatt |#Hiba 404 és társai] -- `sendJson($data)` befejezi a presentert és [adatokat küld |#JSON küldése] JSON formátumban -- `sendTemplate()` befejezi a presentert és azonnal [rendereli a sablont |templates] -- `sendResponse($response)` befejezi a presentert és [saját választ |#Válaszok] küld -- `terminate()` befejezi a presentert válasz nélkül - -Ha egyiket sem hívja meg ezek közül a metódusok közül, a presenter automatikusan a sablon rendereléséhez fog hozzá. Miért? Mert az esetek 99%-ában sablont szeretnénk renderelni, ezért a presenter ezt a viselkedést veszi alapértelmezettnek, és meg akarja könnyíteni a munkánkat. - - -Linkek létrehozása -================== - -A presenter rendelkezik a `link()` metódussal, amellyel URL linkeket lehet létrehozni más presenterekhez. Az első paraméter a cél presenter & akció, ezt követik az átadott argumentumok, amelyek tömbként is megadhatók: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'hu']); -``` - -A sablonban a linkek más presenterekhez & akciókhoz a következőképpen hozhatók létre: - -```latte -termék részletei -``` - -Egyszerűen a valós URL helyett írja be az ismert `Presenter:action` párt, és adja meg az esetleges paramétereket. A trükk az `n:href`-ben van, amely azt mondja, hogy ezt az attribútumot a Latte dolgozza fel, és valós URL-t generál. A Nette-ben tehát egyáltalán nem kell az URL-eken gondolkodnia, csak a presentereken és akciókon. - -További információkat az [URL linkek létrehozása|creating-links] fejezetben talál. - - -Átirányítás -=========== - -Másik presenterhez való átlépéshez a `redirect()` és `forward()` metódusok szolgálnak, amelyeknek nagyon hasonló a szintaxisa, mint a [link() |#Linkek létrehozása] metódusnak. - -A `forward()` metódus azonnal átlép az új presenterhez HTTP átirányítás nélkül: - -```php -$this->forward('Product:show'); -``` - -Példa az ún. ideiglenes átirányításra 302-es HTTP kóddal (vagy 303-mal, ha az aktuális kérés metódusa POST): - -```php -$this->redirect('Product:show', $id); -``` - -Állandó átirányítást 301-es HTTP kóddal így érhet el: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Más, alkalmazáson kívüli URL-re a `redirectUrl()` metódussal lehet átirányítani. Második paraméterként megadható a HTTP kód, az alapértelmezett 302 (vagy 303, ha az aktuális kérés metódusa POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Az átirányítás azonnal befejezi a presenter működését az ún. csendes befejező kivétel, a `Nette\Application\AbortException` dobásával. - -Az átirányítás előtt küldhetünk [flash message-t |#Flash üzenetek], azaz üzeneteket, amelyek az átirányítás után megjelennek a sablonban. - - -Flash üzenetek -============== - -Ezek általában valamilyen művelet eredményéről tájékoztató üzenetek. A flash üzenetek fontos jellemzője, hogy a sablonban átirányítás után is elérhetők. Megjelenítésük után még további 30 másodpercig élnek – például arra az esetre, ha a felhasználó hibás átvitel miatt frissítené az oldalt - az üzenet tehát nem tűnik el azonnal. - -Csak meg kell hívni a [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] metódust, és a sablonba való átadásról a presenter gondoskodik. Az első paraméter az üzenet szövege, a nem kötelező második paraméter pedig a típusa (error, warning, info stb.). A `flashMessage()` metódus visszaadja a flash üzenet példányát, amelyhez további információkat lehet hozzáadni. - -```php -$this->flashMessage('Az elem törölve lett.'); -$this->redirect(/* ... */); // és átirányítunk -``` - -A sablonban ezek az üzenetek a `$flashes` változóban érhetők el `stdClass` objektumokként, amelyek tartalmazzák a `message` (üzenet szövege), `type` (üzenet típusa) tulajdonságokat, és tartalmazhatják a már említett felhasználói információkat is. Például így rendereljük őket: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Hiba 404 és társai -================== - -Ha a kérést nem lehet teljesíteni, például azért, mert a megjeleníteni kívánt cikk nem létezik az adatbázisban, 404-es hibát dobunk az `error(?string $message = null, int $httpCode = 404)` metódussal. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -A hiba HTTP kódját második paraméterként lehet átadni, az alapértelmezett 404. A metódus úgy működik, hogy `Nette\Application\BadRequestException` kivételt dob, mire az `Application` átadja a vezérlést az error-presenternek. Ez egy olyan presenter, amelynek feladata a bekövetkezett hibáról tájékoztató oldal megjelenítése. Az error-presenter beállítása az [application konfigurációban|configuration] történik. - - -JSON küldése -============ - -Példa egy action-metódusra, amely adatokat küld JSON formátumban és befejezi a presentert: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Kérés paraméterei .{data-version:3.1.14} -======================================== - -A presenter és minden komponens is megkapja a paramétereit a HTTP kérésből. Értéküket a `getParameter($name)` vagy `getParameters()` metódussal tudhatja meg. Az értékek stringek vagy string tömbök, lényegében nyers adatok, amelyeket közvetlenül az URL-ből nyerünk. - -A nagyobb kényelem érdekében javasoljuk a paraméterek property-ken keresztüli elérhetővé tételét. Csak meg kell őket jelölni a `#[Parameter]` attribútummal: - -```php -use Nette\Application\Attributes\Parameter; // ez a sor fontos - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // public-nak kell lennie -} -``` - -A property-nél javasoljuk az adattípus megadását (pl. `string`), és a Nette ez alapján automatikusan átalakítja az értéket. A paraméterek értékeit lehet [validálni |#Paraméterek validálása] is. - -Link létrehozásakor a paraméterek értékét közvetlenül be lehet állítani: - -```latte -kattints -``` - - -Perzisztens paraméterek -======================= - -A perzisztens paraméterek az állapot megőrzésére szolgálnak a különböző kérések között. Értékük ugyanaz marad a linkre kattintás után is. A session adatokkal ellentétben az URL-ben kerülnek átvitelre. És ez teljesen automatikusan történik, tehát nem kell explicit módon megadni őket a `link()` vagy `n:href` esetén. - -Példa a használatra? Van egy többnyelvű alkalmazása. Az aktuális nyelv egy paraméter, amelynek folyamatosan az URL részének kell lennie. De hihetetlenül fárasztó lenne minden linkben megadni. Így csinál belőle egy `lang` perzisztens paramétert, és magától átadódik. Remek! - -Perzisztens paraméter létrehozása a Nette-ben rendkívül egyszerű. Csak létre kell hozni egy public property-t és megjelölni egy attribútummal: (korábban a `/** @persistent */` volt használatos) - -```php -use Nette\Application\Attributes\Persistent; // ez a sor fontos - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // public-nak kell lennie -} -``` - -Ha a `$this->lang` értéke például `'en'` lesz, akkor a `link()` vagy `n:href` segítségével létrehozott linkek is tartalmazni fogják a `lang=en` paramétert. És a linkre kattintás után ismét `$this->lang = 'en'` lesz. - -A property-nél javasoljuk az adattípus megadását (pl. `string`), és megadhat alapértelmezett értéket is. A paraméterek értékeit lehet [validálni |#Paraméterek validálása]. - -A perzisztens paraméterek alapértelmezés szerint az adott presenter összes akciója között átadódnak. Ahhoz, hogy több presenter között is átadódjanak, definiálni kell őket vagy: - -- egy közös ősben, amelytől a presenterek örökölnek -- egy trait-ben, amelyet a presenterek használnak: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Link létrehozásakor a perzisztens paraméter értékét meg lehet változtatni: - -```latte -részletek magyarul -``` - -Vagy *resetelhető*, azaz eltávolítható az URL-ből. Ekkor az alapértelmezett értékét veszi fel: - -```latte -kattints -``` - - -Interaktív komponensek -====================== - -A presenterek beépített komponensrendszerrel rendelkeznek. A komponensek önálló, újrafelhasználható egységek, amelyeket presenterekbe illesztünk be. Lehetnek [űrlapok |forms:in-presenter], datagrid-ek, menük, valójában bármi, amit érdemes ismételten használni. - -Hogyan illesztjük be és használjuk a komponenseket a presenterben? Ezt a [Komponensek |components] fejezetben tudhatja meg. Még azt is megtudhatja, mi közük van Hollywoodhoz. - -És hol szerezhetek komponenseket? A [Componette |https://componette.org/search/component] oldalon talál nyílt forráskódú komponenseket és számos más kiegészítőt a Nette-hez, amelyeket a keretrendszer körüli közösség önkéntesei helyeztek el itt. - - -Mélyebbre megyünk -================= - -.[tip] -Azzal, amit eddig ebben a fejezetben megmutattunk, valószínűleg teljesen elboldogul. A következő sorok azoknak szólnak, akiket mélyebben érdekelnek a presenterek, és mindent tudni akarnak róluk. - - -Paraméterek validálása ----------------------- - -Az URL-ből kapott [kérés paramétereinek |#Kérés paraméterei] és [perzisztens paramétereinek |#Perzisztens paraméterek] értékeit a `loadState()` metódus írja be a property-kbe. Ez ellenőrzi azt is, hogy megfelelnek-e a property-nél megadott adattípusnak, különben 404-es hibával válaszol, és az oldal nem jelenik meg. - -Soha ne bízzon vakon a paraméterekben, mert azokat a felhasználó könnyen felülírhatja az URL-ben. Így például ellenőrizzük, hogy a `$this->lang` nyelv a támogatottak között van-e. Megfelelő módszer az említett `loadState()` metódus felülírása: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // itt állítódik be a $this->lang - // következik a saját értékellenőrzés: - if (!in_array($this->lang, ['en', 'hu'])) { - $this->error(); - } - } -} -``` - - -Kérés mentése és visszaállítása -------------------------------- - -A presenter által kezelt kérés egy [api:Nette\Application\Request] objektum, és a presenter `getRequest()` metódusa adja vissza. - -Az aktuális kérést el lehet menteni a sessionbe, vagy onnan visszaállítani, és hagyni, hogy a presenter újra végrehajtsa. Ez hasznos például olyan helyzetben, amikor a felhasználó egy űrlapot tölt ki, és lejár a bejelentkezése. Hogy ne veszítse el az adatokat, a bejelentkezési oldalra való átirányítás előtt az aktuális kérést elmentjük a sessionbe a `$reqId = $this->storeRequest()` segítségével, amely visszaadja annak azonosítóját egy rövid string formájában, és ezt átadjuk paraméterként a bejelentkezési presenternek. - -Bejelentkezés után meghívjuk a `$this->restoreRequest($reqId)` metódust, amely kiemeli a kérést a sessionből és forwardol rá. A metódus közben ellenőrzi, hogy a kérést ugyanaz a felhasználó hozta-e létre, aki most bejelentkezett. Ha másik felhasználó jelentkezett be, vagy a kulcs érvénytelen, nem csinál semmit, és a program folytatódik tovább. - -Nézze meg a [Hogyan térjünk vissza egy korábbi oldalra |best-practices:restore-request] útmutatót. - - -Kanonizáció ------------ - -A presentereknek van egy igazán nagyszerű tulajdonsága, amely hozzájárul a jobb SEO-hoz (keresőoptimalizálás). Automatikusan megakadályozzák a duplikált tartalom létezését különböző URL-eken. Ha egy bizonyos célhoz több URL cím vezet, pl. `/index` és `/index?page=1`, a keretrendszer egyiküket elsődlegesnek (kanonikusnak) határozza meg, a többit pedig 301-es HTTP kóddal átirányítja rá. Ennek köszönhetően a keresőmotorok nem indexelik kétszer az oldalakat, és nem osztják meg a page rankjüket. - -Ezt a folyamatot kanonizációnak nevezik. A kanonikus URL az, amelyet a [router|routing] generál, általában tehát az első megfelelő route a gyűjteményben. - -A kanonizáció alapértelmezés szerint be van kapcsolva, és kikapcsolható a `$this->autoCanonicalize = false` segítségével. - -Az átirányítás nem történik meg AJAX vagy POST kérés esetén, mert adatvesztéshez vezetne, vagy nem lenne hozzáadott értéke SEO szempontból. - -A kanonizációt manuálisan is kiválthatja a `canonicalize()` metódussal, amelynek hasonlóan a `link()` metódushoz, átadódik a presenter, az akció és a paraméterek. Létrehoz egy linket, és összehasonlítja az aktuális URL címmel. Ha különböznek, átirányít a generált linkre. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // átirányít, ha a $slug különbözik a $realSlug-tól - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - - -Események ---------- - -A `startup()`, `beforeRender()` és `shutdown()` metódusokon kívül, amelyek a presenter életciklusának részeként hívódnak meg, definiálhatunk további függvényeket is, amelyeket automatikusan meg kell hívni. A presenter definiálja az ún. [eseményeket |nette:glossary#Eventek események], amelyek handlereit hozzáadhatja a `$onStartup`, `$onRender` és `$onShutdown` tömbökhöz. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -A `$onStartup` tömb handlerei közvetlenül a `startup()` metódus előtt hívódnak meg, továbbá a `$onRender` a `beforeRender()` és `render()` között, végül a `$onShutdown` közvetlenül a `shutdown()` előtt. - - -Válaszok --------- - -A presenter által visszaadott válasz egy objektum, amely implementálja a [api:Nette\Application\Response] interfészt. Számos előkészített válasz áll rendelkezésre: - -- [api:Nette\Application\Responses\CallbackResponse] - callback-et küld -- [api:Nette\Application\Responses\FileResponse] - fájlt küld -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - JSON-t küld -- [api:Nette\Application\Responses\RedirectResponse] - átirányítás -- [api:Nette\Application\Responses\TextResponse] - szöveget küld -- [api:Nette\Application\Responses\VoidResponse] - üres válasz - -A válaszokat a `sendResponse()` metódussal küldjük el: - -```php -use Nette\Application\Responses; - -// Egyszerű szöveg -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Fájlt küld -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// A válasz egy callback lesz -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - - -Hozzáférés korlátozása `#[Requires]` segítségével .{data-version:3.2.2} ------------------------------------------------------------------------ - -A `#[Requires]` attribútum fejlett lehetőségeket kínál a presenterekhez és metódusaikhoz való hozzáférés korlátozására. Használható HTTP metódusok specifikálására, AJAX kérés megkövetelésére, azonos eredetre (same origin) való korlátozásra, és csak forwardoláson keresztüli hozzáférésre. Az attribútum alkalmazható mind a presenter osztályokra, mind az egyes `action()`, `render()`, `handle()` és `createComponent()` metódusokra. - -Meghatározhatja ezeket a korlátozásokat: -- HTTP metódusokra: `#[Requires(methods: ['GET', 'POST'])]` -- AJAX kérés megkövetelése: `#[Requires(ajax: true)]` -- csak azonos eredetű hozzáférés: `#[Requires(sameOrigin: true)]` -- csak forwardon keresztüli hozzáférés: `#[Requires(forward: true)]` -- korlátozás konkrét akciókra: `#[Requires(actions: 'default')]` - -Részleteket a [Hogyan használjuk a Requires attribútumot |best-practices:attribute-requires] útmutatóban talál. - - -HTTP metódus ellenőrzése ------------------------- - -A Nette presenterei automatikusan ellenőrzik minden bejövő kérés HTTP metódusát. Ennek az ellenőrzésnek az oka elsősorban a biztonság. Alapértelmezés szerint a `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` metódusok engedélyezettek. - -Ha további metódust szeretne engedélyezni, például az `OPTIONS`-t, használja a `#[Requires]` attribútumot (Nette Application v3.2-től): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -A 3.1-es verzióban az ellenőrzés a `checkHttpMethod()`-ban történik, amely megállapítja, hogy a kérésben megadott metódus szerepel-e a `$presenter->allowedMethods` tömbben. Metódus hozzáadása így történik: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Fontos hangsúlyozni, hogy ha engedélyezi az `OPTIONS` metódust, azt követően megfelelően kezelnie is kell a presenterében. A metódust gyakran használják ún. preflight kérésként, amelyet a böngésző automatikusan küld a tényleges kérés előtt, amikor meg kell állapítani, hogy a kérés engedélyezett-e a CORS (Cross-Origin Resource Sharing) politika szempontjából. Ha engedélyezi a metódust, de nem implementálja a megfelelő választ, az inkonzisztenciákhoz és potenciális biztonsági problémákhoz vezethet. - - -További olvasmányok -=================== - -- [Inject metódusok és attribútumok |best-practices:inject-method-attribute] -- [Presenterek összeállítása trait-ekből |best-practices:presenter-traits] -- [Beállítások átadása presentereknek |best-practices:passing-settings-to-presenters] -- [Hogyan térjünk vissza egy korábbi oldalra |best-practices:restore-request] diff --git a/application/hu/routing.texy b/application/hu/routing.texy deleted file mode 100644 index 4fb937eba5..0000000000 --- a/application/hu/routing.texy +++ /dev/null @@ -1,721 +0,0 @@ -Routing -******* - -
    - -A Router felelős mindenért, ami az URL címekkel kapcsolatos, hogy Önnek már ne kelljen gondolkodnia rajtuk. Megmutatjuk: - -- hogyan állítsuk be a routert, hogy az URL-ek az elképzeléseinknek megfeleljenek -- beszélünk a SEO-ról és az átirányításról -- és megmutatjuk, hogyan írjunk saját routert - -
    - - -Az emberibb URL-ek (vagy cool vagy pretty URL-ek) használhatóbbak, megjegyezhetőbbek és pozitívan hozzájárulnak a SEO-hoz. A Nette erre gondol, és teljes mértékben támogatja a fejlesztőket. Pontosan olyan URL-struktúrát tervezhet az alkalmazásához, amilyet csak szeretne. Akár akkor is megtervezheti, amikor az alkalmazás már kész, mert ez nem igényel beavatkozást a kódba vagy a sablonokba. Ugyanis elegáns módon egy [egyetlen helyen |#Integrálás az alkalmazásba] definiálódik, a routerben, és így nincs szétszórva annotációk formájában az összes presenterben. - -A Nette routere kivételes abban, hogy **kétirányú.** Képes dekódolni a HTTP kérésben lévő URL-t, és linkeket is létrehozni. Tehát kulcsfontosságú szerepet játszik a [Nette Applicationben |how-it-works#Nette Application], mert egyrészt eldönti, hogy melyik presenter és akció fogja végrehajtani az aktuális kérést, másrészt pedig a [URL generálására |creating-links] használatos a sablonban stb. - -Azonban a router nem korlátozódik csak erre a felhasználásra, használhatja olyan alkalmazásokban is, ahol egyáltalán nem használnak presentereket, REST API-khoz stb. További információk a [#Önálló használat] részben. - - -Route gyűjtemény -================ - -Az alkalmazás URL címeinek formájának definiálásának legkellemesebb módját a [api:Nette\Application\Routers\RouteList] osztály kínálja. A definíció ún. route-ok listájából áll, azaz URL cím maszkokból és a hozzájuk rendelt presenterekből és akciókból, egy egyszerű API segítségével. A route-okat nem kell elneveznünk. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -A példa azt mondja, hogy ha a böngészőben megnyitjuk a `https://domain.com/rss.xml` címet, akkor a `Feed` presenter jelenik meg az `rss` akcióval, ha a `https://domain.com/article/12` címet, akkor az `Article` presenter jelenik meg a `view` akcióval stb. Ha nem található megfelelő route, a Nette Application [BadRequestException |api:Nette\Application\BadRequestException] kivételt dob, amely a felhasználónak 404 Not Found hibaoldalként jelenik meg. - - -Route-ok sorrendje ------------------- - -Teljesen **kulcsfontosságú a sorrend**, amelyben az egyes route-ok fel vannak sorolva, mert sorban fentről lefelé értékelődnek ki. Az a szabály érvényes, hogy a route-okat **a specifikusaktól az általánosakig** deklaráljuk: - -```php -// ROSSZ: az 'rss.xml'-t az első route fogja el, és ezt a stringet -ként értelmezi -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// JÓ -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -A route-ok fentről lefelé értékelődnek ki a linkek generálásakor is: - -```php -// ROSSZ: a 'Feed:rss' linket 'admin/feed/rss'-ként generálja -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// JÓ -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Nem titkoljuk Ön elől, hogy a route-ok helyes összeállítása némi ügyességet igényel. Mielőtt elsajátítaná, hasznos segítő lesz a [routing panel |#Router debuggolása]. - - -Maszk és paraméterek --------------------- - -A maszk a web gyökérkönyvtárától számított relatív utat írja le. A legegyszerűbb maszk egy statikus URL: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Gyakran a maszkok ún. **paramétereket** tartalmaznak. Ezek hegyes zárójelekben vannak megadva (pl. ``), és átadódnak a cél presenternek, például a `renderShow(int $year)` metódusnak vagy a `$year` perzisztens paraméternek: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -A példa azt mondja, hogy ha a böngészőben megnyitjuk a `https://example.com/chronicle/2020` címet, akkor a `History` presenter jelenik meg a `show` akcióval és a `year: 2020` paraméterrel. - -A paramétereknek közvetlenül a maszkban adhatunk alapértelmezett értéket, és ezzel opcionálissá válnak: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -A route mostantól elfogadja a `https://example.com/chronicle/` URL-t is, amely szintén a `History:show`-t jeleníti meg a `year: 2020` paraméterrel. - -A paraméter természetesen lehet a presenter és az akció neve is. Például így: - -```php -$router->addRoute('/', 'Home:default'); -``` - -Az említett route elfogadja pl. az `/article/edit` vagy az `/catalog/list` formátumú URL-eket, és ezeket `Article:edit` és `Catalog:list` presenterekként és akciókként értelmezi. - -Ugyanakkor a `presenter` és `action` paramétereknek alapértelmezett értékként `Home`-ot és `default`-ot ad, így ezek is opcionálisak. Tehát a route elfogadja az `/article` formátumú URL-t is, és azt `Article:default`-ként értelmezi. Vagy fordítva, a `Product:default` link az `/product` utat generálja, az alapértelmezett `Home:default` link pedig a `/` utat. - -A maszk nemcsak a web gyökérkönyvtárától számított relatív utat írhatja le, hanem abszolút utat is, ha perjellel kezdődik, vagy akár teljes abszolút URL-t is, ha két perjellel kezdődik: - -```php -// relatív a document roothoz -$router->addRoute('/', /* ... */); - -// abszolút út (relatív a domainhez) -$router->addRoute('//', /* ... */); - -// abszolút URL domainnel együtt (relatív a sémához) -$router->addRoute('//.example.com//', /* ... */); - -// abszolút URL sémával együtt -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Validációs kifejezések ----------------------- - -Minden paraméterhez meg lehet határozni egy validációs feltételt [reguláris kifejezéssel|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Például az `id` paraméternek meghatározzuk, hogy csak számjegyeket tartalmazhat a `\d+` reguláris kifejezéssel: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -Minden paraméter alapértelmezett reguláris kifejezése `[^/]+`, azaz minden, kivéve a perjelet. Ha egy paraméternek perjeleket is el kell fogadnia, adjuk meg a `.+` kifejezést: - -```php -// elfogadja a https://example.com/a/b/c címet, a path 'a/b/c' lesz -$router->addRoute('', /* ... */); -``` - - -Opcionális szekvenciák ----------------------- - -A maszkban szögletes zárójelekkel lehet jelölni az opcionális részeket. A maszk bármely része lehet opcionális, és tartalmazhatnak paramétereket is: - -```php -$router->addRoute('[/]', /* ... */); - -// Elfogadott utak: -// /cs/download => lang => cs, name => download -// /download => lang => null, name => download -``` - -Ha egy paraméter egy opcionális szekvencia része, természetesen maga is opcionálissá válik. Ha nincs megadva alapértelmezett értéke, akkor null lesz. - -Az opcionális részek a domainben is lehetnek: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -A szekvenciákat tetszőlegesen lehet egymásba ágyazni és kombinálni: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Elfogadott utak: -// /cs/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -Az URL generálásakor a legrövidebb változatra törekszünk, tehát minden, amit ki lehet hagyni, kihagyásra kerül. Ezért például az `index[.html]` route az `/index` utat generálja. A viselkedés megfordítása a bal szögletes zárójel utáni felkiáltójellel lehetséges: - -```php -// elfogadja a /hello és /hello.html címeket, /hello-t generál -$router->addRoute('[.html]', /* ... */); - -// elfogadja a /hello és /hello.html címeket, /hello.html-t generál -$router->addRoute('[!.html]', /* ... */); -``` - -Az opcionális paraméterek (azaz az alapértelmezett értékkel rendelkező paraméterek) szögletes zárójelek nélkül lényegében úgy viselkednek, mintha a következőképpen lennének zárójelezve: - -```php -$router->addRoute('//', /* ... */); - -// megfelel ennek: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Ha befolyásolni szeretnénk a záró perjel viselkedését, hogy pl. a `/home/` helyett csak `/home` generálódjon, azt így lehet elérni: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Helyettesítő karakterek ------------------------ - -Az abszolút út maszkjában használhatjuk a következő helyettesítő karaktereket, és így elkerülhetjük például annak szükségességét, hogy a maszkba írjuk a domaint, amely eltérhet a fejlesztői és az éles környezetben: - -- `%tld%` = top level domain, pl. `com` vagy `org` -- `%sld%` = second level domain, pl. `example` -- `%domain%` = domain aldomainek nélkül, pl. `example.com` -- `%host%` = teljes host, pl. `www.example.com` -- `%basePath%` = út a gyökérkönyvtárhoz - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Részletesebb specifikációhoz használható egy még bővebb forma, ahol az alapértelmezett értékeken kívül beállíthatjuk a paraméterek további tulajdonságait is, mint például a validációs reguláris kifejezést (lásd az `id` paramétert): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Fontos megjegyezni, hogy ha a tömbben definiált paraméterek nincsenek megadva az út maszkjában, értéküket nem lehet megváltoztatni, még az URL-ben a kérdőjel után megadott query paraméterekkel sem. - - -Szűrők és fordítások --------------------- - -Az alkalmazás forráskódjait angolul írjuk, de ha a weboldalnak magyar URL-ekkel kell rendelkeznie, akkor az egyszerű routing típus: - -```php -$router->addRoute('/', 'Home:default'); -``` - -angol URL-eket fog generálni, mint például `/product/123` vagy `/cart`. Ha azt szeretnénk, hogy a presenterek és akciók az URL-ben magyar szavakkal legyenek reprezentálva (pl. `/produkt/123` vagy `/kosik`), használhatunk fordítási szótárat. Ennek megadásához már a második paraméter "beszédesebb" változatára van szükség: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // string az URL-ben => presenter - 'produkt' => 'Product', - 'kosik' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'lista' => 'list', - ], - ], -]); -``` - -A fordítási szótár több kulcsa is ugyanarra a presenterhez vezethet. Ezzel különböző aliasokat hozunk létre hozzá. Kanonikus változatnak (tehát annak, amely a generált URL-ben lesz) az utolsó kulcs számít. - -A fordítási táblát így bármelyik paraméterre lehet alkalmazni. Ha a fordítás nem létezik, az eredeti érték veszi át. Ezt a viselkedést megváltoztathatjuk a `Route::FilterStrict => true` hozzáadásával, és a route elutasítja az URL-t, ha az érték nincs a szótárban. - -A tömb formájú fordítási szótár mellett saját fordítási függvényeket is bevethetünk. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -A `Route::FilterIn` függvény átalakít az URL-ben lévő paraméter és a presenternek átadott string között, a `FilterOut` függvény pedig az ellenkező irányú átalakítást biztosítja. - -A `presenter`, `action` és `module` paramétereknek már vannak előre definiált szűrőik, amelyek átalakítanak a PascalCase ill. camelCase stílus és az URL-ben használt kebab-case között. A paraméterek alapértelmezett értéke már az átalakított formában íródik, tehát például a presenter esetében ``-et írunk, nem pedig ``-et. - - -Általános szűrők ----------------- - -A konkrét paraméterekhez szánt szűrők mellett definiálhatunk általános szűrőket is, amelyek megkapják az összes paraméter asszociatív tömbjét, amelyet tetszőlegesen módosíthatnak, majd visszaadják. Az általános szűrőket a `null` kulcs alatt definiáljuk. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Az általános szűrők lehetővé teszik a route viselkedésének teljesen tetszőleges módosítását. Használhatjuk őket például paraméterek módosítására más paraméterek alapján. Például a `` és `` lefordítása az aktuális `` paraméter értéke alapján. - -Ha egy paraméternek van saját szűrője definiálva, és egyidejűleg létezik általános szűrő is, akkor a saját `FilterIn` hajtódik végre az általános előtt, és fordítva, az általános `FilterOut` a saját előtt. Tehát az általános szűrőn belül a `presenter` ill. `action` paraméterek értékei PascalCase ill. camelCase stílusban vannak megadva. - - -Egyirányú OneWay ----------------- - -Az egyirányú route-okat a régi URL-ek funkcionalitásának megőrzésére használják, amelyeket az alkalmazás már nem generál, de még mindig elfogad. `OneWay` jelzővel jelöljük őket: - -```php -// régi URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); -// új URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -A régi URL-re való hozzáféréskor a presenter automatikusan átirányít az új URL-re, így ezeket az oldalakat a keresőmotorok nem indexelik kétszer (lásd [#SEO és kanonizáció]). - - -Dinamikus routing callbackekkel -------------------------------- - -A dinamikus routing callbackekkel lehetővé teszi, hogy a route-okhoz közvetlenül függvényeket (callbackeket) rendeljen, amelyek akkor hajtódnak végre, amikor az adott utat meglátogatják. Ez a rugalmas funkcionalitás lehetővé teszi, hogy gyorsan és hatékonyan hozzon létre különböző végpontokat (endpoints) az alkalmazásához: - -```php -$router->addRoute('test', function () { - echo 'a /test címen van'; -}); -``` - -Definiálhat paramétereket is a maszkban, amelyek automatikusan átadódnak a callbacknek: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Üdvözöljük weboldalunk cseh verzióján!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - - -Modulok -------- - -Ha több route-unk van, amelyek egy közös [modulba |directory-structure#Presenterek és sablonok] tartoznak, használjuk a `withModule()`-t: - -```php -$router = new RouteList; -$router->withModule('Forum') // a következő route-ok a Forum modul részei - ->addRoute('rss', 'Feed:rss') // a presenter Forum:Feed lesz - ->addRoute('/') - - ->withModule('Admin') // a következő route-ok a Forum:Admin modul részei - ->addRoute('sign:in', 'Sign:in'); -``` - -Alternatívaként használható a `module` paraméter: - -```php -// Az URL manage/dashboard/default az Admin:Dashboard presenterhez map-el -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - - -Aldomainek ----------- - -A route gyűjteményeket aldomainek szerint is tagolhatjuk: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -A domain névben használhatunk [#Helyettesítő karakterek] helyettesítő karaktereket is: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Útvonal prefix --------------- - -A route gyűjteményeket az URL útvonala szerint is tagolhatjuk: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // elfogja az /eshop/rss URL-t - ->addRoute('/'); // elfogja az /eshop// URL-t -``` - - -Kombinációk ------------ - -A fenti tagolásokat kölcsönösen kombinálhatjuk: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Query paraméterek ------------------ - -A maszkok tartalmazhatnak query paramétereket is (paraméterek a kérdőjel után az URL-ben). Ezekhez nem lehet validációs kifejezést definiálni, de meg lehet változtatni a nevüket, amely alatt a presenternek átadódnak: - -```php -// a 'cat' query paramétert az alkalmazásban 'categoryId' néven szeretnénk használni -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Foo paraméterek ---------------- - -Most már mélyebbre megyünk. A Foo paraméterek lényegében névtelen paraméterek, amelyek lehetővé teszik reguláris kifejezések illesztését. Példa egy route-ra, amely elfogadja a `/index`, `/index.html`, `/index.htm` és `/index.php` címeket: - -```php -$router->addRoute('index', /* ... */); -``` - -Explicit módon is definiálható a string, amelyet az URL generálásakor használni kell. A stringnek közvetlenül a kérdőjel után kell elhelyezkednie. A következő route hasonló az előzőhöz, de `/index.html`-t generál `/index` helyett, mert a `.html` string van beállítva generálási értékként: - -```php -$router->addRoute('index', /* ... */); -``` - - -Integrálás az alkalmazásba -========================== - -Ahhoz, hogy a létrehozott routert bekapcsoljuk az alkalmazásba, szólnunk kell róla a DI konténernek. A legegyszerűbb út egy factory elkészítése, amely a router objektumot létrehozza, és a konfigurációban közölni a konténerrel, hogy azt használja. Tegyük fel, hogy erre a célra megírjuk az `App\Core\RouterFactory::createRouter()` metódust: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -A [konfigurációba |dependency-injection:services] pedig beírjuk: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Bármilyen függőség, például adatbázisra stb., átadódik a factory metódusnak annak paramétereiként [autowiring|dependency-injection:autowiring] segítségével: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Sokkal egyszerűbb router, mint a route gyűjtemény, a [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Akkor használjuk, ha nincsenek különösebb igényeink az URL formájára, ha nincs `mod_rewrite` (vagy annak alternatívái) elérhető, vagy ha még nem akarunk szép URL-ekkel foglalkozni. - -Körülbelül ilyen formátumú címeket generál: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -A SimpleRouter konstruktorának paramétere az alapértelmezett presenter & akció, amelyre irányítani kell, ha paraméterek nélkül nyitjuk meg az oldalt, pl. `http://example.com/`. - -```php -// az alapértelmezett presenter 'Home' lesz, az akció pedig 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Javasoljuk a SimpleRouter közvetlen definiálását a [konfigurációban |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO és kanonizáció -================== - -A keretrendszer hozzájárul a SEO-hoz (keresőoptimalizálás) azáltal, hogy megakadályozza a duplikált tartalom létezését különböző URL-eken. Ha egy bizonyos célhoz több cím vezet, pl. `/index` és `/index.html`, a keretrendszer az elsőt elsődlegesnek (kanonikusnak) határozza meg, a többit pedig 301-es HTTP kóddal átirányítja rá. Ennek köszönhetően a keresőmotorok nem indexelik kétszer az oldalakat, és nem osztják meg a page rankjüket. - -Ezt a folyamatot kanonizációnak nevezik. A kanonikus URL az, amelyet a router generál, azaz az első megfelelő route a gyűjteményben OneWay jelző nélkül. Ezért a gyűjteményben **az elsődleges route-okat adjuk meg először**. - -A kanonizációt a presenter végzi, további információk a [kanonizáció |presenters#Kanonizáció] fejezetben. - - -HTTPS -===== - -Ahhoz, hogy a HTTPS protokollt használhassuk, engedélyezni kell a hostingen és helyesen kell konfigurálni a szervert. - -Az egész weboldal HTTPS-re való átirányítását a szerver szintjén kell beállítani, például a `.htaccess` fájl segítségével az alkalmazásunk gyökérkönyvtárában, 301-es HTTP kóddal. A beállítás eltérhet a hostingtól függően, és kb. így néz ki: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -A router ugyanazzal a protokollal generálja az URL-eket, amellyel az oldal betöltődött, így semmi mást nem kell beállítani. - -Ha azonban kivételesen szükségünk van arra, hogy különböző route-ok különböző protokollok alatt fussanak, azt a route maszkjában adjuk meg: - -```php -// HTTP-vel fog címet generálni -$router->addRoute('http://%host%//', /* ... */); - -// HTTPS-sel fog címet generálni -$router->addRoute('https://%host%//', /* ... */); -``` - - -Router debuggolása -================== - -A [Tracy Barban |tracy:] megjelenő routing panel hasznos segítő, amely megjeleníti a route-ok listáját és azokat a paramétereket is, amelyeket a router az URL-ből nyert. - -A zöld sáv a ✓ szimbólummal azt a route-ot jelöli, amely feldolgozta az aktuális URL-t, a kék szín és a ≈ szimbólum azokat a route-okat jelöli, amelyek szintén feldolgozták volna az URL-t, ha a zöld nem előzte volna meg őket. Továbbá látjuk az aktuális presentert & akciót. - -[* routing-debugger.webp *] - -Ugyanakkor, ha váratlan átirányítás történik a [kanonizáció |#SEO és kanonizáció] miatt, hasznos megnézni a *redirect* sávban lévő panelt, ahol megtudhatja, hogyan értelmezte a router eredetileg az URL-t, és miért irányított át. - -.[note] -A router debuggolásakor javasoljuk a Developer Tools (Ctrl+Shift+I vagy Cmd+Option+I) megnyitását a böngészőben, és a Network panelen a cache kikapcsolását, hogy az átirányítások ne kerüljenek bele. - - -Teljesítmény -============ - -A route-ok száma befolyásolja a router sebességét. Számuknak semmiképpen sem szabadna meghaladnia a néhány tucatot. Ha a weboldalának túl bonyolult az URL struktúrája, írhat saját, testreszabott [#Saját router] routert. - -Ha a routernek nincsenek függőségei, például adatbázisra, és a factory-ja nem fogad argumentumokat, akkor az összeállított formáját közvetlenül a DI konténerbe szerializálhatjuk, és ezzel kissé felgyorsíthatjuk az alkalmazást. - -```neon -routing: - cache: true -``` - - -Saját router -============ - -A következő sorok nagyon haladó felhasználóknak szólnak. Létrehozhat saját routert, és teljesen természetesen beillesztheti a route gyűjteménybe. A router a [api:Nette\Routing\Router] interfész implementációja két metódussal: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -A `match` metódus feldolgozza az aktuális kérést [$httpRequest |http:request], amelyből nemcsak az URL-t, hanem a fejléceket stb. is meg lehet szerezni, egy tömbbe, amely tartalmazza a presenter nevét és annak paramétereit. Ha nem tudja feldolgozni a kérést, null-t ad vissza. A kérés feldolgozásakor legalább a presentert és az akciót vissza kell adnunk. A presenter neve teljes, és tartalmazza az esetleges modulokat is: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -A `constructUrl` metódus fordítva, a paraméterek tömbjéből állítja össze a végső abszolút URL-t. Ehhez felhasználhatja a [`$refUrl`|api:Nette\Http\UrlScript] paraméterből származó információkat, ami az aktuális URL. - -A route gyűjteményhez az `add()` segítségével adhatja hozzá: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Önálló használat -================ - -Önálló használat alatt azt értjük, hogy a router képességeit olyan alkalmazásban használjuk, amely nem használja a Nette Applicationt és a presentereket. Szinte minden érvényes rá, amit ebben a fejezetben megmutattunk, a következő különbségekkel: - -- route gyűjteményekhez a [api:Nette\Routing\RouteList] osztályt használjuk -- simple routerként a [api:Nette\Routing\SimpleRouter] osztályt -- mivel nincs `Presenter:action` pár, a [#Bővített jelölés] jelölést használjuk - -Tehát ismét létrehozunk egy metódust, amely összeállítja nekünk a routert, pl.: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Ha DI konténert használ, amit javasolunk, ismét hozzáadjuk a metódust a konfigurációhoz, majd a routert a HTTP kéréssel együtt megszerezzük a konténerből: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Vagy közvetlenül létrehozzuk az objektumokat: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Most már csak hagyni kell a routert dolgozni: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // nem találtunk megfelelő route-ot, 404-es hibát küldünk - exit; -} - -// feldolgozzuk a kapott paramétereket -$controller = $params['controller']; -// ... -``` - -És fordítva, a routert használjuk a link összeállításához: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/router}} diff --git a/application/hu/templates.texy b/application/hu/templates.texy deleted file mode 100644 index bc60ec602d..0000000000 --- a/application/hu/templates.texy +++ /dev/null @@ -1,323 +0,0 @@ -Sablonok -******** - -.[perex] -A Nette a [Latte |latte:] sablonrendszert használja. Egyrészt azért, mert ez a legbiztonságosabb sablonrendszer PHP-hoz, másrészt pedig a legintuitívabb rendszer. Nem kell sok újat tanulnia, elegendő a PHP ismerete és néhány tag. - -Gyakori, hogy egy oldal egy layout sablonból + az adott akció sablonjából áll össze. Így nézhet ki például egy layout sablon, figyelje meg a `{block}` blokkokat és a `{include}` taget: - -```latte - - - - {block title}Saját Alkalmazás{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -És ez lesz az akció sablonja: - -```latte -{block title}Kezdőlap{/block} - -{block content} -

    Kezdőlap

    -... -{/block} -``` - -Ez definiálja a `content` blokkot, amely a `{include content}` helyére kerül a layoutban, és újra definiálja a `title` blokkot, amely felülírja a `{block title}`-t a layoutban. Próbálja meg elképzelni az eredményt. - - -Sablonok keresése ------------------ - -Nem kell a presenterekben megadnia, hogy melyik sablont kell renderelni, a keretrendszer maga vezeti le az utat, és megspórolja Önnek az írást. - -Ha olyan könyvtárstruktúrát használ, ahol minden presenternek saját könyvtára van, egyszerűen helyezze el a sablont ebben a könyvtárban az akció (ill. view) nevével, azaz a `default` akcióhoz használja a `default.latte` sablont: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Ha olyan struktúrát használ, ahol a presenterek egy könyvtárban vannak, a sablonok pedig a `templates` mappában, mentse el vagy a `..latte` vagy a `/.latte` fájlba: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1. változat - └── Home/ - └── default.latte ← 2. változat -\-- - -A `templates` könyvtár egy szinttel feljebb is elhelyezkedhet, azaz ugyanazon a szinten, mint a presenter osztályokat tartalmazó könyvtár. - -Ha a sablon nem található, a presenter [404 - page not found hibával |presenters#Hiba 404 és társai] válaszol. - -A view-t a `$this->setView('jineView')` segítségével változtathatja meg. Közvetlenül is megadhatja a sablonfájlt a `$this->template->setFile('/path/to/template.latte')` segítségével. - -.[note] -A fájlokat, ahol a sablonokat keresi, meg lehet változtatni a [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()] metódus felülírásával, amely visszaadja a lehetséges fájlnevek tömbjét. - - -Layout sablon keresése ----------------------- - -A Nette automatikusan megkeresi a layout fájlt is. - -Ha olyan könyvtárstruktúrát használ, ahol minden presenternek saját könyvtára van, helyezze el a layoutot vagy a presenter mappájában, ha csak rá specifikus, vagy egy szinttel feljebb, ha több presenter számára közös: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← közös layout - └── Home/ - ├── @layout.latte ← csak a Home presenterhez - ├── HomePresenter.php - └── default.latte -\-- - -Ha olyan struktúrát használ, ahol a presenterek egy könyvtárban vannak, a sablonok pedig a `templates` mappában, a layoutot ezeken a helyeken várja: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← közös layout - ├── Home.@layout.latte ← csak a Home-hoz, 1. változat - └── Home/ - └── @layout.latte ← csak a Home-hoz, 2. változat -\-- - -Ha a presenter egy modulban található, akkor további könyvtárszinteken is keresni fog, a modul beágyazási mélységétől függően. - -A layout nevét a `$this->setLayout('layoutAdmin')` segítségével lehet megváltoztatni, és akkor a `@layoutAdmin.latte` fájlban várja. Közvetlenül is megadhatja a layout sablonfájlt a `$this->setLayout('/path/to/template.latte')` segítségével. - -A `$this->setLayout(false)` vagy a `{layout none}` tag használatával a sablonon belül kikapcsolható a layout keresése. - -.[note] -A fájlokat, ahol a layout sablonokat keresi, meg lehet változtatni a [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()] metódus felülírásával, amely visszaadja a lehetséges fájlnevek tömbjét. - - -Változók a sablonban --------------------- - -Változókat úgy adunk át a sablonnak, hogy beírjuk őket a `$this->template`-be, és utána lokális változókként érhetők el a sablonban: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Így egyszerűen bármilyen változót átadhatunk a sablonoknak. Robusztus alkalmazások fejlesztésekor azonban hasznosabb korlátozni magunkat. Például úgy, hogy explicit módon definiáljuk a sablon által várt változók listáját és azok típusait. Ennek köszönhetően a PHP ellenőrizni tudja a típusokat, az IDE helyesen tud súgni, és a statikus analízis felfedezheti a hibákat. - -És hogyan definiálunk egy ilyen listát? Egyszerűen egy osztály és annak property-jei formájában. Nevezzük el hasonlóan a presenterhez, csak `Template` végződéssel: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // és további változók -} -``` - -A `$this->template` objektum a presenterben mostantól az `ArticleTemplate` osztály példánya lesz. Így a PHP ellenőrizni fogja a deklarált típusokat íráskor. És a PHP 8.2-es verziójától kezdve figyelmeztet a nem létező változóba való írásra is, korábbi verziókban ugyanezt a [Nette\SmartObject |utils:smartobject] trait használatával lehet elérni. - -Az `@property-read` annotáció az IDE-nek és a statikus analízisnek szól, ennek köszönhetően működni fog a súgó, lásd "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -A súgó luxusát a sablonokban is élvezheti, csak telepíteni kell a PhpStorm-ba a Latte plugint, és a sablon elejére beírni az osztály nevét, további információk a "Latte: hogyan a típusrendszerre":https://blog.nette.org/hu/latte-how-to-use-type-system cikkben: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Így működnek a sablonok a komponensekben is, csak be kell tartani a névkonvenciót, és például a `FifteenControl` komponenshez létrehozni egy `FifteenTemplate` sablonosztályt. - -Ha a `$template`-et egy másik osztály példányaként kell létrehoznia, használja a `createTemplate()` metódust: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Alapértelmezett változók ------------------------- - -A presenterek és komponensek automatikusan átadnak néhány hasznos változót a sablonoknak: - -- `$basePath` az abszolút URL elérési út a gyökérkönyvtárhoz (pl. `/eshop`) -- `$baseUrl` az abszolút URL a gyökérkönyvtárhoz (pl. `http://localhost/eshop`) -- `$user` a [felhasználót reprezentáló |security:authentication] objektum -- `$presenter` az aktuális presenter -- `$control` az aktuális komponens vagy presenter -- `$flashes` a `flashMessage()` függvénnyel küldött [üzenetek |presenters#Flash üzenetek] tömbje - -Ha saját sablonosztályt használ, ezek a változók átadódnak, ha létrehoz hozzájuk property-t. - - -Linkek létrehozása ------------------- - -A sablonban a linkek más presenterekhez & akciókhoz a következőképpen hozhatók létre: - -```latte -termék részletei -``` - -Az `n:href` attribútum nagyon praktikus a HTML `` tag-ekhez. Ha máshol szeretnénk kiírni a linket, például szövegben, használjuk a `{link}`-et: - -```latte -A cím: {link Home:default} -``` - -További információkat az [URL linkek létrehozása|creating-links] fejezetben talál. - - -Saját szűrők, tag-ek stb. -------------------------- - -A Latte sablonrendszert ki lehet bővíteni saját szűrőkkel, függvényekkel, tag-ekkel stb. Ezt meg lehet tenni közvetlenül a `render` vagy `beforeRender()` metódusban: - -```php -public function beforeRender(): void -{ - // szűrő hozzáadása - $this->template->addFilter('foo', /* ... */); - - // vagy közvetlenül konfiguráljuk a Latte\Engine objektumot - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -A Latte 3-as verziója fejlettebb módszert kínál, mégpedig egy [extension |latte:extending-latte#Latte Extension] létrehozását minden webprojekthez. Egy ilyen osztály töredékes példája: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Regisztráljuk a [konfiguráció |configuration#Latte sablonok] segítségével: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Fordítás --------- - -Ha többnyelvű alkalmazást programoz, valószínűleg szüksége lesz néhány szöveg lefordítására a sablonban különböző nyelvekre. A Nette Framework erre a célra definiál egy interfészt a fordításhoz [api:Nette\Localization\Translator], amelynek egyetlen metódusa van, a `translate()`. Ez fogadja az üzenetet `$message`, ami általában egy string, és tetszőleges további paramétereket. Feladata a lefordított string visszaadása. A Nette-ben nincs alapértelmezett implementáció, választhat igényei szerint több kész megoldás közül, amelyeket a [Componette |https://componette.org/search/localization] oldalon talál. Dokumentációjukban megtudhatja, hogyan konfigurálja a translatort. - -A sablonoknak beállítható egy fordító, amelyet [átkérünk |dependency-injection:passing-dependencies], a `setTranslator()` metódussal: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -A translatort alternatívaként be lehet állítani a [konfiguráció |configuration#Latte sablonok] segítségével is: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Ezután a fordítót például használhatjuk `|translate` szűrőként, beleértve a kiegészítő paramétereket is, amelyek átadódnak a `translate()` metódusnak (lásd `foo, bar`): - -```latte -{='Kosár'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Vagy aláhúzásos tagként: - -```latte -{_'Kosár'} -{_$item} -{_$item, foo, bar} -``` - -A sablon egy szakaszának fordításához létezik egy páros `{translate}` tag (Latte 2.11-től, korábban a `{_}` tag volt használatos): - -```latte -{translate}Rendelés{/translate} -{translate foo, bar}Rendelés{/translate} -``` - -A translator alapértelmezés szerint futásidőben hívódik meg a sablon renderelésekor. A Latte 3-as verziója azonban képes az összes statikus szöveget már a sablon fordítása során lefordítani. Ezzel teljesítményt takarítunk meg, mert minden string csak egyszer fordítódik le, és az eredményül kapott fordítás beíródik a lefordított formába. A cache könyvtárban így több lefordított sablonverzió jön létre, minden nyelvre egy. Ehhez elegendő csak a nyelvet második paraméterként megadni: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Statikus szöveg alatt például a `{_'hello'}` vagy `{translate}hello{/translate}` értendő. A nem statikus szövegek, mint például a `{_$foo}`, továbbra is futásidőben fordítódnak. diff --git a/application/pt/@home.texy b/application/pt/@home.texy deleted file mode 100644 index 5f79b47fc4..0000000000 --- a/application/pt/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application é o núcleo do Nette Framework, que fornece ferramentas poderosas para criar aplicações web modernas. Oferece uma série de recursos excepcionais que facilitam significativamente o desenvolvimento e melhoram a segurança e a manutenção do código. - - -Instalação ----------- - -Faça o download e instale a biblioteca usando a ferramenta [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Porquê escolher Nette Application? ----------------------------------- - -Nette sempre foi pioneiro no campo das tecnologias web. - -**Roteador bidirecional:** Nette possui um sistema de roteamento avançado que é único pela sua bidirecionalidade - não só traduz URLs para ações da aplicação, mas também consegue gerar URLs de volta. Isso significa que: -- Pode alterar a estrutura de URLs de toda a aplicação a qualquer momento sem precisar de editar os templates -- As URLs são automaticamente canonizadas, o que melhora o SEO -- O roteamento é definido num único local, em vez de espalhado em anotações - -**Componentes e sinais:** O sistema de componentes integrado, inspirado no Delphi e React.js, é completamente excecional entre os frameworks PHP: -- Permite criar elementos de UI reutilizáveis -- Suporta composição hierárquica de componentes -- Oferece um tratamento elegante de requisições AJAX usando sinais -- Uma vasta biblioteca de componentes prontos em [Componette](https://componette.org) - -**AJAX e snippets:** Nette introduziu uma forma revolucionária de trabalhar com AJAX já em 2009, muito antes de soluções semelhantes como Hotwire para Ruby on Rails ou Symfony UX Turbo: -- Snippets permitem atualizar apenas partes da página sem a necessidade de escrever JavaScript -- Integração automática com o sistema de componentes -- Invalidação inteligente de partes das páginas -- Quantidade mínima de dados transferidos - -**Templates intuitivos [Latte|latte:]:** O sistema de templates mais seguro para PHP com recursos avançados: -- Proteção automática contra XSS com escaping sensível ao contexto -- Extensibilidade através de filtros, funções e tags personalizadas -- Herança de templates e snippets para AJAX -- Excelente suporte a PHP 8.x com sistema de tipos - -**Dependency Injection:** Nette utiliza totalmente a Injeção de Dependência: -- Passagem automática de dependências (autowiring) -- Configuração através do formato claro NEON -- Suporte para fábricas de componentes - - -Principais vantagens --------------------- - -- **Segurança**: Defesa automática contra [vulnerabilidades|nette:vulnerability-protection] como XSS, CSRF, etc. -- **Produtividade**: Menos escrita, mais funções graças a um design inteligente -- **Depuração**: [Depurador Tracy|tracy:] com painel de roteamento -- **Desempenho**: Cache inteligente, lazy loading de componentes -- **Flexibilidade**: Fácil modificação de URLs mesmo após a conclusão da aplicação -- **Componentes**: Sistema único de elementos de UI reutilizáveis -- **Moderno**: Suporte total a PHP 8.4+ e sistema de tipos - - -Começando ---------- - -1. [Como funcionam as aplicações? |how-it-works] - Compreender a arquitetura básica -2. [Presenters |presenters] - Trabalhar com presenters e ações -3. [Templates |templates] - Criar templates em Latte -4. [Roteamento |routing] - Configurar endereços URL -5. [Componentes interativos |components] - Utilizar o sistema de componentes - - -Compatibilidade com PHP ------------------------ - -| versão | compatível com 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 - -Aplica-se à última versão de patch. diff --git a/application/pt/@left-menu.texy b/application/pt/@left-menu.texy deleted file mode 100644 index 9f525b8f0e..0000000000 --- a/application/pt/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Como funcionam as aplicações? |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Templates |templates] -- [Estrutura de diretórios |directory-structure] -- [Roteamento |routing] -- [Criando links URL |creating-links] -- [Componentes interativos |components] -- [AJAX & snippets |ajax] -- [Multiplier |multiplier] -- [Configuração |configuration] - - -Leitura adicional -***************** -- [Por que usar o Nette? |www:10-reasons-why-nette] -- [Instalação |nette:installation] -- [Escrevendo a primeira aplicação! |quickstart:] -- [Guias e melhores práticas |best-practices:] -- [Solução de problemas |nette:troubleshooting] diff --git a/application/pt/@meta.texy b/application/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/application/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/application/pt/ajax.texy b/application/pt/ajax.texy deleted file mode 100644 index b9ffa8bd66..0000000000 --- a/application/pt/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & Snippets -*************** - -
    - -Na era das aplicações web modernas, onde a funcionalidade é frequentemente dividida entre o servidor e o navegador, o AJAX é um elemento de ligação essencial. Que opções o Nette Framework nos oferece nesta área? -- envio de partes do template, os chamados snippets -- passagem de variáveis entre PHP e JavaScript -- ferramentas para depuração de requisições AJAX - -
    - - -Requisição AJAX -=============== - -Uma requisição AJAX, em princípio, não difere de uma requisição HTTP clássica. Um presenter é chamado com determinados parâmetros. E cabe ao presenter decidir como responder à requisição - ele pode retornar dados em formato JSON, enviar uma parte do código HTML, um documento XML, etc. - -No lado do navegador, inicializamos a requisição AJAX usando a função `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // processamento da resposta -}); -``` - -No lado do servidor, reconhecemos uma requisição AJAX usando o método `$httpRequest->isAjax()` do serviço [encapsulando a requisição HTTP |http:request]. Para a deteção, ele usa o cabeçalho HTTP `X-Requested-With`, por isso é importante enviá-lo. Dentro do presenter, pode-se usar o método `$this->isAjax()`. - -Se desejar enviar dados em formato JSON, use o método [`sendJson()` |presenters#Envio da resposta]. O método também encerra a atividade do presenter. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Se você planeja responder com um template especial destinado ao AJAX, pode fazê-lo da seguinte forma: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippets -======== - -O recurso mais poderoso que o Nette oferece para conectar o servidor ao cliente são os snippets. Graças a eles, você pode transformar uma aplicação comum em uma aplicação AJAX com esforço mínimo e algumas linhas de código. O exemplo Fifteen demonstra como tudo funciona, e seu código pode ser encontrado no [GitHub |https://github.com/nette-examples/fifteen]. - -Snippets, ou trechos, permitem atualizar apenas partes da página, em vez de recarregar a página inteira. Isso não só é mais rápido e eficiente, mas também proporciona uma experiência de usuário mais confortável. Os snippets podem lembrá-lo do Hotwire para Ruby on Rails ou do Symfony UX Turbo. Curiosamente, o Nette introduziu os snippets 14 anos antes. - -Como os snippets funcionam? No primeiro carregamento da página (requisição não-AJAX), a página inteira é carregada, incluindo todos os snippets. Quando o usuário interage com a página (por exemplo, clica em um botão, envia um formulário, etc.), em vez de carregar a página inteira, uma requisição AJAX é disparada. O código no presenter executa a ação e decide quais snippets precisam ser atualizados. O Nette renderiza esses snippets e os envia como um array em formato JSON. O código de manipulação no navegador insere os snippets recebidos de volta na página. Assim, apenas o código dos snippets alterados é transmitido, economizando largura de banda e acelerando o carregamento em comparação com a transferência do conteúdo da página inteira. - - -Naja ----- - -Para manipular snippets no lado do navegador, utiliza-se a [biblioteca Naja |https://naja.js.org]. [Instale-a |https://naja.js.org/#/guide/01-install-setup-naja] como um pacote node.js (para uso com aplicações Webpack, Rollup, Vite, Parcel e outras): - -```shell -npm install naja -``` - -…ou insira-a diretamente no template da página: - -```latte - -``` - -Primeiro, é necessário [inicializar |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] a biblioteca: - -```js -naja.initialize(); -``` - -Para transformar um link comum (sinal) ou o envio de um formulário em uma requisição AJAX, basta marcar o link, formulário ou botão correspondente com a classe `ajax`: - -```latte -Go - -
    - -
    - -ou - -
    - -
    -``` - - -Redesenho de snippets ---------------------- - -Cada objeto da classe [Control |components] (incluindo o próprio Presenter) regista se ocorreram alterações que exigem o seu redesenho. Para isso, serve o método `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // após o login, é necessário redesenhar a parte relevante - $this->redrawControl(); - // ... -} -``` - -O Nette permite um controlo ainda mais fino do que deve ser redesenhado. O método mencionado pode receber o nome do snippet como argumento. Assim, é possível invalidar (entenda-se: forçar o redesenho) ao nível das partes do template. Se todo o componente for invalidado, cada um dos seus snippets também será redesenhado: - -```php -// invalida o snippet 'header' -$this->redrawControl('header'); -``` - - -Snippets em Latte ------------------ - -Usar snippets em Latte é extremamente fácil. Para definir uma parte do template como um snippet, basta envolvê-la com as tags `{snippet}` e `{/snippet}`: - -```latte -{snippet header} -

    Olá ...

    -{/snippet} -``` - -O snippet cria um elemento `
    ` na página HTML com um `id` especial gerado. Ao redesenhar o snippet, o conteúdo desse elemento é atualizado. Por isso, é necessário que, na renderização inicial da página, todos os snippets também sejam renderizados, mesmo que possam estar vazios no início. - -Você também pode criar um snippet com um elemento diferente de `
    ` usando o n:atributo: - -```latte -
    -

    Olá ...

    -
    -``` - - -Áreas de Snippets ------------------ - -Os nomes dos snippets também podem ser expressões: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Assim, teremos vários snippets `item-0`, `item-1`, etc. Se invalidássemos diretamente um snippet dinâmico (por exemplo, `item-1`), nada seria redesenhado. A razão é que os snippets funcionam realmente como recortes e apenas eles próprios são renderizados diretamente. No entanto, no template, não existe de facto nenhum snippet chamado `item-1`. Ele só surge com a execução do código ao redor do snippet, ou seja, o ciclo foreach. Portanto, marcamos a parte do template que deve ser executada usando a tag `{snippetArea}`: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -E mandamos redesenhar tanto o snippet em si quanto toda a área pai: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Ao mesmo tempo, é aconselhável garantir que o array `$items` contenha apenas os itens que devem ser redesenhados. - -Se incluirmos outro template que contém snippets no template usando a tag `{include}`, é necessário envolver a inclusão do template novamente em `snippetArea` e invalidá-la junto com o snippet: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippets em Componentes ------------------------ - -Você também pode criar snippets em [componentes|components] e o Nette irá redesenhá-los automaticamente. Mas existe uma certa limitação: para redesenhar os snippets, ele chama o método `render()` sem parâmetros. Portanto, a passagem de parâmetros no template não funcionará: - -```latte -OK -{control productGrid} - -não funcionará: -{control productGrid $arg, $arg} -{control productGrid:paginator} -``` - - -Envio de dados do usuário -------------------------- - -Juntamente com os snippets, você pode enviar quaisquer outros dados para o cliente. Basta escrevê-los no objeto `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Sucesso'; - } -} -``` - - -Passagem de parâmetros -====================== - -Se enviarmos parâmetros para um componente através de uma requisição AJAX, sejam parâmetros de sinal ou parâmetros persistentes, devemos indicar na requisição o seu nome global, que também inclui o nome do componente. O nome completo do parâmetro é retornado pelo método `getParameterId()`. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -E o método handle com os parâmetros correspondentes no componente: - -```php -public function handleFoo(int $bar): void -{ -} -``` diff --git a/application/pt/bootstrapping.texy b/application/pt/bootstrapping.texy deleted file mode 100644 index 07fbde0ede..0000000000 --- a/application/pt/bootstrapping.texy +++ /dev/null @@ -1,297 +0,0 @@ -Bootstrapping -************* - -
    - -Bootstrapping é o processo de inicialização do ambiente da aplicação, criação de um contêiner de injeção de dependência (DI) e início da aplicação. Vamos discutir: - -- como a classe Bootstrap inicializa o ambiente -- como as aplicações são configuradas usando arquivos NEON -- como distinguir entre modo de produção e desenvolvimento -- como criar e configurar o contêiner DI - -
    - - -Aplicações, sejam elas web ou scripts executados a partir da linha de comando, começam sua execução com alguma forma de inicialização do ambiente. Antigamente, isso era responsabilidade de um arquivo chamado, por exemplo, `include.inc.php`, que o arquivo inicial incluía. Em aplicações Nette modernas, ele foi substituído pela classe `Bootstrap`, que, como parte da aplicação, pode ser encontrada no arquivo `app/Bootstrap.php`. Pode parecer, por exemplo, assim: - -```php -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // O Configurator é responsável por configurar o ambiente da aplicação e os serviços. - $this->configurator = new Configurator; - // Define o diretório para arquivos temporários gerados pelo Nette (por exemplo, templates compilados) - $this->configurator->setTempDirectory($this->rootDir . '/temp'); - } - - public function bootWebApplication(): Nette\DI\Container - { - $this->initializeEnvironment(); - $this->setupContainer(); - return $this->configurator->createContainer(); - } - - private function initializeEnvironment(): void - { - // O Nette é inteligente e o modo de desenvolvimento é ativado automaticamente, - // ou você pode habilitá-lo para um endereço IP específico descomentando a linha seguinte: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Ativa o Tracy: o "canivete suíço" definitivo para depuração. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: carrega automaticamente todas as classes no diretório selecionado - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Carrega os arquivos de configuração - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -O arquivo inicial no caso de aplicações web é `index.php`, localizado no [diretório público |directory-structure#Diretório público www] `www/`. Ele solicita à classe Bootstrap que inicialize o ambiente e crie o contêiner de DI. Em seguida, obtém o serviço `Application` dele, que inicia a aplicação web: - -```php -$bootstrap = new App\Bootstrap; -// Inicialização do ambiente + criação do contêiner de DI -$container = $bootstrap->bootWebApplication(); -// O contêiner de DI cria o objeto Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Inicia a aplicação Nette e processa a requisição recebida -$application->run(); -``` - -Como pode ser visto, a classe [api:Nette\Bootstrap\Configurator] ajuda na configuração do ambiente e na criação do contêiner de injeção de dependência (DI), que agora apresentaremos em mais detalhes. - - -Modo de desenvolvimento vs produção -=================================== - -O Nette se comporta de maneira diferente dependendo se está sendo executado em um servidor de desenvolvimento ou de produção: - -🛠️ Modo de desenvolvimento (Development): - - Exibe a barra de depuração do Tracy com informações úteis (consultas SQL, tempo de execução, memória usada) - - Em caso de erro, exibe uma página de erro detalhada com a pilha de chamadas de funções e o conteúdo das variáveis - - Atualiza automaticamente o cache quando os templates Latte são alterados, os arquivos de configuração são modificados, etc. - - -🚀 Modo de produção (Production): - - Não exibe nenhuma informação de depuração, todos os erros são registrados no log - - Em caso de erro, exibe o ErrorPresenter ou uma página genérica "Server Error" - - O cache nunca é atualizado automaticamente! - - Otimizado para velocidade e segurança - - -A seleção do modo é feita por autodeteção, portanto, geralmente não é necessário configurar nada ou alternar manualmente: - -- modo de desenvolvimento: em localhost (endereço IP `127.0.0.1` ou `::1`) se não houver proxy presente (ou seja, seu cabeçalho HTTP) -- modo de produção: em todos os outros lugares - -Se quisermos habilitar o modo de desenvolvimento em outros casos, por exemplo, para programadores acessando de um endereço IP específico, usamos `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // também pode ser fornecido um array de endereços IP -``` - -Recomendamos fortemente combinar o endereço IP com um cookie. Armazenamos um token secreto no cookie `nette-debug`, por exemplo, `secret1234`, e desta forma ativamos o modo de desenvolvimento para programadores acessando de um endereço IP específico e que também possuem o token mencionado no cookie: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Também podemos desativar completamente o modo de desenvolvimento, mesmo para localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Atenção, o valor `true` ativa o modo de desenvolvimento permanentemente, o que nunca deve acontecer em um servidor de produção. - - -Ferramenta de depuração Tracy -============================= - -Para facilitar a depuração, ativamos também a excelente ferramenta [Tracy |tracy:]. No modo de desenvolvimento, ela visualiza os erros e, no modo de produção, registra os erros no diretório especificado: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Arquivos temporários -==================== - -O Nette utiliza cache para o contêiner de DI, RobotLoader, templates, etc. Portanto, é necessário definir o caminho para o diretório onde o cache será armazenado: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -No Linux ou macOS, defina as [permissões de escrita |nette:troubleshooting#Configurando Permissões de Diretório] para os diretórios `log/` e `temp/`. - - -RobotLoader -=========== - -Geralmente, queremos carregar classes automaticamente usando o [RobotLoader |robot-loader:], então precisamos iniciá-lo e deixá-lo carregar classes do diretório onde `Bootstrap.php` está localizado (ou seja, `__DIR__`), e de todos os subdiretórios: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Uma abordagem alternativa é deixar as classes serem carregadas apenas através do [Composer |best-practices:composer] seguindo o PSR-4. - - -Fuso horário -============ - -Através do configurator, você pode definir o fuso horário padrão. - -```php -$this->configurator->setTimeZone('Europe/Lisbon'); -``` - - -Configuração do contêiner de DI -=============================== - -Parte do processo de inicialização é a criação do contêiner de DI, ou seja, a fábrica de objetos, que é o coração de toda a aplicação. Na verdade, é uma classe PHP gerada pelo Nette e armazenada no diretório de cache. A fábrica produz os objetos chave da aplicação e, por meio de arquivos de configuração, a instruímos sobre como criá-los e configurá-los, influenciando assim o comportamento de toda a aplicação. - -Os arquivos de configuração são geralmente escritos no formato [NEON |neon:format]. Em um capítulo separado, você aprenderá [o que pode ser configurado |nette:configuring]. - -.[tip] -No modo de desenvolvimento, o contêiner é atualizado automaticamente a cada alteração no código ou nos arquivos de configuração. No modo de produção, ele é gerado apenas uma vez e as alterações não são verificadas para maximizar o desempenho. - -Carregamos os arquivos de configuração usando `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Se quisermos adicionar mais arquivos de configuração, podemos chamar a função `addConfig()` várias vezes. - -```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'); -} -``` - -O nome `cli.php` não é um erro de digitação, a configuração também pode ser escrita em um arquivo PHP, que a retorna como um array. - -Também podemos adicionar outros arquivos de configuração na [seção `includes` |dependency-injection:configuration#Inclusão de arquivos]. - -Se elementos com as mesmas chaves aparecerem nos arquivos de configuração, eles serão sobrescritos ou, no caso de [arrays, mesclados |dependency-injection:configuration#Mesclagem]. O arquivo incluído posteriormente tem prioridade maior que o anterior. O arquivo em que a seção `includes` é listada tem prioridade maior do que os arquivos incluídos nele. - - -Parâmetros estáticos --------------------- - -Parâmetros usados nos arquivos de configuração podem ser definidos [na seção `parameters` |dependency-injection:configuration#Parâmetros] e também podem ser passados (ou sobrescritos) pelo método `addStaticParameters()` (tem o alias `addParameters()`). É importante que diferentes valores de parâmetros causem a geração de contêineres de DI adicionais, ou seja, classes adicionais. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -O parâmetro `projectId` pode ser referenciado na configuração usando a notação usual `%projectId%`. - - -Parâmetros dinâmicos --------------------- - -Também podemos adicionar parâmetros dinâmicos ao contêiner, cujos diferentes valores, ao contrário dos parâmetros estáticos, não causam a geração de novos contêineres de DI. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Assim, podemos adicionar facilmente, por exemplo, variáveis de ambiente, que podem ser referenciadas na configuração usando a notação `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Parâmetros padrão ------------------ - -Nos arquivos de configuração, você pode usar estes parâmetros estáticos: - -- `%appDir%` é o caminho absoluto para o diretório com o arquivo `Bootstrap.php` -- `%wwwDir%` é o caminho absoluto para o diretório com o arquivo de entrada `index.php` -- `%tempDir%` é o caminho absoluto para o diretório de arquivos temporários -- `%vendorDir%` é o caminho absoluto para o diretório onde o Composer instala as bibliotecas -- `%rootDir%` é o caminho absoluto para o diretório raiz do projeto -- `%debugMode%` indica se a aplicação está em modo de depuração -- `%consoleMode%` indica se a requisição veio da linha de comando - - -Serviços importados -------------------- - -Agora estamos indo mais a fundo. Embora o propósito do contêiner de DI seja criar objetos, excepcionalmente pode surgir a necessidade de inserir um objeto existente no contêiner. Fazemos isso definindo o serviço com o sinalizador `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -E no bootstrap, inserimos o objeto no contêiner: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Ambiente diferente -================== - -Não hesite em modificar a classe Bootstrap de acordo com suas necessidades. Você pode adicionar parâmetros ao método `bootWebApplication()` para distinguir projetos web. Ou podemos adicionar outros métodos, como `bootTestEnvironment()`, que inicializa o ambiente para testes unitários, `bootConsoleApplication()` para scripts chamados da linha de comando, etc. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // inicialização do Nette Tester - $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/pt/components.texy b/application/pt/components.texy deleted file mode 100644 index c6d4d414a4..0000000000 --- a/application/pt/components.texy +++ /dev/null @@ -1,485 +0,0 @@ -Componentes interativos -*********************** - -
    - -Componentes são objetos reutilizáveis independentes que inserimos nas páginas. Podem ser formulários, datagrids, enquetes, na verdade, qualquer coisa que faça sentido usar repetidamente. Vamos mostrar: - -- como usar componentes? -- como escrevê-los? -- o que são sinais? - -
    - -O Nette possui um sistema de componentes embutido. Algo semelhante pode ser familiar para veteranos do Delphi ou ASP.NET Web Forms, e algo remotamente parecido é a base do React ou Vue.js. No entanto, no mundo dos frameworks PHP, é uma característica única. - -Ao mesmo tempo, os componentes influenciam fundamentalmente a abordagem para a criação de aplicações. Você pode montar páginas a partir de unidades pré-preparadas. Precisa de um datagrid na administração? Encontre-o na [Componette |https://componette.org/search/component], um repositório de add-ons open-source (ou seja, não apenas componentes) para o Nette e simplesmente insira-o no presenter. - -Você pode incorporar qualquer número de componentes em um presenter. E em alguns componentes, você pode inserir outros componentes. Isso cria uma árvore de componentes, cuja raiz é o presenter. - - -Métodos de fábrica -================== - -Como os componentes são inseridos no presenter e subsequentemente usados? Geralmente através de métodos de fábrica. - -A fábrica de componentes representa uma maneira elegante de criar componentes apenas quando eles são realmente necessários (lazy / on demand). Toda a mágica reside na implementação de um método chamado `createComponent()`, onde `` é o nome do componente a ser criado, e que cria e retorna o componente. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->item; - return $poll; - } -} -``` - -Graças ao fato de que todos os componentes são criados em métodos separados, o código ganha clareza. - -.[note] -Os nomes dos componentes sempre começam com letra minúscula, embora no nome do método sejam escritos com letra maiúscula. - -As fábricas nunca são chamadas diretamente; elas são chamadas automaticamente na primeira vez que usamos o componente. Graças a isso, o componente é criado no momento certo e apenas se for realmente necessário. Se não usarmos o componente (por exemplo, durante uma requisição AJAX em que apenas parte da página é transferida, ou ao armazenar o template em cache), ele não será criado de forma alguma e economizaremos o desempenho do servidor. - -```php .{file:DefaultPresenter.php} -// acessamos o componente e, se for a primeira vez, -// createComponentPoll() é chamado para criá-lo -$poll = $this->getComponent('poll'); -// sintaxe alternativa: $poll = $this['poll']; -``` - -No template, é possível renderizar o componente usando a tag [{control} |#Renderização]. Portanto, não é necessário passar manualmente os componentes para o template. - -```latte -

    Vote

    - -{control poll} -``` - - -Estilo Hollywood -================ - -Os componentes geralmente usam uma técnica inovadora que gostamos de chamar de Estilo Hollywood. Você certamente conhece a frase famosa que os participantes de audições de cinema ouvem com tanta frequência: "Não nos ligue, nós ligaremos para você". E é exatamente disso que se trata. - -No Nette, em vez de ter que perguntar constantemente ("o formulário foi enviado?", "era válido?" ou "o usuário pressionou este botão?"), você diz ao framework "quando isso acontecer, chame este método" e deixa o resto do trabalho para ele. Se você programa em JavaScript, está intimamente familiarizado com este estilo de programação. Você escreve funções que são chamadas quando um determinado evento ocorre. E a linguagem passa os parâmetros apropriados para elas. - -Isso muda completamente a perspectiva sobre a escrita de aplicações. Quanto mais tarefas você puder deixar para o framework, menos trabalho você terá. E menos coisas você pode esquecer. - - -Escrevendo um componente -======================== - -Sob o termo componente, geralmente entendemos um descendente da classe [api:Nette\Application\UI\Control]. (Seria mais preciso usar o termo "controls", mas "controles" tem um significado diferente em português e "componentes" se tornou mais comum.) O próprio presenter [api:Nette\Application\UI\Presenter] também é, aliás, um descendente da classe `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Renderização -============ - -Já sabemos que para renderizar um componente, usamos a tag `{control componentName}`. Ela basicamente chama o método `render()` do componente, no qual cuidamos da renderização. Temos à nossa disposição, exatamente como no presenter, um [template Latte|templates] na variável `$this->template`, para a qual passamos parâmetros. Ao contrário do presenter, precisamos especificar o arquivo de template e deixá-lo renderizar: - -```php .{file:PollControl.php} -public function render(): void -{ - // inserimos alguns parâmetros no template - $this->template->param = $value; - // e o renderizamos - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -A tag `{control}` permite passar parâmetros para o método `render()`: - -```latte -{control poll $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Às vezes, um componente pode consistir em várias partes que queremos renderizar separadamente. Para cada uma delas, criamos nosso próprio método de renderização, aqui no exemplo, `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -E no template, então a chamamos usando: - -```latte -{control poll:paginator} -``` - -Para uma melhor compreensão, é bom saber como esta tag é traduzida para PHP. - -```latte -{control poll} -{control poll:paginator 123, 'hello'} -``` - -é traduzido como: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -O método `getComponent()` retorna o componente `poll` e chama o método `render()` neste componente, ou `renderPaginator()` se um método de renderização diferente for especificado na tag após os dois pontos. - -.[caution] -Atenção, se **`=>`** aparecer em qualquer lugar nos parâmetros, todos os parâmetros serão agrupados em um array e passados como o primeiro argumento: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -é traduzido como: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Renderização de subcomponente: - -```latte -{control cartControl-someForm} -``` - -é traduzido como: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Componentes, assim como presenters, passam automaticamente várias variáveis úteis para os templates: - -- `$basePath` é o caminho URL absoluto para o diretório raiz (por exemplo, `/loja`) -- `$baseUrl` é a URL absoluta para o diretório raiz (por exemplo, `http://localhost/loja`) -- `$user` é o objeto [representando o usuário |security:authentication] -- `$presenter` é o presenter atual -- `$control` é o componente atual -- `$flashes` array de [mensagens |#Mensagens Flash] enviadas pela função `flashMessage()` - - -Sinal -===== - -Já sabemos que a navegação em uma aplicação Nette consiste em vincular ou redirecionar para pares `Presenter:action`. Mas e se quisermos apenas executar uma ação na **página atual**? Por exemplo, alterar a ordenação das colunas em uma tabela; excluir um item; alternar entre modo claro/escuro; enviar um formulário; votar em uma enquete; etc. - -Esse tipo de requisição é chamado de sinal. E, assim como as ações invocam métodos `action()` ou `render()`, os sinais chamam métodos `handle()`. Enquanto o conceito de ação (ou view) está puramente relacionado aos presenters, os sinais se aplicam a todos os componentes. E, portanto, também aos presenters, porque `UI\Presenter` é um descendente de `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... processamento do sinal ... -} -``` - -Criamos o link que chama o sinal da maneira usual, ou seja, no template com o atributo `n:href` ou a tag `{link}`, no código com o método `link()`. Mais no capítulo [Criando Links URL |creating-links#Links para sinal]. - -```latte -clique aqui -``` - -O sinal é sempre chamado no presenter e action atuais, não é possível chamá-lo em outro presenter ou outra action. - -Portanto, o sinal causa o recarregamento da página exatamente como na requisição original, mas adicionalmente chama o método de manipulação do sinal com os parâmetros apropriados. Se o método não existir, uma exceção [api:Nette\Application\UI\BadSignalException] é lançada, que é exibida ao usuário como uma página de erro 403 Forbidden. - - -Snippets e AJAX -=============== - -Sinais podem lembrá-lo um pouco de AJAX: manipuladores que são invocados na página atual. E você está certo, sinais são frequentemente chamados via AJAX e, subsequentemente, apenas as partes alteradas da página são transmitidas para o navegador. Ou seja, os chamados snippets. Mais informações podem ser encontradas na [página dedicada ao AJAX |ajax]. - - -Mensagens Flash -=============== - -O componente tem seu próprio armazenamento de mensagens flash independente do presenter. São mensagens que, por exemplo, informam sobre o resultado de uma operação. Uma característica importante das mensagens flash é que elas estão disponíveis no template mesmo após um redirecionamento. Mesmo após serem exibidas, elas permanecem ativas por mais 30 segundos - por exemplo, caso o usuário atualize a página devido a um erro de transmissão - a mensagem não desaparecerá imediatamente. - -O envio é feito pelo método [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. O primeiro parâmetro é o texto da mensagem ou um objeto `stdClass` representando a mensagem. O segundo parâmetro opcional é o seu tipo (erro, aviso, informação, etc.). O método `flashMessage()` retorna uma instância da mensagem flash como um objeto `stdClass`, ao qual informações adicionais podem ser adicionadas. - -```php -$this->flashMessage('O item foi excluído.'); -$this->redirect(/* ... */); // e redirecionamos -``` - -No template, essas mensagens estão disponíveis na variável `$flashes` como objetos `stdClass`, que contêm as propriedades `message` (texto da mensagem), `type` (tipo da mensagem) e podem conter as informações do usuário já mencionadas. Nós as renderizamos assim, por exemplo: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Redirecionamento após sinal -=========================== - -Após o processamento de um sinal de componente, frequentemente segue-se um redirecionamento. É uma situação semelhante à dos formulários - após o envio deles, também redirecionamos para que, ao atualizar a página no navegador, os dados não sejam enviados novamente. - -```php -$this->redirect('this'); // redireciona para o presenter e action atuais -``` - -Como o componente é um elemento reutilizável e geralmente não deve ter um vínculo direto com presenters específicos, os métodos `redirect()` e `link()` interpretam automaticamente o parâmetro como um sinal do componente: - -```php -$this->redirect('click'); // redireciona para o sinal 'click' do mesmo componente -``` - -Se precisar redirecionar para outro presenter ou ação, você pode fazer isso através do presenter: - -```php -$this->getPresenter()->redirect('Product:show'); // redireciona para outro presenter/action -``` - - -Parâmetros persistentes -======================= - -Parâmetros persistentes são usados para manter o estado nos componentes entre diferentes requisições. Seu valor permanece o mesmo mesmo após clicar em um link. Ao contrário dos dados na sessão, eles são transmitidos na URL. E isso de forma totalmente automática, inclusive em links criados em outros componentes na mesma página. - -Por exemplo, você tem um componente para paginação de conteúdo. Pode haver vários desses componentes em uma página. E desejamos que, após clicar em um link, todos os componentes permaneçam em sua página atual. Portanto, transformamos o número da página (`page`) em um parâmetro persistente. - -Criar um parâmetro persistente no Nette é extremamente simples. Basta criar uma propriedade pública e marcá-la com um atributo: (anteriormente usava-se `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // esta linha é importante - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // deve ser público -} -``` - -Recomendamos especificar o tipo de dados para a propriedade (por exemplo, `int`) e você também pode especificar um valor padrão. Os valores dos parâmetros podem ser [validados |#Validação de parâmetros persistentes]. - -Ao criar um link, o valor do parâmetro persistente pode ser alterado: - -```latte -próximo -``` - -Ou pode ser *resetado*, ou seja, removido da URL. Então ele assumirá seu valor padrão: - -```latte -resetar -``` - - -Componentes persistentes -======================== - -Não apenas parâmetros, mas também componentes podem ser persistentes. Em tal componente, seus parâmetros persistentes são transmitidos mesmo entre diferentes ações do presenter ou entre vários presenters. Marcamos componentes persistentes com uma anotação na classe do presenter. Por exemplo, marcamos os componentes `calendar` e `poll` assim: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Subcomponentes dentro desses componentes não precisam ser marcados, eles também se tornarão persistentes. - -No PHP 8, você também pode usar atributos para marcar componentes persistentes: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Componentes com dependências -============================ - -Como criar componentes com dependências sem "poluir" os presenters que os usarão? Graças às propriedades inteligentes do contêiner de DI no Nette, assim como no uso de serviços clássicos, podemos deixar a maior parte do trabalho para o framework. - -Vamos pegar como exemplo um componente que tem dependência do serviço `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // ID da enquete para a qual estamos criando o componente - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Se estivéssemos escrevendo um serviço clássico, não haveria problema. O contêiner de DI cuidaria invisivelmente da passagem de todas as dependências. Mas com componentes, geralmente lidamos de forma que criamos sua nova instância diretamente no presenter nos [#métodos de fábrica] `createComponent…()`. Mas passar todas as dependências de todos os componentes para o presenter, para então passá-las aos componentes, é complicado. E a quantidade de código escrito… - -A questão lógica é: por que simplesmente não registramos o componente como um serviço clássico, o passamos para o presenter e depois o retornamos no método `createComponent…()`? Essa abordagem, no entanto, é inadequada, porque queremos ter a possibilidade de criar o componente várias vezes, se necessário. - -A solução correta é escrever uma fábrica para o componente, ou seja, uma classe que criará o componente para nós: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Registramos essa fábrica em nosso contêiner na configuração: - -```neon -services: - - PollControlFactory -``` - -e finalmente a usamos em nosso presenter: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // podemos passar nosso parâmetro - return $this->pollControlFactory->create($pollId); - } -} -``` - -O ótimo é que o Nette DI pode [gerar |dependency-injection:factory] essas fábricas simples, então, em vez de todo o seu código, basta escrever apenas sua interface: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -E isso é tudo. O Nette implementará internamente esta interface e a passará para o presenter, onde já podemos usá-la. Ele magicamente adiciona o parâmetro `$id` e a instância da classe `PollFacade` ao nosso componente. - - -Componentes em profundidade -=========================== - -Componentes na Nette Application representam partes reutilizáveis de uma aplicação web que inserimos nas páginas e às quais, aliás, todo este capítulo é dedicado. Quais são exatamente as capacidades de tal componente? - -1) é renderizável no template -2) sabe [qual parte sua |ajax#Snippets] deve ser renderizada durante uma requisição AJAX (snippets) -3) tem a capacidade de armazenar seu estado na URL (parâmetros persistentes) -4) tem a capacidade de reagir a ações do usuário (sinais) -5) cria uma estrutura hierárquica (onde a raiz é o presenter) - -Cada uma dessas funções é cuidada por alguma das classes da linha de herança. A renderização (1 + 2) é responsabilidade de [api:Nette\Application\UI\Control], a integração no [ciclo de vida |presenters#Ciclo de vida do presenter] (3, 4) da classe [api:Nette\Application\UI\Component] e a criação da estrutura hierárquica (5) das classes [Container e Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Ciclo de vida do componente ---------------------------- - -[* lifecycle-component.svg *] *** *Ciclo de vida do componente* .<> - - -Validação de parâmetros persistentes ------------------------------------- - -Os valores dos [#parâmetros persistentes] recebidos da URL são escritos nas propriedades pelo método `loadState()`. Ele também verifica se o tipo de dados especificado na propriedade corresponde, caso contrário, responde com um erro 404 e a página não é exibida. - -Nunca confie cegamente nos parâmetros persistentes, pois eles podem ser facilmente sobrescritos pelo usuário na URL. Assim, por exemplo, verificamos se o número da página `$this->page` é maior que 0. Uma maneira adequada é sobrescrever o método mencionado `loadState()`: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // aqui $this->page é definido - // segue a verificação personalizada do valor: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -O processo oposto, ou seja, coletar valores das propriedades persistentes, é responsabilidade do método `saveState()`. - - -Sinais em profundidade ----------------------- - -Um sinal causa o recarregamento da página exatamente como na requisição original (exceto quando chamado via AJAX) e invoca o método `signalReceived($signal)`, cuja implementação padrão na classe `Nette\Application\UI\Component` tenta chamar um método composto pelas palavras `handle{signal}`. O processamento adicional depende do objeto em questão. Objetos que herdam de `Component` (ou seja, `Control` e `Presenter`) reagem tentando chamar o método `handle{signal}` com os parâmetros apropriados. - -Em outras palavras: pega-se a definição da função `handle{signal}` e todos os parâmetros que vieram com a requisição, e os parâmetros da URL são atribuídos aos argumentos pelo nome e tenta-se chamar o método dado. Por exemplo, o valor do parâmetro `id` na URL é passado como parâmetro `$id`, `something` da URL é passado como `$something`, etc. E se o método não existir, o método `signalReceived` lança uma [exceção |api:Nette\Application\UI\BadSignalException]. - -O sinal pode ser recebido por qualquer componente, presenter ou objeto que implemente a interface `SignalReceiver` e esteja conectado à árvore de componentes. - -Os principais receptores de sinais serão `Presenters` e componentes visuais que herdam de `Control`. O sinal deve servir como um sinal para o objeto de que ele deve fazer algo - a enquete deve contar o voto do usuário, o bloco de notícias deve se expandir e exibir o dobro de notícias, o formulário foi enviado e deve processar os dados, e assim por diante. - -A URL para o sinal é criada usando o método [Component::link() |api:Nette\Application\UI\Component::link()]. Como parâmetro `$destination`, passamos a string `{signal}!` e como `$args`, um array de argumentos que queremos passar para o sinal. O sinal é sempre chamado no presenter e action atuais com os parâmetros atuais, os parâmetros do sinal são apenas adicionados. Além disso, o **parâmetro `?do`, que especifica o sinal**, é adicionado logo no início. - -Seu formato é `{signal}` ou `{signalReceiver}-{signal}`. `{signalReceiver}` é o nome do componente no presenter. É por isso que um hífen não pode estar no nome do componente - ele é usado para separar o nome do componente e o sinal, mas é possível aninhar vários componentes dessa maneira. - -O método [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] verifica se o componente (primeiro argumento) é o receptor do sinal (segundo argumento). Podemos omitir o segundo argumento - então ele verifica se o componente é o receptor de qualquer sinal. `true` pode ser passado como segundo parâmetro para verificar se não apenas o componente especificado, mas também qualquer um de seus descendentes é o receptor. - -Em qualquer fase anterior a `handle{signal}`, podemos executar o sinal manualmente chamando o método [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], que se encarrega de tratar o sinal - pega o componente que foi determinado como o receptor do sinal (se nenhum receptor de sinal for especificado, é o próprio presenter) e envia o sinal para ele. - -Exemplo: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Assim, o sinal é executado prematuramente e não será chamado novamente. diff --git a/application/pt/configuration.texy b/application/pt/configuration.texy deleted file mode 100644 index e4a50ee6a1..0000000000 --- a/application/pt/configuration.texy +++ /dev/null @@ -1,191 +0,0 @@ -Configuração de aplicações -************************** - -.[perex] -Visão geral das opções de configuração para Aplicações Nette. - - -Application -=========== - -```neon -application: - # exibir o painel "Nette Application" no Tracy BlueScreen? - debugger: ... # (bool) padrão é true - - # o error-presenter será chamado em caso de erro? - # tem efeito apenas no modo de desenvolvimento - catchExceptions: ... # (bool) padrão é true - - # nome do error-presenter - errorPresenter: Error # (string|array) padrão é 'Nette:Error' - - # define aliases para presenters e ações - aliases: ... - - # define regras para traduzir o nome do presenter para a classe - mapping: ... - - # links inválidos não geram avisos? - # tem efeito apenas no modo de desenvolvimento - silentLinks: ... # (bool) padrão é false -``` - -A partir da versão `nette/application` 3.2, é possível definir um par de error-presenters: - -```neon -application: - errorPresenter: - 4xx: Error4xx # para a exceção Nette\Application\BadRequestException - 5xx: Error5xx # para outras exceções -``` - -A opção `silentLinks` determina como o Nette se comporta no modo de desenvolvimento quando a geração de um link falha (por exemplo, porque o presenter não existe, etc.). O valor padrão `false` significa que o Nette lançará um erro `E_USER_WARNING`. Definir como `true` suprimirá esta mensagem de erro. No ambiente de produção, `E_USER_WARNING` é sempre lançado. Este comportamento também pode ser influenciado definindo a variável do presenter [$invalidLinkMode |creating-links#Links inválidos]. - -[Aliases simplificam a vinculação |creating-links#Aliases] a presenters frequentemente usados. - -[Mapeamento define regras |directory-structure#Mapeamento de presenters], segundo as quais o nome da classe é derivado do nome do presenter. - - -Registro automático de presenters ---------------------------------- - -O Nette adiciona automaticamente presenters como serviços ao contêiner de DI, o que acelera significativamente sua criação. Como o Nette localiza os presenters pode ser configurado: - -```neon -application: - # procurar presenters no mapa de classes do Composer? - scanComposer: ... # (bool) padrão é true - - # máscara que o nome da classe e do arquivo deve corresponder - scanFilter: ... # (string) padrão é '*Presenter' - - # em quais diretórios procurar presenters? - scanDirs: # (string[]|false) padrão é '%appDir%' - - %vendorDir%/mymodule -``` - -Os diretórios listados em `scanDirs` não sobrescrevem o valor padrão `%appDir%`, mas o complementam, então `scanDirs` conterá ambos os caminhos `%appDir%` e `%vendorDir%/mymodule`. Se quisermos omitir o diretório padrão, usamos [um ponto de exclamação |dependency-injection:configuration#Mesclagem], que sobrescreve o valor: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -A varredura de diretórios pode ser desativada especificando o valor false. Não recomendamos suprimir completamente a adição automática de presenters, pois isso resultará em uma redução no desempenho da aplicação. - - -Templates Latte -=============== - -Com esta configuração, o comportamento do Latte em componentes e presenters pode ser influenciado globalmente. - -```neon -latte: - # exibir o painel Latte na Barra Tracy para o template principal (true) ou todos os componentes (all)? - debugger: ... # (true|false|'all') padrão é true - - # gera templates com o cabeçalho declare(strict_types=1) - strictTypes: ... # (bool) padrão é false - - # ativa o modo de [parser estrito |latte:develop#striktní režim] - strictParsing: ... # (bool) padrão é false - - # ativa a [verificação do código gerado |latte:develop#Kontrola vygenerovaného kódu] - phpLinter: ... # (string) padrão é null - - # define a localidade - locale: pt_BR # (string) padrão é null - - # classe do objeto $this->template - templateClass: App\MyTemplateClass # padrão é Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Se você estiver usando Latte versão 3, pode adicionar novas [extensões |latte:extending-latte#Latte Extension] usando: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Se você estiver usando Latte versão 2, pode registrar novas tags especificando o nome da classe ou uma referência a um serviço. Por padrão, o método `install()` é chamado, mas isso pode ser alterado especificando o nome de outro método: - -```neon -latte: - # registro de tags Latte personalizadas - macros: - - App\MyLatteMacros::register # método estático, nome da classe ou callable - - @App\MyLatteMacrosFactory # serviço com método install() - - @App\MyLatteMacrosFactory::register # serviço com método register() - -services: - - App\MyLatteMacrosFactory -``` - - -Roteamento -========== - -Configurações básicas: - -```neon -routing: - # exibir o painel de roteamento na Barra Tracy? - debugger: ... # (bool) padrão é true - - # serializa o roteador no contêiner DI - cache: ... # (bool) padrão é false -``` - -O roteamento geralmente é definido na classe [RouterFactory |routing#Coleção de rotas]. Alternativamente, as rotas também podem ser definidas na configuração usando pares `máscara: ação`, mas este método não oferece tanta variabilidade nas configurações: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Constantes -========== - -Criação de constantes PHP. - -```neon -constants: - Foobar: 'baz' -``` - -Após iniciar a aplicação, a constante `Foobar` será criada. - -.[note] -Constantes não devem servir como variáveis globalmente disponíveis. Para passar valores para objetos, use [injeção de dependência |dependency-injection:passing-dependencies]. - - -PHP -=== - -Configuração de diretivas PHP. Uma visão geral de todas as diretivas pode ser encontrada em [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Lisbon -``` - - -Serviços DI -=========== - -Estes serviços são adicionados ao contêiner de DI: - -| Nome | Tipo | Descrição -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [iniciador de toda a aplicação |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | fábrica de presenters -| `application.###` | [api:Nette\Application\UI\Presenter] | presenters individuais -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | fábrica do objeto `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | fábrica para [`$this->template` |templates] diff --git a/application/pt/creating-links.texy b/application/pt/creating-links.texy deleted file mode 100644 index 67af4a919b..0000000000 --- a/application/pt/creating-links.texy +++ /dev/null @@ -1,286 +0,0 @@ -Criando Links URL -***************** - -
    - -Criar links no Nette é tão simples quanto apontar o dedo. Basta apontar e o framework faz todo o trabalho por você. Vamos mostrar: - -- como criar links em templates e em outros lugares -- como distinguir um link para a página atual -- o que fazer com links inválidos - -
    - - -Graças ao [roteamento bidirecional |routing], você nunca precisará escrever URLs fixas da sua aplicação em templates ou código, que podem mudar posteriormente, ou montá-las de forma complicada. No link, basta indicar o presenter e a ação, passar quaisquer parâmetros e o framework gerará a URL por si só. Na verdade, é muito semelhante a chamar uma função. Você vai gostar disso. - - -No template do presenter -======================== - -Mais frequentemente, criamos links em templates e um ótimo auxiliar é o atributo `n:href`: - -```latte -detalhe -``` - -Observe que, em vez do atributo HTML `href`, usamos o [n:atributo |latte:syntax#n:atributos] `n:href`. Seu valor não é uma URL, como seria no caso do atributo `href`, but o nome do presenter e da ação. - -Clicar no link é, simplificadamente, algo como chamar o método `ProductPresenter::renderShow()`. E se ele tiver parâmetros em sua assinatura, podemos chamá-lo com argumentos: - -```latte -detalhe do produto -``` - -Também é possível passar parâmetros nomeados. O link a seguir passa o parâmetro `lang` com o valor `pt`: - -```latte -detalhe do produto -``` - -Se o método `ProductPresenter::renderShow()` não tiver `$lang` em sua assinatura, ele pode obter o valor do parâmetro usando `$lang = $this->getParameter('lang')` ou da [propriedade |presenters#Parâmetros da requisição]. - -Se os parâmetros estiverem armazenados em um array, eles podem ser expandidos com o operador `...` (no Latte 2.x, com o operador `(expand)`): - -```latte -{var $args = [$product->id, lang => pt]} -detalhe do produto -``` - -Nos links, os chamados [parâmetros persistentes |presenters#Parâmetros persistentes] também são transmitidos automaticamente. - -O atributo `n:href` é muito útil para tags HTML ``. Se quisermos exibir o link em outro lugar, por exemplo, no texto, usamos `{link}`: - -```latte -O endereço é: {link Home:default} -``` - - -No código -========= - -Para criar um link no presenter, usa-se o método `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Os parâmetros também podem ser passados através de um array, onde também podem ser especificados parâmetros nomeados: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'pt']); -``` - -Links também podem ser criados sem um presenter, para isso existe o [#LinkGenerator] e seu método `link()`. - - -Links para o presenter -====================== - -Se o destino do link for um presenter e uma ação, ele tem esta sintaxe: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -O formato é suportado por todas as tags Latte e todos os métodos do presenter que trabalham com links, ou seja, `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` e também [#LinkGenerator]. Portanto, mesmo que `n:href` seja usado nos exemplos, qualquer uma das funções poderia estar lá. - -A forma básica é, portanto, `Presenter:action`: - -```latte -página inicial -``` - -Se estivermos vinculando a uma ação do presenter atual, podemos omitir seu nome: - -```latte -página inicial -``` - -Se o destino for a ação `default`, podemos omiti-la, mas os dois pontos devem permanecer: - -```latte -página inicial -``` - -Links também podem apontar para outros [módulos |directory-structure#Presenters e templates]. Aqui, os links são distinguidos entre relativos para um submódulo aninhado ou absolutos. O princípio é análogo aos caminhos no disco, apenas em vez de barras, são dois pontos. Suponha que o presenter atual faça parte do módulo `Front`, então escrevemos: - -```latte -link para Front:Shop:Product:show -link para Admin:Product:show -``` - -Um caso especial é um link [para si mesmo |#Link para a página atual], onde especificamos `this` como destino. - -```latte -atualizar -``` - -Podemos vincular a uma parte específica da página através do chamado fragmento após o caractere de cerquilha `#`: - -```latte -link para Home:default e fragmento #main -``` - - -Caminhos absolutos -================== - -Links gerados usando `link()` ou `n:href` são sempre caminhos absolutos (ou seja, começam com o caractere `/`), mas não URLs absolutas com protocolo e domínio como `https://domain`. - -Para gerar uma URL absoluta, adicione duas barras no início (por exemplo, `n:href="//Home:"`). Ou você pode configurar o presenter para gerar apenas links absolutos definindo `$this->absoluteUrls = true`. - - -Link para a página atual -======================== - -O destino `this` cria um link para a página atual: - -```latte -atualizar -``` - -Ao mesmo tempo, todos os parâmetros especificados na assinatura do método `action()` ou `render()` são transmitidos, se `action()` não estiver definida. Portanto, se estivermos na página `Product:show` e `id: 123`, o link para `this` também passará este parâmetro. - -Claro, é possível especificar os parâmetros diretamente: - -```latte -atualizar -``` - -A função `isLinkCurrent()` verifica se o destino do link é idêntico à página atual. Isso pode ser usado, por exemplo, em um template para diferenciar links, etc. - -Os parâmetros são os mesmos do método `link()`, mas adicionalmente é possível usar o caractere curinga `*` em vez de uma ação específica, o que significa qualquer ação do presenter dado. - -```latte -{if !isLinkCurrent('Admin:login')} - Faça login -{/if} - -
  • - ... -
  • -``` - -Em combinação com `n:href` em um único elemento, uma forma abreviada pode ser usada: - -```latte -... -``` - -O caractere curinga `*` só pode ser usado no lugar da ação, não do presenter. - -Para verificar se estamos em um determinado módulo ou seu submódulo, usamos o método `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Links para sinal -================ - -O destino de um link não precisa ser apenas um presenter e uma ação, mas também um [sinal |components#Sinal] (eles chamam o método `handle()`). Então a sintaxe é a seguinte: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -O sinal é, portanto, distinguido por um ponto de exclamação: - -```latte -sinal -``` - -Também é possível criar um link para o sinal de um subcomponente (ou sub-subcomponente): - -```latte -sinal -``` - - -Links no componente -=================== - -Como os [componentes|components] são unidades reutilizáveis separadas que não devem ter vínculos com os presenters circundantes, os links funcionam um pouco diferente aqui. O atributo Latte `n:href` e a tag `{link}`, bem como os métodos do componente como `link()` e outros, consideram o destino do link **sempre como o nome de um sinal**. Portanto, nem mesmo é necessário incluir o ponto de exclamação: - -```latte -sinal, não ação -``` - -Se quiséssemos vincular a presenters no template do componente, usaríamos a tag `{plink}`: - -```latte -início -``` - -ou no código - -```php -$this->getPresenter()->link('Home:default') -``` - - -Aliases .{data-version:v3.2.2} -============================== - -Às vezes, pode ser útil atribuir um alias fácil de lembrar a um par Presenter:action. Por exemplo, nomear a página inicial `Front:Home:default` simplesmente como `home` ou `Admin:Dashboard:default` como `admin`. - -Aliases são definidos na [configuração|configuration] sob a chave `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -Nos links, eles são então escritos usando um arroba, por exemplo: - -```latte -administração -``` - -Eles também são suportados em todos os métodos que trabalham com links, como `redirect()` e similares. - - -Links inválidos -=============== - -Pode acontecer que criemos um link inválido - seja porque ele leva a um presenter inexistente, ou porque passa mais parâmetros do que o método de destino aceita em sua assinatura, ou quando uma URL não pode ser gerada para a ação de destino. Como lidar com links inválidos é determinado pela variável estática `Presenter::$invalidLinkMode`. Ela pode assumir uma combinação destes valores (constantes): - -- `Presenter::InvalidLinkSilent` - modo silencioso, o caractere # é retornado como URL -- `Presenter::InvalidLinkWarning` - um aviso E_USER_WARNING é lançado, que será registrado no modo de produção, mas não causará a interrupção da execução do script -- `Presenter::InvalidLinkTextual` - aviso visual, exibe o erro diretamente no link -- `Presenter::InvalidLinkException` - a exceção InvalidLinkException é lançada - -A configuração padrão é `InvalidLinkWarning` no modo de produção e `InvalidLinkWarning | InvalidLinkTextual` no modo de desenvolvimento. `InvalidLinkWarning` no ambiente de produção não causa a interrupção do script, mas o aviso será registrado. No ambiente de desenvolvimento, ele é capturado pelo [Tracy |tracy:] e exibe uma bluescreen. `InvalidLinkTextual` funciona retornando uma mensagem de erro como URL, que começa com os caracteres `#error:`. Para tornar esses links visíveis à primeira vista, adicionamos ao CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Se não quisermos que avisos sejam produzidos no ambiente de desenvolvimento, podemos definir o modo silencioso diretamente na [configuração|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Como criar links com conforto semelhante ao método `link()`, mas sem a presença de um presenter? Para isso existe a [api:Nette\Application\LinkGenerator]. - -LinkGenerator é um serviço que você pode solicitar via construtor e, em seguida, criar links usando seu método `link()`. - -Há uma diferença em relação aos presenters. O LinkGenerator cria todos os links diretamente como URLs absolutas. Além disso, não existe um "presenter atual", então não é possível especificar apenas o nome da ação como destino `link('default')` ou usar caminhos relativos para módulos. - -Links inválidos sempre lançam `Nette\Application\UI\InvalidLinkException`. diff --git a/application/pt/directory-structure.texy b/application/pt/directory-structure.texy deleted file mode 100644 index c7f269dfff..0000000000 --- a/application/pt/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Estrutura de diretórios da aplicação -************************************ - -
    - -Como projetar uma estrutura de diretórios clara e escalável para projetos no Nette Framework? Mostraremos as melhores práticas que o ajudarão a organizar seu código. Você aprenderá: - -- como **dividir logicamente** a aplicação em diretórios -- como projetar a estrutura para que ela **escale bem** com o crescimento do projeto -- quais são as **alternativas possíveis** e suas vantagens ou desvantagens - -
    - - -É importante mencionar que o próprio Nette Framework não impõe nenhuma estrutura específica. Ele é projetado para ser facilmente adaptável a quaisquer necessidades e preferências. - - -Estrutura básica do projeto -=========================== - -Embora o Nette Framework não dite nenhuma estrutura de diretórios fixa, existe uma organização padrão comprovada na forma do [Web Project|https://github.com/nette/web-project]: - -/--pre -web-project/ -├── app/ ← diretório com a aplicação -├── assets/ ← arquivos SCSS, JS, imagens..., alternativamente resources/ -├── bin/ ← scripts para a linha de comando -├── config/ ← configuração -├── log/ ← erros registrados -├── temp/ ← arquivos temporários, cache -├── tests/ ← testes -├── vendor/ ← bibliotecas instaladas pelo Composer -└── www/ ← diretório público (document-root) -\-- - -Você pode modificar esta estrutura livremente de acordo com suas necessidades - renomear ou mover pastas. Depois, basta apenas ajustar os caminhos relativos aos diretórios no arquivo `Bootstrap.php` e, opcionalmente, `composer.json`. Nada mais é necessário, nenhuma reconfiguração complicada, nenhuma alteração de constantes. O Nette possui uma autodeteção inteligente e reconhece automaticamente a localização da aplicação, incluindo sua base de URL. - - -Princípios de organização do código -=================================== - -Quando você explora um novo projeto pela primeira vez, deve conseguir se orientar rapidamente nele. Imagine que você abre o diretório `app/Model/` e vê esta estrutura: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -A partir dela, você só pode deduzir que o projeto usa alguns serviços, repositórios e entidades. Você não aprenderá nada sobre o propósito real da aplicação. - -Vejamos outra abordagem - **organização por domínios**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Aqui é diferente - à primeira vista, fica claro que se trata de uma loja virtual. Os próprios nomes dos diretórios revelam o que a aplicação faz - trabalha com pagamentos, pedidos e produtos. - -A primeira abordagem (organização por tipo de classe) traz na prática uma série de problemas: o código que está logicamente relacionado é fragmentado em diferentes pastas e você precisa pular entre elas. Portanto, organizaremos por domínios. - - -Namespaces ----------- - -É costume que a estrutura de diretórios corresponda aos namespaces na aplicação. Isso significa que a localização física dos arquivos corresponde ao seu namespace. Por exemplo, uma classe localizada em `app/Model/Product/ProductRepository.php` deve ter o namespace `App\Model\Product`. Este princípio ajuda na orientação no código e simplifica o autoloading. - - -Singular vs. plural nos nomes ------------------------------ - -Observe que para os diretórios principais da aplicação usamos o singular: `app`, `config`, `log`, `temp`, `www`. O mesmo vale para o interior da aplicação: `Model`, `Core`, `Presentation`. Isso ocorre porque cada um deles representa um conceito coeso. - -Da mesma forma, por exemplo, `app/Model/Product` representa tudo relacionado a produtos. Não o chamaremos de `Products`, porque não é uma pasta cheia de produtos (isso significaria que haveria arquivos `nokia.php`, `samsung.php`). É um namespace contendo classes para trabalhar com produtos - `ProductRepository.php`, `ProductService.php`. - -A pasta `app/Tasks` está no plural porque contém um conjunto de scripts executáveis independentes - `CleanupTask.php`, `ImportTask.php`. Cada um deles é uma unidade separada. - -Para consistência, recomendamos usar: -- Singular para namespaces que representam uma unidade funcional (mesmo que trabalhe com múltiplas entidades) -- Plural para coleções de unidades independentes -- Em caso de incerteza ou se você não quiser pensar sobre isso, escolha o singular - - -Diretório público `www/` -======================== - -Este diretório é o único acessível pela web (o chamado document-root). Frequentemente, você pode encontrar o nome `public/` em vez de `www/` - é apenas uma questão de convenção e não afeta a funcionalidade do Nette. O diretório contém: -- [Ponto de entrada |bootstrapping#index.php] da aplicação `index.php` -- Arquivo `.htaccess` com regras para mod_rewrite (no Apache) -- Arquivos estáticos (CSS, JavaScript, imagens) -- Arquivos carregados (uploads) - -Para a segurança adequada da aplicação, é crucial ter o [document-root configurado corretamente |nette:troubleshooting#Como alterar ou remover o diretório www da URL]. - -.[note] -Nunca coloque a pasta `node_modules/` neste diretório - ela contém milhares de arquivos que podem ser executáveis e não devem estar publicamente acessíveis. - - -Diretório da aplicação `app/` -============================= - -Este é o diretório principal com o código da aplicação. Estrutura básica: - -/--pre -app/ -├── Core/ ← questões de infraestrutura -├── Model/ ← lógica de negócios -├── Presentation/ ← presenters e templates -├── Tasks/ ← scripts de comando -└── Bootstrap.php ← classe de inicialização da aplicação -\-- - -`Bootstrap.php` é a [classe de inicialização da aplicação|bootstrapping], que inicializa o ambiente, carrega a configuração e cria o contêiner de DI. - -Vamos agora examinar os subdiretórios individuais com mais detalhes. - - -Presenters e templates -====================== - -A parte de apresentação da aplicação está no diretório `app/Presentation`. Uma alternativa é o curto `app/UI`. É o local para todos os presenters, seus templates e quaisquer classes auxiliares. - -Organizamos esta camada por domínios. Em um projeto complexo que combina uma loja virtual, um blog e uma API, a estrutura seria assim: - -/--pre -app/Presentation/ -├── Shop/ ← frontend da loja virtual -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administração -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← endpoints da API - └── V1/ -\-- - -Por outro lado, para um blog simples, usaríamos a seguinte divisão: - -/--pre -app/Presentation/ -├── Front/ ← frontend do site -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administração -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, sitemaps, etc. -\-- - -Pastas como `Home/` ou `Dashboard/` contêm presenters e templates. Pastas como `Front/`, `Admin/` ou `Api/` são chamadas de **módulos**. Tecnicamente, são diretórios comuns que servem para a divisão lógica da aplicação. - -Cada pasta com um presenter contém um presenter de mesmo nome e seus templates. Por exemplo, a pasta `Dashboard/` contém: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← template -\-- - -Esta estrutura de diretórios se reflete nos namespaces das classes. Por exemplo, `DashboardPresenter` está localizado no namespace `App\Presentation\Admin\Dashboard` (veja [#Mapeamento de presenters]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Referimo-nos ao presenter `Dashboard` dentro do módulo `Admin` na aplicação usando a notação de dois pontos como `Admin:Dashboard`. À sua ação `default`, então, como `Admin:Dashboard:default`. No caso de módulos aninhados, usamos mais dois pontos, por exemplo, `Shop:Order:Detail:default`. - - -Desenvolvimento flexível da estrutura -------------------------------------- - -Uma das grandes vantagens desta estrutura é como ela se adapta elegantemente às necessidades crescentes do projeto. Como exemplo, vejamos a parte que gera feeds XML. No início, temos uma forma simples: - -/--pre -Export/ -├── ExportPresenter.php ← um presenter para todas as exportações -├── sitemap.latte ← template para o sitemap -└── feed.latte ← template para o feed RSS -\-- - -Com o tempo, mais tipos de feeds são adicionados e precisamos de mais lógica para eles... Sem problemas! A pasta `Export/` simplesmente se torna um módulo: - -/--pre -Export/ -├── Sitemap/ -│ ├── SitemapPresenter.php -│ └── sitemap.latte -└── Feed/ - ├── FeedPresenter.php - ├── zbozi.latte ← feed para Zboží.cz - └── heureka.latte ← feed para Heureka.cz -\-- - -Esta transformação é completamente fluida - basta criar novas subpastas, dividir o código nelas e atualizar os links (por exemplo, de `Export:feed` para `Export:Feed:zbozi`). Graças a isso, podemos expandir gradualmente a estrutura conforme necessário, o nível de aninhamento não é limitado de forma alguma. - -Se, por exemplo, na administração você tiver muitos presenters relacionados ao gerenciamento de pedidos, como `OrderDetail`, `OrderEdit`, `OrderDispatch`, etc., você pode criar um módulo (pasta) `Order` neste local para melhor organização, que conterá (pastas para) os presenters `Detail`, `Edit`, `Dispatch` e outros. - - -Localização dos templates -------------------------- - -Nos exemplos anteriores, vimos que os templates estão localizados diretamente na pasta com o presenter: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← classe opcional para o template -└── default.latte ← template -\-- - -Esta localização se mostra na prática a mais conveniente - todos os arquivos relacionados estão à mão. - -Alternativamente, você pode colocar os templates em uma subpasta `templates/`. O Nette suporta ambas as variantes. Você pode até colocar os templates completamente fora da pasta `Presentation/`. Tudo sobre as opções de localização de templates pode ser encontrado no capítulo [Procurando templates |templates#Procurando templates]. - - -Classes auxiliares e componentes --------------------------------- - -Frequentemente, presenters e templates são acompanhados por outros arquivos auxiliares. Nós os colocamos logicamente de acordo com seu escopo: - -1. **Diretamente com o presenter** no caso de componentes específicos para esse presenter: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← componente para listagem de produtos -└── FilterForm.php ← formulário para filtragem -\-- - -2. **Para o módulo** - recomendamos usar a pasta `Accessory`, que é colocada de forma clara no início do alfabeto: - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← componentes para o frontend -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **Para toda a aplicação** - em `Presentation/Accessory/`: -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Ou você pode colocar classes auxiliares como `LatteExtension.php` ou `TemplateFilters.php` na pasta de infraestrutura `app/Core/Latte/`. E componentes em `app/Components`. A escolha depende dos costumes da equipe. - - -Model - o coração da aplicação -============================== - -O Model contém toda a lógica de negócios da aplicação. Para sua organização, a regra se aplica novamente - estruturamos por domínios: - -/--pre -app/Model/ -├── Payment/ ← tudo sobre pagamentos -│ ├── PaymentFacade.php ← ponto de entrada principal -│ ├── PaymentRepository.php -│ ├── Payment.php ← entidade -├── Order/ ← tudo sobre pedidos -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← tudo sobre envio -\-- - -No model, você normalmente encontrará estes tipos de classes: - -**Facades**: representam o ponto de entrada principal para um domínio específico na aplicação. Atuam como um orquestrador que coordena a cooperação entre diferentes serviços para implementar casos de uso completos (como "criar pedido" ou "processar pagamento"). Sob sua camada de orquestração, a facade esconde os detalhes de implementação do resto da aplicação, fornecendo assim uma interface limpa para trabalhar com o domínio dado. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // validação - // criação do pedido - // envio de e-mail - // registro nas estatísticas - } -} -``` - -**Serviços**: focam em uma operação de negócios específica dentro do domínio. Ao contrário da facade, que orquestra casos de uso inteiros, um serviço implementa lógica de negócios específica (como cálculos de preços ou processamento de pagamentos). Os serviços são tipicamente sem estado e podem ser usados por facades como blocos de construção para operações mais complexas, ou diretamente por outras partes da aplicação para tarefas mais simples. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // cálculo do preço - } -} -``` - -**Repositórios**: garantem toda a comunicação com o armazenamento de dados, tipicamente um banco de dados. Sua tarefa é carregar e salvar entidades e implementar métodos para sua busca. O repositório isola o resto da aplicação dos detalhes de implementação do banco de dados e fornece uma interface orientada a objetos para trabalhar com dados. - -```php -class OrderRepository -{ - public function find(int $id): ?Order - { - } - - public function findByCustomer(int $customerId): array - { - } -} -``` - -**Entidades**: objetos que representam os principais conceitos de negócios na aplicação, que têm sua identidade e mudam ao longo do tempo. Tipicamente, são classes mapeadas para tabelas de banco de dados usando ORM (como Nette Database Explorer ou Doctrine). As entidades podem conter regras de negócios relacionadas aos seus dados e lógica de validação. - -```php -// Entidade mapeada para a tabela de banco de dados 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 objects**: objetos imutáveis que representam valores sem identidade própria - por exemplo, um valor monetário ou um endereço de e-mail. Duas instâncias de um value object com os mesmos valores são consideradas idênticas. - - -Código de infraestrutura -======================== - -A pasta `Core/` (ou também `Infrastructure/`) é o lar da base técnica da aplicação. O código de infraestrutura normalmente inclui: - -/--pre -app/Core/ -├── Router/ ← roteamento e gerenciamento de URL -│ └── RouterFactory.php -├── Security/ ← autenticação e autorização -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← logging e monitoramento -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← camada de cache -│ └── FullPageCache.php -└── Integration/ ← integração com serviços ext. - ├── Slack/ - └── Stripe/ -\-- - -Para projetos menores, uma estrutura plana é obviamente suficiente: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -É o código que: - -- Lida com a infraestrutura técnica (roteamento, logging, cache) -- Integra serviços externos (Sentry, Elasticsearch, Redis) -- Fornece serviços básicos para toda a aplicação (e-mail, banco de dados) -- É geralmente independente do domínio específico - cache ou logger funciona da mesma forma para uma loja virtual ou blog. - -Está em dúvida se uma determinada classe pertence aqui ou ao model? A diferença crucial é que o código em `Core/`: - -- Não sabe nada sobre o domínio (produtos, pedidos, artigos) -- Geralmente pode ser transferido para outro projeto -- Lida com "como funciona" (como enviar um e-mail), não "o que faz" (qual e-mail enviar) - -Exemplo para melhor compreensão: - -- `App\Core\MailerFactory` - cria instâncias da classe para envio de e-mails, lida com configurações SMTP -- `App\Model\OrderMailer` - usa `MailerFactory` para enviar e-mails sobre pedidos, conhece seus templates e sabe quando devem ser enviados - - -Scripts de comando -================== - -Aplicações frequentemente precisam executar atividades fora das requisições HTTP normais - seja processamento de dados em segundo plano, manutenção ou tarefas periódicas. Scripts simples no diretório `bin/` são usados para execução, enquanto a lógica de implementação é colocada em `app/Tasks/` (ou `app/Commands/`). - -Exemplo: - -/--pre -app/Tasks/ -├── Maintenance/ ← scripts de manutenção -│ ├── CleanupCommand.php ← exclusão de dados antigos -│ └── DbOptimizeCommand.php ← otimização do banco de dados -├── Integration/ ← integração com sistemas externos -│ ├── ImportProducts.php ← importação do sistema do fornecedor -│ └── SyncOrders.php ← sincronização de pedidos -└── Scheduled/ ← tarefas agendadas - ├── NewsletterCommand.php ← envio de newsletters - └── ReminderCommand.php ← notificações para clientes -\-- - -O que pertence ao model e o que pertence aos scripts de comando? Por exemplo, a lógica para enviar um único e-mail faz parte do model, o envio em massa de milhares de e-mails já pertence a `Tasks/`. - -As tarefas são geralmente [executadas a partir da linha de comando |https://blog.nette.org/en/cli-scripts-in-nette-application] ou via cron. Elas também podem ser executadas via requisição HTTP, mas é necessário pensar na segurança. O presenter que inicia a tarefa precisa ser protegido, por exemplo, apenas para usuários logados ou com um token forte e acesso de endereços IP permitidos. Para tarefas longas, é necessário aumentar o limite de tempo do script e usar `session_write_close()` para não bloquear a sessão. - - -Outros diretórios possíveis -=========================== - -Além dos diretórios básicos mencionados, você pode adicionar outras pastas especializadas de acordo com as necessidades do projeto. Vejamos as mais comuns e seus usos: - -/--pre -app/ -├── Api/ ← lógica para API independente da camada de apresentação -├── Database/ ← scripts de migração e seeders para dados de teste -├── Components/ ← componentes visuais compartilhados em toda a aplicação -├── Event/ ← útil se você usa arquitetura orientada a eventos -├── Mail/ ← templates de e-mail e lógica relacionada -└── Utils/ ← classes auxiliares -\-- - -Para componentes visuais compartilhados usados em presenters em toda a aplicação, a pasta `app/Components` ou `app/Controls` pode ser usada: - -/--pre -app/Components/ -├── Form/ ← componentes de formulário compartilhados -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← componentes para listagens de dados -│ └── DataGrid.php -└── Navigation/ ← elementos de navegação - ├── Breadcrumbs.php - └── Menu.php -\-- - -Aqui pertencem componentes que têm lógica mais complexa. Se você deseja compartilhar componentes entre vários projetos, é aconselhável extraí-los para um pacote composer separado. - -No diretório `app/Mail`, você pode colocar o gerenciamento da comunicação por e-mail: - -/--pre -app/Mail/ -├── templates/ ← templates de e-mail -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Mapeamento de presenters -======================== - -O mapeamento define regras para derivar o nome da classe a partir do nome do presenter. Especificamo-las na [configuração|configuration] sob a chave `application › mapping`. - -Nesta página, mostramos que colocamos os presenters na pasta `app/Presentation` (ou `app/UI`). Precisamos informar esta convenção ao Nette no arquivo de configuração. Basta uma linha: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Como funciona o mapeamento? Para melhor compreensão, imaginemos primeiro uma aplicação sem módulos. Queremos que as classes dos presenters caiam no namespace `App\Presentation`, para que o presenter `Home` seja mapeado para a classe `App\Presentation\HomePresenter`. O que conseguimos com esta configuração: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -O mapeamento funciona de forma que o nome do presenter `Home` substitui o asterisco na máscara `App\Presentation\*Presenter`, resultando no nome final da classe `App\Presentation\HomePresenter`. Simples! - -Mas, como você pode ver nos exemplos neste e em outros capítulos, colocamos as classes dos presenters em subdiretórios homônimos, por exemplo, o presenter `Home` é mapeado para a classe `App\Presentation\Home\HomePresenter`. Conseguimos isso duplicando os dois pontos (requer Nette Application 3.2): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Agora vamos mapear presenters para módulos. Para cada módulo, podemos definir um mapeamento específico: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -De acordo com esta configuração, o presenter `Front:Home` é mapeado para a classe `App\Presentation\Front\Home\HomePresenter`, enquanto o presenter `Api:OAuth` para a classe `App\Api\OAuthPresenter`. - -Como os módulos `Front` e `Admin` têm um método de mapeamento semelhante e provavelmente haverá mais módulos assim, é possível criar uma regra geral que os substitua. Um novo asterisco para o módulo é adicionado à máscara da classe: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Isso também funciona para estruturas de diretórios mais profundamente aninhadas, como, por exemplo, o presenter `Admin:User:Edit`, o segmento com asterisco se repete para cada nível e o resultado é a classe `App\Presentation\Admin\User\Edit\EditPresenter`. - -Uma notação alternativa é usar um array composto por três segmentos em vez de uma string. Esta notação é equivalente à anterior: - -```neon -application: - mapping: - *: [App\Presentation, *, **Presenter] - Api: [App\Api, '', *Presenter] -``` diff --git a/application/pt/how-it-works.texy b/application/pt/how-it-works.texy deleted file mode 100644 index 3e6fd2db4b..0000000000 --- a/application/pt/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Como funcionam as aplicações? -***************************** - -
    - -Você está lendo o documento fundamental da documentação do Nette. Aprenderá como as aplicações web funcionam. Do início ao fim, desde o momento do nascimento até o último suspiro do script PHP. Após a leitura, você saberá: - -- como tudo funciona -- o que é Bootstrap, Presenter e Contêiner de DI -- como é a estrutura de diretórios - -
    - - -Estrutura de diretórios -======================= - -Abra o exemplo do esqueleto da aplicação web chamado [WebProject|https://github.com/nette/web-project] e, enquanto lê, pode consultar os arquivos sobre os quais estamos falando. - -A estrutura de diretórios se parece com algo assim: - -/--pre -web-project/ -├── app/ ← diretório da aplicação -│ ├── Core/ ← classes base necessárias para a execução -│ │ └── RouterFactory.php ← configuração de endereços URL -│ ├── Presentation/ ← presenters, templates & cia. -│ │ ├── @layout.latte ← template de layout -│ │ └── Home/ ← diretório do presenter Home -│ │ ├── HomePresenter.php ← classe do presenter Home -│ │ └── default.latte ← template da ação default -│ └── Bootstrap.php ← classe de inicialização Bootstrap -├── assets/ ← recursos (SCSS, TypeScript, imagens de origem) -├── bin/ ← scripts executados a partir da linha de comando -├── config/ ← arquivos de configuração -│ ├── common.neon -│ └── services.neon -├── log/ ← erros registrados -├── temp/ ← arquivos temporários, cache, … -├── vendor/ ← bibliotecas instaladas pelo Composer -│ ├── ... -│ └── autoload.php ← autoloading de todos os pacotes instalados -├── www/ ← diretório público ou document-root do projeto -│ ├── assets/ ← arquivos estáticos compilados (CSS, JS, imagens, ...) -│ ├── .htaccess ← regras mod_rewrite -│ └── index.php ← arquivo inicial pelo qual a aplicação é iniciada -└── .htaccess ← proíbe o acesso a todos os diretórios exceto www -\-- - -Você pode alterar a estrutura de diretórios de qualquer forma, renomear ou mover pastas, é totalmente flexível. Além disso, o Nette possui uma autodeteção inteligente e reconhece automaticamente a localização da aplicação, incluindo sua base de URL. - -Para aplicações um pouco maiores, podemos [dividir as pastas com presenters e templates em subdiretórios |directory-structure#Presenters e templates] e as classes em namespaces, que chamamos de módulos. - -O diretório `www/` representa o chamado diretório público ou document-root do projeto. Você pode renomeá-lo sem a necessidade de configurar mais nada no lado da aplicação. Apenas é necessário [configurar a hospedagem |nette:troubleshooting#Como alterar ou remover o diretório www da URL] para que o document-root aponte para este diretório. - -Você também pode baixar o WebProject diretamente, incluindo o Nette, usando o [Composer |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -No Linux ou macOS, defina as [permissões de escrita |nette:troubleshooting#Configurando Permissões de Diretório] para as pastas `log/` e `temp/`. - -A aplicação WebProject está pronta para ser executada, não é necessário configurar absolutamente nada e você pode exibi-la imediatamente no navegador acessando a pasta `www/`. - - -Requisição HTTP -=============== - -Tudo começa no momento em que o usuário abre a página no navegador. Ou seja, quando o navegador faz uma requisição HTTP ao servidor. A requisição é direcionada para um único arquivo PHP, localizado no diretório público `www/`, que é `index.php`. Digamos que seja uma requisição para o endereço `https://example.com/product/123`. Graças à [configuração adequada do servidor |nette:troubleshooting#Como configurar o servidor para URLs amigáveis], até mesmo esta URL é mapeada para o arquivo `index.php` e ele é executado. - -Sua tarefa é: - -1) inicializar o ambiente -2) obter a fábrica -3) iniciar a aplicação Nette, que tratará da requisição - -Que fábrica? Não estamos fabricando tratores, mas sim páginas web! Aguarde, isso será explicado em breve. - -Com as palavras "inicialização do ambiente", queremos dizer, por exemplo, que o [Tracy|tracy:] é ativado, que é uma ferramenta incrível para logging ou visualização de erros. No servidor de produção, ele registra os erros; no de desenvolvimento, ele os exibe diretamente. Portanto, a inicialização também inclui a decisão sobre se a web está sendo executada no modo de produção ou de desenvolvimento. Para isso, o Nette usa uma [autodeteção inteligente |bootstrapping#Modo de desenvolvimento vs produção]: se você executar a web em localhost, ela será executada no modo de desenvolvimento. Assim, você não precisa configurar nada e a aplicação está imediatamente pronta tanto para o desenvolvimento quanto para a implantação em produção. Esses passos são realizados e detalhadamente descritos no capítulo sobre a [classe Bootstrap|bootstrapping]. - -O terceiro ponto (sim, pulamos o segundo, mas voltaremos a ele) é iniciar a aplicação. O tratamento de requisições HTTP no Nette é responsabilidade da classe `Nette\Application\Application` (doravante `Application`), então quando dizemos iniciar a aplicação, queremos dizer especificamente chamar o método com o nome apropriado `run()` no objeto desta classe. - -O Nette é um mentor que o guia para escrever aplicações limpas de acordo com metodologias comprovadas. E uma das mais comprovadas é chamada de **injeção de dependência**, abreviada como DI. Neste momento, não queremos sobrecarregá-lo com a explicação da DI, para isso existe um [capítulo separado|dependency-injection:introduction], o resultado essencial é que os objetos chave geralmente serão criados para nós por uma fábrica de objetos, chamada de **Contêiner de DI** (abreviado como DIC). Sim, essa é a fábrica da qual falamos há pouco. E ela também nos fabricará o objeto `Application`, por isso precisamos primeiro do contêiner. Obtemo-lo usando a classe `Configurator` e deixamos que ele fabrique o objeto `Application`, chamamos o método `run()` nele e assim a aplicação Nette é iniciada. É exatamente isso que acontece no arquivo [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -A classe Application tem uma única tarefa: responder a uma requisição HTTP. - -Aplicações escritas em Nette são divididas em muitos chamados presenters (em outros frameworks, você pode encontrar o termo controller, é a mesma coisa), que são classes, cada uma representando uma página específica do site: por exemplo, a página inicial; um produto em uma loja virtual; um formulário de login; um feed de sitemap, etc. Uma aplicação pode ter de um a milhares de presenters. - -A Application começa pedindo ao chamado roteador (router) para decidir a qual dos presenters a requisição atual deve ser passada para tratamento. O roteador decide de quem é a responsabilidade. Ele olha para a URL de entrada `https://example.com/product/123` e, com base em como está configurado, decide que este é o trabalho, por exemplo, do **presenter** `Product`, do qual ele desejará como **ação** a exibição (`show`) do produto com `id: 123`. É um bom costume escrever o par presenter + ação separados por dois pontos como `Product:show`. - -Portanto, o roteador transformou a URL no par `Presenter:action` + parâmetros, no nosso caso `Product:show` + `id: 123`. Como tal roteador se parece, você pode ver no arquivo `app/Core/RouterFactory.php` e o descrevemos detalhadamente no capítulo [Roteamento |Routing]. - -Vamos continuar. A Application já conhece o nome do presenter e pode prosseguir. Criando o objeto da classe `ProductPresenter`, que é o código do presenter `Product`. Mais precisamente, ele pede ao Contêiner de DI para fabricar o presenter, porque fabricar é a função dele. - -O presenter pode parecer assim: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // obtemos dados do model e passamos para o template - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -O tratamento da requisição é assumido pelo presenter. E a tarefa é clara: execute a ação `show` com `id: 123`. O que, na linguagem dos presenters, significa que o método `renderShow()` é chamado e recebe `123` no parâmetro `$id`. - -Um presenter pode atender a várias ações, ou seja, ter vários métodos `render()`. Mas recomendamos projetar presenters com uma ou o mínimo possível de ações. - -Então, o método `renderShow(123)` foi chamado, cujo código é um exemplo fictício, mas você pode ver nele como os dados são passados para o template, ou seja, escrevendo em `$this->template`. - -Posteriormente, o presenter retorna uma resposta. Esta pode ser uma página HTML, uma imagem, um documento XML, o envio de um arquivo do disco, JSON ou talvez um redirecionamento para outra página. O importante é que, se não dissermos explicitamente como ele deve responder (o que é o caso de `ProductPresenter`), a resposta será a renderização de um template com uma página HTML. Por quê? Porque em 99% dos casos queremos renderizar um template, então o presenter considera esse comportamento como padrão e quer facilitar nosso trabalho. Esse é o propósito do Nette. - -Nem precisamos indicar qual template renderizar, ele deduzirá o caminho por si só. No caso da ação `show`, ele simplesmente tentará carregar o template `show.latte` no diretório com a classe `ProductPresenter`. Ele também tentará localizar o layout no arquivo `@layout.latte` (mais detalhes sobre [localização de templates |templates#Procurando templates]). - -E então ele renderiza os templates. Com isso, a tarefa do presenter e de toda a aplicação está concluída e o trabalho está finalizado. Se o template não existisse, seria retornada uma página com erro 404. Você pode ler mais sobre presenters na página [Presenters|presenters]. - -[* request-flow.svg *] - -Para ter certeza, vamos tentar recapitular todo o processo com uma URL ligeiramente diferente: - -1) A URL será `https://example.com` -2) Inicializamos a aplicação, o contêiner é criado e `Application::run()` é iniciado -3) O roteador decodifica a URL como o par `Home:default` -4) O objeto da classe `HomePresenter` é criado -5) O método `renderDefault()` é chamado (se existir) -6) O template, por exemplo, `default.latte` com o layout, por exemplo, `@layout.latte` é renderizado - - -Talvez você tenha encontrado muitos termos novos agora, mas acreditamos que eles fazem sentido. Criar aplicações no Nette é muito fácil. - - -Templates -========= - -Já que falamos de templates, no Nette usa-se o sistema de templates [Latte |latte:]. É por isso que as extensões `.latte` nos templates. O Latte é usado, por um lado, porque é o sistema de templates mais seguro para PHP e, ao mesmo tempo, o sistema mais intuitivo. Você não precisa aprender muito de novo, basta o conhecimento de PHP e algumas tags. Você aprenderá tudo na [documentação |templates]. - -No template, [links são criados |creating-links] para outros presenters & ações assim: - -```latte -detalhe do produto -``` - -Simplesmente, em vez da URL real, você escreve o par conhecido `Presenter:action` e especifica quaisquer parâmetros. O truque está no `n:href`, que diz que este atributo será processado pelo Nette. E ele gera: - -```latte -detalhe do produto -``` - -A geração de URLs é responsabilidade do já mencionado roteador. De fato, os roteadores no Nette são excepcionais porque podem realizar não apenas transformações de URL para o par presenter:action, mas também o inverso, ou seja, gerar uma URL a partir do nome do presenter + ação + parâmetros. Graças a isso, no Nette, você pode alterar completamente as formas das URLs em toda a aplicação finalizada, sem alterar um único caractere no template ou presenter. Apenas modificando o roteador. Também graças a isso funciona a chamada canonização, que é outra característica única do Nette, que contribui para um melhor SEO (otimização para motores de busca) ao impedir automaticamente a existência de conteúdo duplicado em URLs diferentes. Muitos programadores consideram isso surpreendente. - - -Componentes interativos -======================= - -Sobre os presenters, precisamos contar mais uma coisa: eles têm um sistema de componentes embutido. Algo semelhante pode ser familiar aos veteranos do Delphi ou ASP.NET Web Forms, algo remotamente parecido é a base do React ou Vue.js. No mundo dos frameworks PHP, é uma característica absolutamente única. - -Componentes são unidades reutilizáveis independentes que inserimos nas páginas (ou seja, presenters). Podem ser [formulários |forms:in-presenter], [datagrids |https://componette.org/contributte/datagrid/], menus, enquetes de votação, na verdade, qualquer coisa que faça sentido usar repetidamente. Podemos criar nossos próprios componentes ou usar alguns da [enorme oferta |https://componette.org] de componentes open source. - -Os componentes influenciam fundamentalmente a abordagem para a criação de aplicações. Eles abrirão novas possibilidades para você compor páginas a partir de unidades pré-preparadas. E, além disso, eles têm algo em comum com [Hollywood |components#Estilo Hollywood]. - - -Contêiner de DI e configuração -============================== - -O Contêiner de DI, ou fábrica de objetos, é o coração de toda a aplicação. - -Não se preocupe, não é nenhuma caixa preta mágica, como poderia parecer das linhas anteriores. Na verdade, é uma classe PHP bastante comum, que o Nette gera e salva no diretório de cache. Ela tem muitos métodos nomeados como `createServiceAbcd()` e cada um deles sabe como fabricar e retornar algum objeto. Sim, também existe o método `createServiceApplication()`, que fabrica `Nette\Application\Application`, que precisávamos no arquivo `index.php` para iniciar a aplicação. E existem métodos que fabricam os presenters individuais. E assim por diante. - -Objetos que o Contêiner de DI cria são, por algum motivo, chamados de serviços. - -O que é realmente especial sobre esta classe é que você não a programa, mas sim o framework. Ele realmente gera o código PHP e o salva no disco. Você apenas dá instruções sobre quais objetos o contêiner deve saber fabricar e como exatamente. E essas instruções são escritas nos [arquivos de configuração |bootstrapping#Configuração do contêiner de DI], para os quais se usa o formato [NEON|neon:format] e, portanto, também têm a extensão `.neon`. - -Os arquivos de configuração servem puramente para instruir o Contêiner de DI. Então, por exemplo, se eu especificar na seção [sessão |http:configuration#Sessão] a opção `expiration: 14 days`, o Contêiner de DI, ao criar o objeto `Nette\Http\Session` representando a sessão, chamará seu método `setExpiration('14 days')` e assim a configuração se tornará realidade. - -Há um capítulo inteiro preparado para você descrevendo tudo o que pode ser [configurado |nette:configuring] e como [definir seus próprios serviços |dependency-injection:services]. - -Assim que você se aprofundar um pouco na criação de serviços, encontrará a palavra [autowiring |dependency-injection:autowiring]. Esta é uma funcionalidade que simplificará sua vida de maneira incrível. Ela pode passar automaticamente objetos para onde você precisa deles (por exemplo, nos construtores de suas classes), sem que você precise fazer nada. Você descobrirá que o Contêiner de DI no Nette é um pequeno milagre. - - -Para onde ir agora? -=================== - -Percorremos os princípios básicos das aplicações no Nette. Até agora, muito superficialmente, mas em breve você se aprofundará e, com o tempo, criará aplicações web maravilhosas. Para onde continuar agora? Você já experimentou o tutorial [Escrevendo a primeira aplicação|quickstart:]? - -Além do que foi descrito acima, o Nette possui todo um arsenal de [classes úteis|utils:], uma [camada de banco de dados|database:], etc. Tente apenas navegar pela documentação. Ou pelo [blog|https://blog.nette.org]. Você descobrirá muitas coisas interessantes. - -Que o framework lhe traga muita alegria 💙 diff --git a/application/pt/multiplier.texy b/application/pt/multiplier.texy deleted file mode 100644 index f9f867f5ca..0000000000 --- a/application/pt/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: componentes dinâmicos -********************************* - -.[perex] -Ferramenta para criação dinâmica de componentes interativos - -Vamos partir de um exemplo típico: temos uma lista de produtos em uma loja virtual, e para cada um queremos exibir um formulário para adicionar o produto ao carrinho. Uma das opções possíveis é envolver toda a listagem em um único formulário. No entanto, um método muito mais conveniente nos é oferecido pelo [api:Nette\Application\UI\Multiplier]. - -O Multiplier permite definir convenientemente uma pequena fábrica para múltiplos componentes. Funciona com base no princípio de componentes aninhados - cada componente que herda de [api:Nette\ComponentModel\Container] pode conter outros componentes. - -.[tip] -Veja o capítulo sobre o [modelo de componentes |components#Componentes em profundidade] na documentação ou a [palestra de Honza Tvrdík|https://www.youtube.com/watch?v=8y3LLexWu-I]. - -A essência do Multiplier é que ele atua na posição de pai, que pode criar seus descendentes dinamicamente usando um callback passado no construtor. Veja o exemplo: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Quantidade de produtos:') - ->setRequired(); - $form->addSubmit('send', 'Adicionar ao carrinho'); - return $form; - }); -} -``` - -Agora podemos, no template, simplesmente deixar renderizar o formulário para cada produto - e cada um será realmente um componente único. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -O argumento passado na tag `{control}` está em um formato que diz: - -1. obtenha o componente `shopForm` -2. e dele obtenha o descendente `$item->id` - -Na primeira chamada do ponto **1.**, `shopForm` ainda não existe, então sua fábrica `createComponentShopForm` é chamada. No componente obtido (instância do Multiplier), a fábrica do formulário específico é então chamada - que é a função anônima que passamos para o Multiplier no construtor. - -Na próxima iteração do foreach, o método `createComponentShopForm` não será mais chamado (o componente existe), mas como estamos procurando por um descendente diferente dele (`$item->id` será diferente em cada iteração), a função anônima será chamada novamente e nos retornará um novo formulário. - -A única coisa que resta é garantir que o formulário adicione ao carrinho realmente o produto que deve - atualmente, o formulário é completamente idêntico para cada produto. A propriedade do Multiplier (e geralmente de cada fábrica de componentes no Nette Framework) nos ajudará, que é que cada fábrica recebe como seu primeiro argumento o nome do componente sendo criado. No nosso caso, será `$item->id`, que é exatamente a informação que precisamos. Basta, portanto, ajustar ligeiramente a criação do formulário: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function ($itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Quantidade de produtos:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Adicionar ao carrinho'); - return $form; - }); -} -``` diff --git a/application/pt/presenters.texy b/application/pt/presenters.texy deleted file mode 100644 index c0f77a364e..0000000000 --- a/application/pt/presenters.texy +++ /dev/null @@ -1,500 +0,0 @@ -Presenters -********** - -
    - -Vamos nos familiarizar com como escrever presenters e templates no Nette. Após a leitura, você saberá: - -- como funciona um presenter -- o que são parâmetros persistentes -- como os templates são renderizados - -
    - -[Já sabemos |how-it-works#Nette Application], que um presenter é uma classe que representa uma página específica de uma aplicação web, por exemplo, a página inicial; um produto em uma loja virtual; um formulário de login; um feed de sitemap, etc. Uma aplicação pode ter de um a milhares de presenters. Em outros frameworks, eles também são chamados de controllers. - -Geralmente, sob o termo presenter, entende-se um descendente da classe [api:Nette\Application\UI\Presenter], que é adequado para gerar interfaces web e ao qual nos dedicaremos no restante deste capítulo. Em um sentido geral, um presenter é qualquer objeto que implementa a interface [api:Nette\Application\IPresenter]. - - -Ciclo de vida do presenter -========================== - -A tarefa do presenter é processar a requisição e retornar uma resposta (que pode ser uma página HTML, uma imagem, um redirecionamento, etc.). - -Portanto, no início, a requisição é passada a ele. Não é diretamente uma requisição HTTP, mas um objeto [api:Nette\Application\Request], no qual a requisição HTTP foi transformada com a ajuda do roteador. Geralmente não interagimos com este objeto, pois o presenter delega inteligentemente o processamento da requisição para outros métodos, que mostraremos agora. - -[* lifecycle.svg *] *** *Ciclo de vida do presenter* .<> - -A imagem representa uma lista de métodos que são chamados sequencialmente de cima para baixo, se existirem. Nenhum deles precisa existir, podemos ter um presenter completamente vazio sem um único método e construir um site estático simples sobre ele. - - -`__construct()` ---------------- - -O construtor não pertence exatamente ao ciclo de vida do presenter, porque é chamado no momento da criação do objeto. Mas o mencionamos devido à sua importância. O construtor (juntamente com o [método inject|best-practices:inject-method-attribute]) serve para passar dependências. - -O presenter não deve cuidar da lógica de negócios da aplicação, escrever e ler do banco de dados, realizar cálculos, etc. Para isso existem classes da camada que chamamos de model. Por exemplo, a classe `ArticleRepository` pode ser responsável por carregar e salvar artigos. Para que o presenter possa trabalhar com ela, ele a solicita [via injeção de dependência |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Imediatamente após receber a requisição, o método `startup()` é chamado. Você pode usá-lo para inicializar propriedades, verificar permissões de usuário, etc. É necessário que o método sempre chame o ancestral `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -Análogo ao método `render()`. Enquanto `render()` se destina a preparar dados para um template específico que será subsequentemente renderizado, em `action()` a requisição é processada sem ligação à renderização do template. Por exemplo, os dados são processados, o usuário é logado ou deslogado, e assim por diante, e então [redireciona para outro lugar |#Redirecionamento]. - -O importante é que `action()` é chamado antes de `render()`, então nele podemos eventualmente mudar o curso dos eventos, ou seja, mudar o template que será renderizado, e também o método `render()` que será chamado. E isso usando `setView('outroView')`. - -Parâmetros da requisição são passados para o método. É possível e recomendado especificar tipos para os parâmetros, por exemplo, `actionShow(int $id, ?string $slug = null)` - se o parâmetro `id` estiver faltando ou não for um inteiro, o presenter retornará um [erro 404 |#Erro 404 e cia] e encerrará a atividade. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -O método processa os chamados sinais, com os quais nos familiarizaremos no capítulo dedicado aos [componentes |components#Sinal]. Ele é destinado principalmente a componentes e ao processamento de requisições AJAX. - -Parâmetros da requisição são passados para o método, como no caso de `action()`, incluindo verificação de tipo. - - -`beforeRender()` ----------------- - -O método `beforeRender`, como o nome sugere, é chamado antes de cada método `render()`. É usado para configuração comum do template, passagem de variáveis para o layout e assim por diante. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -O local onde preparamos o template para a renderização subsequente, passamos dados para ele, etc. - -Parâmetros da requisição são passados para o método, como no caso de `action()`, incluindo verificação de tipo. - -```php -public function renderShow(int $id): void -{ - // obtemos dados do model e passamos para o template - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -O método `afterRender`, como o nome novamente sugere, é chamado após cada método `render()`. É usado de forma bastante excepcional. - - -`shutdown()` ------------- - -É chamado no final do ciclo de vida do presenter. - - -**Um bom conselho antes de prosseguirmos**. Como pode ser visto, um presenter pode atender a várias ações/views, ou seja, ter vários métodos `render()`. Mas recomendamos projetar presenters com uma ou o mínimo possível de ações. - - -Envio da resposta -================= - -A resposta do presenter geralmente é a [renderização de um template com uma página HTML|templates], mas também pode ser o envio de um arquivo, JSON ou talvez um redirecionamento para outra página. - -A qualquer momento durante o ciclo de vida, podemos enviar uma resposta usando um dos seguintes métodos e, ao mesmo tempo, encerrar o presenter: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` e `forward()` [redirecionam |#Redirecionamento] -- `error()` encerra o presenter [devido a um erro |#Erro 404 e cia] -- `sendJson($data)` encerra o presenter e [envia dados |#Envio de JSON] no formato JSON -- `sendTemplate()` encerra o presenter e imediatamente [renderiza o template |templates] -- `sendResponse($response)` encerra o presenter e envia uma [resposta personalizada |#Respostas] -- `terminate()` encerra o presenter sem resposta - -Se você não chamar nenhum desses métodos, o presenter automaticamente procederá à renderização do template. Por quê? Porque em 99% dos casos queremos renderizar um template, então o presenter considera esse comportamento como padrão e quer facilitar nosso trabalho. - - -Criação de links -================ - -O presenter possui o método `link()`, com o qual é possível criar links URL para outros presenters. O primeiro parâmetro é o presenter & ação de destino, seguido pelos argumentos passados, que podem ser especificados como um array: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'pt']); -``` - -No template, links para outros presenters & ações são criados desta forma: - -```latte -detalhe do produto -``` - -Simplesmente, em vez da URL real, você escreve o par conhecido `Presenter:action` e especifica quaisquer parâmetros. O truque está no `n:href`, que diz que este atributo será processado pelo Latte e gerará a URL real. No Nette, você não precisa pensar em URLs, apenas em presenters e ações. - -Mais informações podem ser encontradas no capítulo [Criando Links URL|creating-links]. - - -Redirecionamento -================ - -Para ir para outro presenter, usam-se os métodos `redirect()` e `forward()`, que têm uma sintaxe muito semelhante ao método [link() |#Criação de links]. - -O método `forward()` vai para o novo presenter imediatamente sem um redirecionamento HTTP: - -```php -$this->forward('Product:show'); -``` - -Exemplo do chamado redirecionamento temporário com código HTTP 302 (ou 303, se o método da requisição atual for POST): - -```php -$this->redirect('Product:show', $id); -``` - -O redirecionamento permanente com código HTTP 301 é alcançado assim: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Para redirecionar para outra URL fora da aplicação, pode-se usar o método `redirectUrl()`. O código HTTP pode ser passado como segundo parâmetro, o padrão é 302 (ou 303, se o método da requisição atual for POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -O redirecionamento encerra imediatamente a atividade do presenter lançando a chamada exceção de terminação silenciosa `Nette\Application\AbortException`. - -Antes do redirecionamento, é possível enviar uma [flash message |#Mensagens Flash], ou seja, mensagens que serão exibidas no template após o redirecionamento. - - -Mensagens Flash -=============== - -São mensagens que geralmente informam sobre o resultado de alguma operação. Uma característica importante das mensagens flash é que elas estão disponíveis no template mesmo após um redirecionamento. Mesmo após serem exibidas, elas permanecem ativas por mais 30 segundos – por exemplo, caso o usuário atualize a página devido a um erro de transmissão - a mensagem não desaparecerá imediatamente. - -Basta chamar o método [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] e o presenter se encarrega de passá-la para o template. O primeiro parâmetro é o texto da mensagem e o segundo parâmetro opcional é o seu tipo (error, warning, info, etc.). O método `flashMessage()` retorna uma instância da mensagem flash, à qual informações adicionais podem ser adicionadas. - -```php -$this->flashMessage('O item foi excluído.'); -$this->redirect(/* ... */); // e redirecionamos -``` - -No template, essas mensagens estão disponíveis na variável `$flashes` como objetos `stdClass`, que contêm as propriedades `message` (texto da mensagem), `type` (tipo da mensagem) e podem conter as informações do usuário já mencionadas. Nós as renderizamos assim, por exemplo: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Erro 404 e cia. -=============== - -Se a requisição não puder ser atendida, por exemplo, porque o artigo que queremos exibir não existe no banco de dados, lançamos um erro 404 com o método `error(?string $message = null, int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -O código HTTP do erro pode ser passado como segundo parâmetro, o padrão é 404. O método funciona lançando a exceção `Nette\Application\BadRequestException`, após o qual `Application` passa o controle para o error-presenter. Que é um presenter cuja tarefa é exibir uma página informando sobre o erro ocorrido. A configuração do error-presenter é feita na [configuração da aplicação|configuration]. - - -Envio de JSON -============= - -Exemplo de um método de ação que envia dados no formato JSON e encerra o presenter: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Parâmetros da requisição .{data-version:3.1.14} -=============================================== - -O presenter e também cada componente obtêm seus parâmetros da requisição HTTP. Você pode descobrir seu valor usando o método `getParameter($name)` ou `getParameters()`. Os valores são strings ou arrays de strings, são basicamente dados brutos obtidos diretamente da URL. - -Para maior conveniência, recomendamos tornar os parâmetros acessíveis através de propriedades. Basta marcá-los com o atributo `#[Parameter]`: - -```php -use Nette\Application\Attributes\Parameter; // esta linha é importante - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // deve ser público -} -``` - -Recomendamos especificar o tipo de dados para a propriedade (por exemplo, `string`) e o Nette converterá automaticamente o valor de acordo com ele. Os valores dos parâmetros também podem ser [validados |#Validação de parâmetros]. - -Ao criar um link, o valor dos parâmetros pode ser definido diretamente: - -```latte -clique -``` - - -Parâmetros persistentes -======================= - -Parâmetros persistentes são usados para manter o estado entre diferentes requisições. Seu valor permanece o mesmo mesmo após clicar em um link. Ao contrário dos dados na sessão, eles são transmitidos na URL. E isso de forma totalmente automática, não sendo necessário especificá-los explicitamente em `link()` ou `n:href`. - -Exemplo de uso? Você tem uma aplicação multilíngue. O idioma atual é um parâmetro que deve estar constantemente presente na URL. Mas seria incrivelmente tedioso especificá-lo em cada link. Então você o transforma em um parâmetro persistente `lang` e ele será transmitido por si só. Ótimo! - -Criar um parâmetro persistente no Nette é extremamente simples. Basta criar uma propriedade pública e marcá-la com um atributo: (anteriormente usava-se `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // esta linha é importante - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // deve ser público -} -``` - -Se `$this->lang` tiver o valor, por exemplo, `'en'`, então os links criados usando `link()` ou `n:href` também conterão o parâmetro `lang=en`. E após clicar no link, novamente `$this->lang = 'en'`. - -Recomendamos especificar o tipo de dados para a propriedade (por exemplo, `string`) e você também pode especificar um valor padrão. Os valores dos parâmetros podem ser [validados |#Validação de parâmetros]. - -Parâmetros persistentes são normalmente transmitidos entre todas as ações de um determinado presenter. Para que sejam transmitidos também entre vários presenters, é necessário defini-los: - -- em um ancestral comum do qual os presenters herdam -- em uma trait que os presenters usam: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Ao criar um link, o valor do parâmetro persistente pode ser alterado: - -```latte -detalhe em português -``` - -Ou pode ser *resetado*, ou seja, removido da URL. Então ele assumirá seu valor padrão: - -```latte -clique -``` - - -Componentes interativos -======================= - -Presenters têm um sistema de componentes embutido. Componentes são unidades reutilizáveis independentes que inserimos nos presenters. Podem ser [formulários |forms:in-presenter], datagrids, menus, na verdade, qualquer coisa que faça sentido usar repetidamente. - -Como os componentes são inseridos no presenter e subsequentemente usados? Isso você aprenderá no capítulo [Componentes |components]. Você descobrirá até o que eles têm em comum com Hollywood. - -E onde posso obter componentes? Na página [Componette |https://componette.org/search/component] você encontrará componentes open-source e também uma série de outros add-ons para Nette, que foram colocados lá por voluntários da comunidade em torno do framework. - - -Vamos aprofundar -================ - -.[tip] -Com o que mostramos até agora neste capítulo, você provavelmente se sairá bem. As linhas a seguir são destinadas àqueles que estão interessados em presenters em profundidade e querem saber absolutamente tudo. - - -Validação de parâmetros ------------------------ - -Os valores dos [#parâmetros da requisição] e [#parâmetros persistentes] recebidos da URL são escritos nas propriedades pelo método `loadState()`. Ele também verifica se o tipo de dados especificado na propriedade corresponde, caso contrário, responde com um erro 404 e a página não é exibida. - -Nunca confie cegamente nos parâmetros, pois eles podem ser facilmente sobrescritos pelo usuário na URL. Assim, por exemplo, verificamos se o idioma `$this->lang` está entre os suportados. Uma maneira adequada é sobrescrever o método mencionado `loadState()`: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // aqui $this->lang é definido - // segue a verificação personalizada do valor: - if (!in_array($this->lang, ['en', 'pt'])) { - $this->error(); - } - } -} -``` - - -Salvar e restaurar requisição ------------------------------ - -A requisição que o presenter processa é um objeto [api:Nette\Application\Request] e é retornado pelo método do presenter `getRequest()`. - -A requisição atual pode ser salva na sessão ou, inversamente, restaurada dela e deixar o presenter executá-la novamente. Isso é útil, por exemplo, em uma situação em que o usuário está preenchendo um formulário e sua sessão expira. Para não perder os dados, antes de redirecionar para a página de login, salvamos a requisição atual na sessão usando `$reqId = $this->storeRequest()`, que retorna seu identificador na forma de uma string curta e o passamos como parâmetro para o presenter de login. - -Após o login, chamamos o método `$this->restoreRequest($reqId)`, que recupera a requisição da sessão e encaminha para ela. O método verifica se a requisição foi criada pelo mesmo usuário que está logado agora. Se outro usuário fizer login ou a chave for inválida, ele não faz nada e o programa continua. - -Veja o tutorial [Como retornar à página anterior |best-practices:restore-request]. - - -Canonização ------------ - -Presenters têm uma característica realmente ótima que contribui para um melhor SEO (otimização para motores de busca). Eles impedem automaticamente a existência de conteúdo duplicado em URLs diferentes. Se houver várias URLs que levam ao mesmo destino, por exemplo, `/index` e `/index?page=1`, o framework determina uma delas como primária (canônica) e redireciona as outras para ela usando o código HTTP 301. Graças a isso, os motores de busca não indexam suas páginas duas vezes e não diluem seu page rank. - -Este processo é chamado de canonização. A URL canônica é aquela gerada pelo [roteador|routing], geralmente a primeira rota correspondente na coleção. - -A canonização está ativada por padrão e pode ser desativada através de `$this->autoCanonicalize = false`. - -O redirecionamento não ocorre durante uma requisição AJAX ou POST, pois isso causaria perda de dados ou não teria valor agregado do ponto de vista de SEO. - -Você também pode invocar a canonização manualmente usando o método `canonicalize()`, ao qual, de forma semelhante ao método `link()`, são passados o presenter, a ação e os parâmetros. Ele cria um link e o compara com a URL atual. Se diferirem, ele redireciona para o link gerado. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // redireciona se $slug for diferente de $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - - -Eventos -------- - -Além dos métodos `startup()`, `beforeRender()` e `shutdown()`, que são chamados como parte do ciclo de vida do presenter, é possível definir outras funções que devem ser chamadas automaticamente. O presenter define os chamados [eventos |nette:glossary#Eventos], cujos manipuladores você adiciona aos arrays `$onStartup`, `$onRender` e `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Os manipuladores no array `$onStartup` são chamados logo antes do método `startup()`, `$onRender` entre `beforeRender()` e `render()` e, finalmente, `$onShutdown` logo antes de `shutdown()`. - - -Respostas ---------- - -A resposta que o presenter retorna é um objeto que implementa a interface [api:Nette\Application\Response]. Há uma série de respostas prontas disponíveis: - -- [api:Nette\Application\Responses\CallbackResponse] - envia um callback -- [api:Nette\Application\Responses\FileResponse] - envia um arquivo -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - envia JSON -- [api:Nette\Application\Responses\RedirectResponse] - redirecionamento -- [api:Nette\Application\Responses\TextResponse] - envia texto -- [api:Nette\Application\Responses\VoidResponse] - resposta vazia - -As respostas são enviadas pelo método `sendResponse()`: - -```php -use Nette\Application\Responses; - -// Texto simples -$this->sendResponse(new Responses\TextResponse('Olá Nette!')); - -// Envia um arquivo -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// A resposta será um callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Olá

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - - -Restrição de acesso usando `#[Requires]` .{data-version:3.2.2} --------------------------------------------------------------- - -O atributo `#[Requires]` oferece opções avançadas para restringir o acesso a presenters e seus métodos. Pode ser usado para especificar métodos HTTP, exigir requisição AJAX, restringir à mesma origem (same origin) e acesso apenas via encaminhamento (forwarding). O atributo pode ser aplicado tanto a classes de presenters quanto a métodos individuais `action()`, `render()`, `handle()` e `createComponent()`. - -Você pode especificar estas restrições: -- em métodos HTTP: `#[Requires(methods: ['GET', 'POST'])]` -- exigir requisição AJAX: `#[Requires(ajax: true)]` -- acesso apenas da mesma origem: `#[Requires(sameOrigin: true)]` -- acesso apenas via forward: `#[Requires(forward: true)]` -- restrição a ações específicas: `#[Requires(actions: 'default')]` - -Detalhes podem ser encontrados no tutorial [Como usar o atributo Requires |best-practices:attribute-requires]. - - -Verificação do método HTTP --------------------------- - -Presenters no Nette verificam automaticamente o método HTTP de cada requisição recebida. A razão para esta verificação é principalmente a segurança. Por padrão, os métodos `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH` são permitidos. - -Se você quiser permitir adicionalmente, por exemplo, o método `OPTIONS`, use o atributo `#[Requires]` (a partir do Nette Application v3.2): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Na versão 3.1, a verificação é feita em `checkHttpMethod()`, que verifica se o método especificado na requisição está contido no array `$presenter->allowedMethods`. Adicione o método assim: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -É importante enfatizar que, se você permitir o método `OPTIONS`, deverá subsequentemente tratá-lo adequadamente dentro do seu presenter. O método é frequentemente usado como a chamada requisição preflight, que o navegador envia automaticamente antes da requisição real, quando é necessário verificar se a requisição é permitida do ponto de vista da política CORS (Cross-Origin Resource Sharing). Se você permitir o método, mas não implementar a resposta correta, isso pode levar a inconsistências e potenciais problemas de segurança. - - -Leitura adicional -================= - -- [Métodos e atributos inject |best-practices:inject-method-attribute] -- [Compondo presenters a partir de traits |best-practices:presenter-traits] -- [Passando configurações para presenters |best-practices:passing-settings-to-presenters] -- [Como retornar à página anterior |best-practices:restore-request] diff --git a/application/pt/routing.texy b/application/pt/routing.texy deleted file mode 100644 index 6becfb0d3b..0000000000 --- a/application/pt/routing.texy +++ /dev/null @@ -1,721 +0,0 @@ -Roteamento -********** - -
    - -O Roteador cuida de tudo relacionado aos endereços URL, para que você não precise mais pensar neles. Vamos mostrar: - -- como configurar o roteador para que as URLs fiquem como desejado -- falaremos sobre SEO e redirecionamento -- e mostraremos como escrever seu próprio roteador - -
    - - -URLs mais amigáveis (ou também cool ou pretty URLs) são mais usáveis, memoráveis e contribuem positivamente para o SEO. O Nette pensa nisso e atende plenamente aos desenvolvedores. Você pode projetar para sua aplicação exatamente a estrutura de URLs que desejar. Você pode até projetá-la quando a aplicação já estiver pronta, pois isso pode ser feito sem intervenções no código ou nos templates. É definido de forma elegante em um [único local |#Integração na aplicação], no roteador, e não está espalhado na forma de anotações em todos os presenters. - -O Roteador no Nette é extraordinário por ser **bidirecional.** Ele pode tanto decodificar URLs na requisição HTTP quanto criar links. Portanto, desempenha um papel crucial na [Nette Application |how-it-works#Nette Application], pois decide qual presenter e ação executará a requisição atual, mas também é usado para [gerar URLs |creating-links] no template, etc. - -No entanto, o roteador não está limitado apenas a este uso, você pode usá-lo em aplicações onde presenters não são usados de forma alguma, para APIs REST, etc. Mais na seção [#Uso independente]. - - -Coleção de rotas -================ - -A maneira mais agradável de definir a aparência das URLs na aplicação é oferecida pela classe [api:Nette\Application\Routers\RouteList]. A definição consiste em uma lista das chamadas rotas, ou seja, máscaras de URLs e seus presenters e ações associados por meio de uma API simples. Não precisamos nomear as rotas de forma alguma. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -O exemplo diz que se abrirmos `https://domain.com/rss.xml` no navegador, o presenter `Feed` com a ação `rss` será exibido, se `https://domain.com/article/12`, o presenter `Article` com a ação `view` será exibido, etc. No caso de não encontrar uma rota adequada, a Nette Application reage lançando a exceção [BadRequestException |api:Nette\Application\BadRequestException], que é exibida ao usuário como uma página de erro 404 Not Found. - - -Ordem das rotas ---------------- - -A **ordem** em que as rotas individuais são listadas é **absolutamente crucial**, pois elas são avaliadas sequencialmente de cima para baixo. A regra é que declaramos as rotas **das mais específicas para as mais gerais**: - -```php -// ERRADO: 'rss.xml' é capturado pela primeira rota e entende esta string como -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// CORRETO -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -As rotas também são avaliadas de cima para baixo ao gerar links: - -```php -// ERRADO: link para 'Feed:rss' gera como 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// CORRETO -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Não esconderemos de você que a montagem correta das rotas requer alguma habilidade. Antes de dominá-la, o [painel de roteamento |#Depuração do roteador] será um auxiliar útil. - - -Máscara e parâmetros --------------------- - -A máscara descreve o caminho relativo a partir do diretório raiz da web. A máscara mais simples é uma URL estática: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Frequentemente, as máscaras contêm os chamados **parâmetros**. Eles são indicados entre colchetes angulares (por exemplo, ``) e são passados para o presenter de destino, por exemplo, para o método `renderShow(int $year)` ou para o parâmetro persistente `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -O exemplo diz que se abrirmos `https://example.com/chronicle/2020` no navegador, o presenter `History` com a ação `show` e o parâmetro `year: 2020` será exibido. - -Podemos definir um valor padrão para os parâmetros diretamente na máscara, tornando-os opcionais: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -A rota agora também aceitará a URL `https://example.com/chronicle/`, que novamente exibirá `History:show` com o parâmetro `year: 2020`. - -O parâmetro também pode ser, obviamente, o nome do presenter e da ação. Por exemplo, assim: - -```php -$router->addRoute('/', 'Home:default'); -``` - -A rota especificada aceita, por exemplo, URLs no formato `/article/edit` ou também `/catalog/list` e as entende como presenters e ações `Article:edit` e `Catalog:list`. - -Ao mesmo tempo, ela atribui aos parâmetros `presenter` e `action` os valores padrão `Home` e `default`, tornando-os também opcionais. Portanto, a rota também aceita URLs no formato `/article` e a entende como `Article:default`. Ou vice-versa, um link para `Product:default` gerará o caminho `/product`, um link para o padrão `Home:default` o caminho `/`. - -A máscara pode descrever não apenas o caminho relativo a partir do diretório raiz da web, mas também o caminho absoluto, se começar com uma barra, ou até mesmo a URL absoluta inteira, se começar com duas barras: - -```php -// relativo ao document root -$router->addRoute('/', /* ... */); - -// caminho absoluto (relativo ao domínio) -$router->addRoute('//', /* ... */); - -// URL absoluta incluindo domínio (relativa ao esquema) -$router->addRoute('//.example.com//', /* ... */); - -// URL absoluta incluindo esquema -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Expressões de validação ------------------------ - -Para cada parâmetro, pode-se estabelecer uma condição de validação usando uma [expressão regular|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Por exemplo, para o parâmetro `id`, determinamos que ele só pode conter dígitos usando a regex `\d+`: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -A expressão regular padrão para todos os parâmetros é `[^/]+`, ou seja, tudo exceto a barra. Se um parâmetro precisar aceitar também barras, especificamos a expressão `.+`: - -```php -// aceita https://example.com/a/b/c, path será 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Sequências opcionais --------------------- - -Na máscara, partes opcionais podem ser marcadas usando colchetes. Qualquer parte da máscara pode ser opcional, e elas também podem conter parâmetros: - -```php -$router->addRoute('[/]', /* ... */); - -// Aceita caminhos: -// /pt/download => lang => pt, name => download -// /download => lang => null, name => download -``` - -Quando um parâmetro faz parte de uma sequência opcional, ele obviamente também se torna opcional. Se não tiver um valor padrão especificado, será null. - -Partes opcionais também podem estar no domínio: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -As sequências podem ser aninhadas e combinadas livremente: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Aceita caminhos: -// /pt/ola -// /en-us/ola -// /ola -// /ola/page-12 -``` - -Ao gerar URLs, busca-se a variante mais curta, então tudo que pode ser omitido, é omitido. Por isso, por exemplo, a rota `index[.html]` gera o caminho `/index`. É possível reverter o comportamento especificando um ponto de exclamação após o colchete esquerdo: - -```php -// aceita /ola e /ola.html, gera /ola -$router->addRoute('[.html]', /* ... */); - -// aceita /ola e /ola.html, gera /ola.html -$router->addRoute('[!.html]', /* ... */); -``` - -Parâmetros opcionais (ou seja, parâmetros com valor padrão) sem colchetes se comportam basicamente como se estivessem entre colchetes da seguinte forma: - -```php -$router->addRoute('//', /* ... */); - -// corresponde a isto: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Se quiséssemos influenciar o comportamento da barra final, para que, por exemplo, em vez de `/home/` fosse gerado apenas `/home`, poderíamos fazer isso assim: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Caracteres curinga ------------------- - -Na máscara de caminho absoluto, podemos usar os seguintes caracteres curinga para evitar, por exemplo, a necessidade de escrever o domínio na máscara, que pode diferir entre os ambientes de desenvolvimento e produção: - -- `%tld%` = top level domain, por exemplo, `com` ou `org` -- `%sld%` = second level domain, por exemplo, `example` -- `%domain%` = domínio sem subdomínios, por exemplo, `example.com` -- `%host%` = host completo, por exemplo, `www.example.com` -- `%basePath%` = caminho para o diretório raiz - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Para uma especificação mais detalhada, pode-se usar uma forma ainda mais estendida, onde, além dos valores padrão, podemos definir outras propriedades dos parâmetros, como uma expressão regular de validação (veja o parâmetro `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -É importante notar que se os parâmetros definidos no array não estiverem listados na máscara do caminho, seus valores não podem ser alterados, nem mesmo usando parâmetros de consulta especificados após o ponto de interrogação na URL. - - -Filtros e traduções -------------------- - -Escrevemos o código-fonte da aplicação em inglês, mas se o site precisar ter URLs em português, então um roteamento simples do tipo: - -```php -$router->addRoute('/', 'Home:default'); -``` - -gerará URLs em inglês, como `/product/123` ou `/cart`. Se quisermos ter presenters e ações na URL representados por palavras em português (por exemplo, `/produto/123` ou `/carrinho`), podemos usar um dicionário de tradução. Para escrevê-lo, já precisamos da variante "mais verbosa" do segundo parâmetro: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // string na URL => presenter - 'produto' => 'Product', - 'carrinho' => 'Cart', - 'catalogo' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'lista' => 'list', - ], - ], -]); -``` - -Várias chaves do dicionário de tradução podem levar ao mesmo presenter. Assim, diferentes aliases são criados para ele. A variante canônica (ou seja, aquela que estará na URL gerada) é considerada a última chave. - -A tabela de tradução pode ser usada desta forma para qualquer parâmetro. Se a tradução não existir, o valor original é usado. Podemos alterar esse comportamento adicionando `Route::FilterStrict => true`, e a rota então rejeitará a URL se o valor não estiver no dicionário. - -Além do dicionário de tradução na forma de array, também é possível aplicar funções de tradução personalizadas. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -A função `Route::FilterIn` converte entre o parâmetro na URL e a string que é então passada para o presenter, a função `FilterOut` garante a conversão na direção oposta. - -Os parâmetros `presenter`, `action` e `module` já possuem filtros predefinidos que convertem entre o estilo PascalCase ou camelCase e o kebab-case usado na URL. O valor padrão dos parâmetros já é escrito na forma transformada, então, por exemplo, no caso do presenter, escrevemos ``, não ``. - - -Filtros gerais --------------- - -Além dos filtros destinados a parâmetros específicos, também podemos definir filtros gerais que recebem um array associativo de todos os parâmetros, que podem modificar de qualquer forma e depois retorná-los. Definimos filtros gerais sob a chave `null`. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Filtros gerais oferecem a possibilidade de ajustar o comportamento da rota de absolutamente qualquer maneira. Podemos usá-los, por exemplo, para modificar parâmetros com base em outros parâmetros. Por exemplo, traduzir `` e `` com base no valor atual do parâmetro ``. - -Se um parâmetro tiver um filtro próprio definido e, ao mesmo tempo, existir um filtro geral, o `FilterIn` próprio será executado antes do geral e, inversamente, o `FilterOut` geral antes do próprio. Ou seja, dentro do filtro geral, os valores dos parâmetros `presenter` ou `action` estão escritos no estilo PascalCase ou camelCase. - - -Rotas de sentido único (OneWay) -------------------------------- - -Rotas de sentido único são usadas para preservar a funcionalidade de URLs antigas que a aplicação não gera mais, mas ainda aceita. Nós as marcamos com o sinalizador `OneWay`: - -```php -// URL antiga /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); -// nova URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -Ao acessar a URL antiga, o presenter redireciona automaticamente para a nova URL, para que os motores de busca não indexem essas páginas duas vezes (veja [#SEO e canonização]). - - -Roteamento dinâmico com callbacks ---------------------------------- - -O roteamento dinâmico com callbacks permite atribuir diretamente funções (callbacks) às rotas, que são executadas quando o caminho correspondente é visitado. Esta funcionalidade flexível permite criar rápida e eficientemente vários endpoints para a sua aplicação: - -```php -$router->addRoute('test', function () { - echo 'você está no endereço /test'; -}); -``` - -Você também pode definir parâmetros na máscara, que são passados automaticamente para o seu callback: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'pt' => 'Bem-vindo à versão em português do nosso site!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - - -Módulos -------- - -Se tivermos várias rotas que pertencem a um [módulo |directory-structure#Presenters e templates] comum, usamos `withModule()`: - -```php -$router = new RouteList; -$router->withModule('Forum') // as rotas seguintes fazem parte do módulo Forum - ->addRoute('rss', 'Feed:rss') // o presenter será Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // as rotas seguintes fazem parte do módulo Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Uma alternativa é usar o parâmetro `module`: - -```php -// URL manage/dashboard/default mapeia para o presenter Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - - -Subdomínios ------------ - -Podemos agrupar coleções de rotas por subdomínios: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -No nome do domínio, também é possível usar [#Caracteres curinga]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Prefixo de caminho ------------------- - -Podemos agrupar coleções de rotas pelo caminho na URL: - -```php -$router = new RouteList; -$router->withPath('loja') - ->addRoute('rss', 'Feed:rss') // captura URL /loja/rss - ->addRoute('/'); // captura URL /loja// -``` - - -Combinações ------------ - -Podemos combinar as agrupações acima: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Parâmetros de consulta (Query) ------------------------------- - -As máscaras também podem conter parâmetros de consulta (parâmetros após o ponto de interrogação na URL). Não é possível definir uma expressão de validação para eles, mas pode-se alterar o nome sob o qual são passados para o presenter: - -```php -// queremos usar o parâmetro de consulta 'cat' na aplicação com o nome 'categoryId' -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Parâmetros Foo --------------- - -Agora estamos indo mais a fundo. Parâmetros Foo são basicamente parâmetros sem nome que permitem corresponder a uma expressão regular. Um exemplo é uma rota que aceita `/index`, `/index.html`, `/index.htm` e `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -Também é possível definir explicitamente a string que será usada ao gerar a URL. A string deve ser colocada diretamente após o ponto de interrogação. A seguinte rota é semelhante à anterior, mas gera `/index.html` em vez de `/index`, porque a string `.html` está definida como o valor de geração: - -```php -$router->addRoute('index', /* ... */); -``` - - -Integração na aplicação -======================= - -Para integrar o roteador criado na aplicação, precisamos informar o Contêiner de DI sobre ele. O caminho mais fácil é preparar uma fábrica que produzirá o objeto roteador e informar na configuração do contêiner que ele deve usá-la. Digamos que, para esse fim, escrevamos o método `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -Na [configuração |dependency-injection:services], então escrevemos: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Quaisquer dependências, como banco de dados, etc., são passadas para o método de fábrica como seus parâmetros usando [autowiring|dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Um roteador muito mais simples do que a coleção de rotas é o [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Usamo-lo quando não temos requisitos especiais para a forma da URL, se `mod_rewrite` (ou suas alternativas) não estiver disponível, ou se ainda não quisermos lidar com URLs bonitas. - -Ele gera endereços aproximadamente neste formato: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -O parâmetro do construtor SimpleRouter é o presenter & ação padrão para o qual deve ser direcionado se abrirmos a página sem parâmetros, por exemplo, `http://example.com/`. - -```php -// o presenter padrão será 'Home' e a ação 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Recomendamos definir o SimpleRouter diretamente na [configuração |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO e canonização -================= - -O framework contribui para o SEO (otimização para motores de busca) ao impedir a duplicação de conteúdo em URLs diferentes. Se houver vários endereços que levam ao mesmo destino, por exemplo, `/index` e `/index.html`, o framework determina o primeiro deles como primário (canônico) e redireciona os outros para ele usando o código HTTP 301. Graças a isso, os motores de busca não indexam suas páginas duas vezes e não diluem seu page rank. - -Este processo é chamado de canonização. A URL canônica é aquela gerada pelo roteador, ou seja, a primeira rota correspondente na coleção sem o sinalizador OneWay. Por isso, na coleção, listamos as **rotas primárias primeiro**. - -A canonização é realizada pelo presenter, mais no capítulo [canonização |presenters#Canonização]. - - -HTTPS -===== - -Para usar o protocolo HTTPS, é necessário habilitá-lo na hospedagem e configurar corretamente o servidor. - -O redirecionamento de todo o site para HTTPS deve ser configurado no nível do servidor, por exemplo, usando o arquivo .htaccess no diretório raiz da nossa aplicação, com o código HTTP 301. A configuração pode variar dependendo da hospedagem e se parece aproximadamente com isto: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -O roteador gera URLs com o mesmo protocolo com que a página foi carregada, então nada mais precisa ser configurado. - -No entanto, se excepcionalmente precisarmos que rotas diferentes sejam executadas sob protocolos diferentes, especificamos isso na máscara da rota: - -```php -// Gerará endereço com HTTP -$router->addRoute('http://%host%//', /* ... */); - -// Gerará endereço com HTTPS -$router->addRoute('https://%host%//', /* ... */); -``` - - -Depuração do roteador -===================== - -O painel de roteamento exibido na [Barra Tracy |tracy:] é um auxiliar útil que exibe a lista de rotas e também os parâmetros que o roteador obteve da URL. - -A barra verde com o símbolo ✓ representa a rota que processou a URL atual, a cor azul e o símbolo ≈ indicam rotas que também processariam a URL se a verde não as tivesse precedido. Em seguida, vemos o presenter & ação atuais. - -[* routing-debugger.webp *] - -Ao mesmo tempo, se ocorrer um redirecionamento inesperado devido à [canonização |#SEO e canonização], é útil olhar para o painel na barra *redirect*, onde você descobrirá como o roteador entendeu originalmente a URL e por que redirecionou. - -.[note] -Ao depurar o roteador, recomendamos abrir as Ferramentas do Desenvolvedor no navegador (Ctrl+Shift+I ou Cmd+Option+I) e desativar o cache no painel Network, para que os redirecionamentos não sejam armazenados nele. - - -Desempenho -========== - -O número de rotas afeta a velocidade do roteador. Seu número definitivamente não deve exceder algumas dezenas. Se o seu site tiver uma estrutura de URL muito complicada, você pode escrever um [#Roteador personalizado] personalizado. - -Se o roteador não tiver dependências, por exemplo, no banco de dados, e sua fábrica não aceitar argumentos, podemos serializar sua forma compilada diretamente no Contêiner de DI e, assim, acelerar ligeiramente a aplicação. - -```neon -routing: - cache: true -``` - - -Roteador personalizado -====================== - -As linhas a seguir são destinadas a usuários muito avançados. Você pode criar seu próprio roteador e integrá-lo naturalmente à coleção de rotas. O Roteador é uma implementação da interface [api:Nette\Routing\Router] com dois métodos: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -O método `match` processa a requisição atual [$httpRequest |http:request], da qual é possível obter não apenas a URL, mas também cabeçalhos, etc., em um array contendo o nome do presenter e seus parâmetros. Se não puder processar a requisição, retorna null. Ao processar a requisição, devemos retornar pelo menos o presenter e a ação. O nome do presenter é completo e contém também eventuais módulos: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -O método `constructUrl`, por outro lado, monta a URL absoluta final a partir do array de parâmetros. Para isso, pode usar informações do parâmetro [`$refUrl`|api:Nette\Http\UrlScript], que é a URL atual. - -Você o adiciona à coleção de rotas usando `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Uso independente -================ - -Por uso independente, entendemos a utilização das capacidades do roteador em uma aplicação que não utiliza Nette Application e presenters. Quase tudo o que mostramos neste capítulo se aplica a ele, com estas diferenças: - -- para coleções de rotas, usamos a classe [api:Nette\Routing\RouteList] -- como simple router, a classe [api:Nette\Routing\SimpleRouter] -- como não existe o par `Presenter:action`, usamos a [#Notação estendida] - -Então, novamente, criamos um método que montará o roteador para nós, por exemplo: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Se você usa um Contêiner de DI, o que recomendamos, adicionamos novamente o método à configuração e, em seguida, obtemos o roteador juntamente com a requisição HTTP do contêiner: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Ou fabricamos os objetos diretamente: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Agora resta apenas colocar o roteador para trabalhar: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // não foi encontrada uma rota correspondente, enviamos erro 404 - exit; -} - -// processamos os parâmetros obtidos -$controller = $params['controller']; -// ... -``` - -E, inversamente, usamos o roteador para montar um link: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/router}} diff --git a/application/pt/templates.texy b/application/pt/templates.texy deleted file mode 100644 index c5fb40d21d..0000000000 --- a/application/pt/templates.texy +++ /dev/null @@ -1,323 +0,0 @@ -Templates -********* - -.[perex] -O Nette usa o sistema de templates [Latte |latte:]. Por um lado, porque é o sistema de templates mais seguro para PHP e, ao mesmo tempo, o sistema mais intuitivo. Você não precisa aprender muito de novo, basta o conhecimento de PHP e algumas tags. - -É comum que uma página seja composta por um template de layout + o template da ação específica. Assim pode parecer um template de layout, observe os blocos `{block}` e a tag `{include}`: - -```latte - - - - {block title}Minha App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -E este será o template da ação: - -```latte -{block title}Página Inicial{/block} - -{block content} -

    Página Inicial

    -... -{/block} -``` - -Ele define o bloco `content`, que será inserido no lugar de `{include content}` no layout, e também re-define o bloco `title`, que sobrescreverá `{block title}` no layout. Tente imaginar o resultado. - - -Procurando templates --------------------- - -Você não precisa especificar nos presenters qual template deve ser renderizado, o framework deduzirá o caminho por si só e economizará sua digitação. - -Se você usa uma estrutura de diretórios onde cada presenter tem seu próprio diretório, simplesmente coloque o template neste diretório com o nome da ação (ou view), ou seja, para a ação `default`, use o template `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Se você usa uma estrutura onde os presenters estão juntos em um diretório e os templates na pasta `templates`, salve-o no arquivo `..latte` ou `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1ª variante - └── Home/ - └── default.latte ← 2ª variante -\-- - -O diretório `templates` também pode estar um nível acima, ou seja, no mesmo nível do diretório com as classes dos presenters. - -Se o template não for encontrado, o presenter responderá com um [erro 404 - página não encontrada |presenters#Erro 404 e cia]. - -A view é alterada usando `$this->setView('outraView')`. Também é possível especificar diretamente o arquivo de template usando `$this->template->setFile('/caminho/para/template.latte')`. - -.[note] -Os arquivos onde os templates são procurados podem ser alterados sobrescrevendo o método [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], que retorna um array de possíveis nomes de arquivos. - - -Procurando o template de layout -------------------------------- - -O Nette também procura automaticamente o arquivo de layout. - -Se você usa uma estrutura de diretórios onde cada presenter tem seu próprio diretório, coloque o layout ou na pasta com o presenter, se for específico apenas para ele, ou um nível acima, se for comum a vários presenters: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← layout comum - └── Home/ - ├── @layout.latte ← apenas para o presenter Home - ├── HomePresenter.php - └── default.latte -\-- - -Se você usa uma estrutura onde os presenters estão juntos em um diretório e os templates na pasta `templates`, o layout será esperado nestes locais: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← layout comum - ├── Home.@layout.latte ← apenas para Home, 1ª variante - └── Home/ - └── @layout.latte ← apenas para Home, 2ª variante -\-- - -Se o presenter estiver em um módulo, a busca também ocorrerá em níveis de diretório superiores, de acordo com o aninhamento do módulo. - -O nome do layout pode ser alterado usando `$this->setLayout('layoutAdmin')` e então será esperado no arquivo `@layoutAdmin.latte`. Também é possível especificar diretamente o arquivo de template de layout usando `$this->setLayout('/caminho/para/template.latte')`. - -Usando `$this->setLayout(false)` ou a tag `{layout none}` dentro do template, a busca por layout é desativada. - -.[note] -Os arquivos onde os templates de layout são procurados podem ser alterados sobrescrevendo o método [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], que retorna um array de possíveis nomes de arquivos. - - -Variáveis no template ---------------------- - -Passamos variáveis para o template escrevendo-as em `$this->template` e depois as temos disponíveis no template como variáveis locais: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Desta forma simples, podemos passar quaisquer variáveis para os templates. No entanto, no desenvolvimento de aplicações robustas, geralmente é mais útil limitar-se. Por exemplo, definindo explicitamente a lista de variáveis que o template espera e seus tipos. Graças a isso, o PHP poderá verificar os tipos, o IDE sugerirá corretamente e a análise estática revelará erros. - -E como definimos tal lista? Simplesmente na forma de uma classe e suas propriedades. Nomeamo-la de forma semelhante ao presenter, apenas com `Template` no final: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // e outras variáveis -} -``` - -O objeto `$this->template` no presenter será agora uma instância da classe `ArticleTemplate`. Assim, o PHP verificará os tipos declarados ao escrever. E a partir da versão PHP 8.2, também alertará sobre a escrita em uma variável inexistente; em versões anteriores, o mesmo pode ser alcançado usando a trait [Nette\SmartObject |utils:smartobject]. - -A anotação `@property-read` destina-se ao IDE e à análise estática, graças a ela o autocompletar funcionará, veja [PhpStorm and code completion for $this⁠-⁠>⁠template|https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template]. - -[* phpstorm-completion.webp *] - -Você pode desfrutar do luxo do autocompletar também nos templates, basta instalar o plugin para Latte no PhpStorm e indicar o nome da classe no início do template, mais no artigo [Latte: como usar o sistema de tipos|https://blog.nette.org/pt/latte-how-to-use-type-system]: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -É assim que os templates em componentes também funcionam, basta seguir a convenção de nomenclatura e para um componente, por exemplo, `FifteenControl`, criar uma classe de template `FifteenTemplate`. - -Se precisar criar `$template` como uma instância de outra classe, use o método `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Variáveis padrão ----------------- - -Presenters e componentes passam automaticamente várias variáveis úteis para os templates: - -- `$basePath` é o caminho URL absoluto para o diretório raiz (por exemplo, `/loja`) -- `$baseUrl` é a URL absoluta para o diretório raiz (por exemplo, `http://localhost/loja`) -- `$user` é o objeto [representando o usuário |security:authentication] -- `$presenter` é o presenter atual -- `$control` é o componente ou presenter atual -- `$flashes` array de [mensagens |presenters#Mensagens Flash] enviadas pela função `flashMessage()` - -Se você usar sua própria classe de template, essas variáveis serão passadas se você criar uma propriedade para elas. - - -Criação de links ----------------- - -No template, links para outros presenters & ações são criados desta forma: - -```latte -detalhe do produto -``` - -O atributo `n:href` é muito útil para tags HTML ``. Se quisermos exibir o link em outro lugar, por exemplo, no texto, usamos `{link}`: - -```latte -O endereço é: {link Home:default} -``` - -Mais informações podem ser encontradas no capítulo [Criando Links URL|creating-links]. - - -Filtros personalizados, tags, etc. ----------------------------------- - -O sistema de templates Latte pode ser estendido com filtros, funções, tags, etc. personalizados. Isso pode ser feito diretamente no método `render` ou `beforeRender()`: - -```php -public function beforeRender(): void -{ - // adicionando um filtro - $this->template->addFilter('foo', /* ... */); - - // ou configuramos diretamente o objeto Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -O Latte na versão 3 oferece uma maneira mais avançada, que é criar uma [extensão |latte:extending-latte#Latte Extension] para cada projeto web. Um exemplo fragmentado de tal classe: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Nós a registramos usando a [configuração |configuration#Templates Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Tradução --------- - -Se você está programando uma aplicação multilíngue, provavelmente precisará exibir alguns textos no template em diferentes idiomas. O Nette Framework define para este propósito uma interface para tradução [api:Nette\Localization\Translator], que tem um único método `translate()`. Ele recebe a mensagem `$message`, que geralmente é uma string, e quaisquer outros parâmetros. A tarefa é retornar a string traduzida. No Nette, não há implementação padrão, você pode escolher de acordo com suas necessidades entre várias soluções prontas que podem ser encontradas na [Componette |https://componette.org/search/localization]. Em sua documentação, você aprenderá como configurar o tradutor. - -É possível definir um tradutor para os templates, que [solicitamos |dependency-injection:passing-dependencies], usando o método `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -O tradutor também pode ser definido alternativamente através da [configuração |configuration#Templates Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Depois, o tradutor pode ser usado, por exemplo, como um filtro `|translate`, incluindo parâmetros adicionais que são passados para o método `translate()` (veja `foo, bar`): - -```latte -{='Carrinho'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Ou como uma tag de sublinhado: - -```latte -{_'Carrinho'} -{_$item} -{_$item, foo, bar} -``` - -Para traduzir uma seção do template, existe uma tag de par `{translate}` (a partir do Latte 2.11, anteriormente usava-se a tag `{_}`): - -```latte -{translate}Pedido{/translate} -{translate foo, bar}Pedido{/translate} -``` - -O tradutor é chamado por padrão em tempo de execução durante a renderização do template. O Latte versão 3, no entanto, pode traduzir todos os textos estáticos já durante a compilação do template. Isso economiza desempenho, pois cada string é traduzida apenas uma vez e a tradução resultante é escrita na forma compilada. No diretório de cache, são criadas várias versões compiladas do template, uma para cada idioma. Para isso, basta apenas especificar o idioma como segundo parâmetro: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Texto estático significa, por exemplo, `{_'olá'}` ou `{translate}olá{/translate}`. Textos não estáticos, como `{_$foo}`, continuarão a ser traduzidos em tempo de execução. diff --git a/application/ro/@home.texy b/application/ro/@home.texy deleted file mode 100644 index aad3662fe8..0000000000 --- a/application/ro/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application este nucleul framework-ului Nette, care oferă instrumente puternice pentru crearea de aplicații web moderne. Oferă o serie de caracteristici excepționale care facilitează semnificativ dezvoltarea și îmbunătățesc securitatea și mentenabilitatea codului. - - -Instalare ---------- - -Descărcați și instalați biblioteca folosind [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -De ce să alegeți Nette Application? ------------------------------------ - -Nette a fost întotdeauna un pionier în domeniul tehnologiilor web. - -**Router bidirecțional:** Nette dispune de un sistem avansat de rutare, unic prin bidirecționalitatea sa - nu numai că traduce URL-urile în acțiuni ale aplicației, dar poate și genera invers adrese URL. Acest lucru înseamnă că: -- Puteți schimba oricând structura URL a întregii aplicații fără a fi nevoie să modificați șabloanele -- URL-urile sunt canonizate automat, ceea ce îmbunătățește SEO -- Rutarea este definită într-un singur loc, nu dispersată în adnotări - -**Componente și semnale:** Sistemul de componente încorporat, inspirat de Delphi și React.js, este complet excepțional printre framework-urile PHP: -- Permite crearea de elemente UI reutilizabile -- Suportă compunerea ierarhică a componentelor -- Oferă o procesare elegantă a cererilor AJAX folosind semnale -- Bibliotecă bogată de componente gata făcute pe [Componette](https://componette.org) - -**AJAX și snippete:** Nette a introdus un mod revoluționar de lucru cu AJAX încă din 2009, cu mult înainte de soluții similare precum Hotwire pentru Ruby on Rails sau Symfony UX Turbo: -- Snippetele permit actualizarea doar a unor părți ale paginii fără a fi nevoie să scrieți JavaScript -- Integrare automată cu sistemul de componente -- Invalidare inteligentă a părților paginii -- Cantitate minimă de date transferate - -**Șabloane intuitive [Latte|latte:]:** Cel mai sigur sistem de șabloane pentru PHP cu funcții avansate: -- Protecție automată împotriva XSS cu escapare sensibilă la context -- Extensibilitate prin filtre, funcții și tag-uri personalizate -- Moștenirea șabloanelor și snippete pentru AJAX -- Suport excelent pentru PHP 8.x cu sistem de tipuri - -**Dependency Injection:** Nette utilizează pe deplin Dependency Injection: -- Transmiterea automată a dependențelor (autowiring) -- Configurare folosind formatul clar NEON -- Suport pentru fabrici de componente - - -Principalele avantaje ---------------------- - -- **Securitate**: Protecție automată împotriva [vulnerabilităților|nette:vulnerability-protection] precum XSS, CSRF, etc. -- **Productivitate**: Mai puțin cod, mai multe funcții datorită designului inteligent -- **Depanare**: [Tracy debugger|tracy:] cu panou de rutare -- **Performanță**: Cache inteligent, încărcare leneșă a componentelor -- **Flexibilitate**: Modificare ușoară a URL-urilor chiar și după finalizarea aplicației -- **Componente**: Sistem unic de elemente UI reutilizabile -- **Modern**: Suport complet pentru PHP 8.4+ și sistem de tipuri - - -Primii pași ------------ - -1. [Cum funcționează aplicațiile? |how-it-works] - Înțelegerea arhitecturii de bază -2. [Presenters |presenters] - Lucrul cu presenteri și acțiuni -3. [Șabloane |templates] - Crearea șabloanelor în Latte -4. [Rutare |routing] - Configurarea adreselor URL -5. [Componente interactive |components] - Utilizarea sistemului de componente - - -Compatibilitate cu PHP ----------------------- - -| versiune | compatibil cu 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 - -Se aplică pentru ultima versiune patch. diff --git a/application/ro/@left-menu.texy b/application/ro/@left-menu.texy deleted file mode 100644 index c281e45b41..0000000000 --- a/application/ro/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Cum funcționează aplicațiile? |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Șabloane |templates] -- [Structura directoarelor |directory-structure] -- [Rutare |routing] -- [Crearea linkurilor URL |creating-links] -- [Componente interactive |components] -- [AJAX & snippete |ajax] -- [Multiplier |multiplier] -- [Configurație |configuration] - - -Lectură suplimentară -******************** -- [De ce să folosiți Nette? |www:10-reasons-why-nette] -- [Instalare |nette:installation] -- [Scriem prima aplicație! |quickstart:] -- [Tutoriale și proceduri |best-practices:] -- [Rezolvarea problemelor |nette:troubleshooting] diff --git a/application/ro/@meta.texy b/application/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/application/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/application/ro/ajax.texy b/application/ro/ajax.texy deleted file mode 100644 index 682d4cbf0d..0000000000 --- a/application/ro/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & snippety -*************** - -
    - -În era aplicațiilor web moderne, unde funcționalitatea este adesea împărțită între server și browser, AJAX este un element de legătură esențial. Ce opțiuni ne oferă Nette Framework în acest domeniu? -- trimiterea unor părți din șablon, așa-numitele snippets -- transmiterea variabilelor între PHP și JavaScript -- instrumente pentru depanarea cererilor AJAX - -
    - - -Cererea AJAX -============ - -O cerere AJAX nu diferă, în esență, de o cerere HTTP clasică. Se apelează un presenter cu anumiți parametri. Și depinde de presenter cum va reacționa la cerere - poate returna date în format JSON, poate trimite o parte din codul HTML, un document XML etc. - -Pe partea de browser, inițializăm cererea AJAX folosind funcția `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // procesarea răspunsului -}); -``` - -Pe partea de server, recunoaștem o cerere AJAX prin metoda `$httpRequest->isAjax()` a serviciului [încapsulând cererea HTTP |http:request]. Pentru detectare, utilizează antetul HTTP `X-Requested-With`, de aceea este important să îl trimitem. În cadrul presenterului, se poate utiliza metoda `$this->isAjax()`. - -Dacă doriți să trimiteți date în format JSON, utilizați metoda [`sendJson()` |presenters#Trimiterea răspunsului]. Metoda încheie, de asemenea, activitatea presenterului. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Dacă intenționați să răspundeți folosind un șablon special destinat AJAX, puteți face acest lucru după cum urmează: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Snippets -======== - -Cel mai puternic instrument pe care Nette îl oferă pentru conectarea serverului cu clientul sunt snippets. Datorită lor, puteți transforma o aplicație obișnuită într-una AJAX cu un efort minim și câteva linii de cod. Exemplul Fifteen, al cărui cod îl găsiți pe [GitHub |https://github.com/nette-examples/fifteen], demonstrează cum funcționează totul. - -Snippets, sau fragmente, permit actualizarea doar a unor părți ale paginii, în loc de a reîncărca întreaga pagină. Acest lucru este nu numai mai rapid și mai eficient, dar oferă și o experiență de utilizare mai confortabilă. Snippets vă pot aminti de Hotwire pentru Ruby on Rails sau Symfony UX Turbo. Interesant este că Nette a introdus snippets cu 14 ani mai devreme. - -Cum funcționează snippets? La prima încărcare a paginii (cerere non-AJAX), se încarcă întreaga pagină, inclusiv toate snippets. Când utilizatorul interacționează cu pagina (de exemplu, face clic pe un buton, trimite un formular etc.), în loc de a încărca întreaga pagină, se declanșează o cerere AJAX. Codul din presenter execută acțiunea și decide ce snippets trebuie actualizate. Nette redă aceste snippets și le trimite sub forma unui array în format JSON. Codul de gestionare din browser inserează snippets primite înapoi în pagină. Astfel, se transferă doar codul snippets modificate, ceea ce economisește lățimea de bandă și accelerează încărcarea în comparație cu transferul conținutului întregii pagini. - - -Naja ----- - -Pentru gestionarea snippets pe partea de browser, se utilizează [biblioteca Naja |https://naja.js.org]. Aceasta se [instalează |https://naja.js.org/#/guide/01-install-setup-naja] ca pachet node.js (pentru utilizare cu aplicații Webpack, Rollup, Vite, Parcel și altele): - -```shell -npm install naja -``` - -…sau se inserează direct în șablonul paginii: - -```latte - -``` - -Mai întâi, este necesar să [inițializați |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization] biblioteca: - -```js -naja.initialize(); -``` - -Pentru a transforma un link obișnuit (semnal) sau trimiterea unui formular într-o cerere AJAX, este suficient să marcați linkul, formularul sau butonul respectiv cu clasa `ajax`: - -```latte -Go - -
    - -
    - -sau - -
    - -
    -``` - - -Redesenarea snippetelor ------------------------ - -Fiecare obiect al clasei [Control |components] (inclusiv Presenterul însuși) înregistrează dacă au avut loc modificări care necesită redesenarea sa. Pentru aceasta se utilizează metoda `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // după logare este necesar să redesenăm partea relevantă - $this->redrawControl(); - // ... -} -``` - -Nette permite un control și mai fin asupra a ceea ce trebuie redesenat. Metoda menționată poate primi ca argument numele snippetului. Astfel, se poate invalida (adică forța redesenarea) la nivelul părților șablonului. Dacă se invalidează întreaga componentă, se va redesena și fiecare snippet al acesteia: - -```php -// invalidează snippetul 'header' -$this->redrawControl('header'); -``` - - -Snippets în Latte ------------------ - -Utilizarea snippetelor în Latte este extrem de ușoară. Dacă doriți să definiți o parte a șablonului ca snippet, încadrați-o pur și simplu între tag-urile `{snippet}` și `{/snippet}`: - -```latte -{snippet header} -

    Hello ...

    -{/snippet} -``` - -Snippetul creează în pagina HTML un element `
    ` cu un `id` special generat. La redesenarea snippetului, se actualizează conținutul acestui element. De aceea, este necesar ca la redarea inițială a paginii să se redea și toate snippet-urile, chiar dacă acestea pot fi inițial goale. - -Puteți crea și un snippet cu un alt element decât `
    ` folosind n:atributul: - -```latte -
    -

    Hello ...

    -
    -``` - - -Zone de snippets ----------------- - -Numele snippetelor pot fi și expresii: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Astfel, vom crea mai multe snippets `item-0`, `item-1` etc. Dacă am invalida direct un snippet dinamic (de exemplu, `item-1`), nu s-ar redesena nimic. Motivul este că snippet-urile funcționează într-adevăr ca fragmente și se redau doar ele însele direct. Însă, în șablon, nu există de fapt niciun snippet numit `item-1`. Acesta apare doar prin executarea codului din jurul snippetului, adică ciclul foreach. Prin urmare, marcăm porțiunea de șablon care trebuie executată folosind tag-ul `{snippetArea}`: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -Și lăsăm să se redeseneze atât snippetul însuși, cât și întreaga zonă părinte: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -În același timp, este recomandabil să ne asigurăm că array-ul `$items` conține doar acele elemente care trebuie redesenate. - -Dacă includem în șablon, folosind tag-ul `{include}`, un alt șablon care conține snippets, este necesar să includem din nou includerea șablonului în `snippetArea` și să o invalidăm împreună cu snippetul: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Snippets în componente ----------------------- - -Puteți crea snippets și în [componente|components], iar Nette le va redesena automat. Dar există o anumită limitare: pentru redesenarea snippetelor, se apelează metoda `render()` fără parametri. Prin urmare, nu va funcționa transmiterea parametrilor în șablon: - -```latte -OK -{control productGrid} - -nu va funcționa: -{control productGrid $arg, $arg} -{control productGrid:paginator} -``` - - -Trimiterea datelor utilizatorului ---------------------------------- - -Împreună cu snippets, puteți trimite clientului orice alte date. Este suficient să le scrieți în obiectul `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Success'; - } -} -``` - - -Transmiterea parametrilor -========================= - -Dacă trimitem parametri unei componente printr-o cerere AJAX, fie că sunt parametri de semnal sau parametri persistenți, trebuie să specificăm în cerere numele lor global, care include și numele componentei. Numele complet al parametrului este returnat de metoda `getParameterId()`. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -Și metoda handle cu parametrii corespunzători în componentă: - -```php -public function handleFoo(int $bar): void -{ -} -``` diff --git a/application/ro/bootstrapping.texy b/application/ro/bootstrapping.texy deleted file mode 100644 index 929a260190..0000000000 --- a/application/ro/bootstrapping.texy +++ /dev/null @@ -1,297 +0,0 @@ -Bootstrapping -************* - -
    - -Bootstrapping-ul este procesul de inițializare a mediului aplicației, crearea unui container de dependency injection (DI) și pornirea aplicației. Vom discuta: - -- cum clasa Bootstrap inițializează mediul -- cum sunt configurate aplicațiile folosind fișiere NEON -- cum să distingem între modul de producție și dezvoltare -- cum să creăm și să configurăm containerul DI - -
    - - -Aplicațiile, fie că sunt web sau scripturi rulate din linia de comandă, își încep execuția cu o formă de inițializare a mediului. În trecut, acest lucru era de obicei gestionat de un fișier numit, de exemplu, `include.inc.php`, pe care fișierul inițial îl includea. În aplicațiile Nette moderne, acesta a fost înlocuit de clasa `Bootstrap`, pe care o veți găsi ca parte a aplicației în fișierul `app/Bootstrap.php`. Poate arăta, de exemplu, astfel: - -```php -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Configuratorul este responsabil pentru setarea mediului aplicației și a serviciilor. - $this->configurator = new Configurator; - // Setează directorul pentru fișierele temporare generate de Nette (de ex., șabloane compilate) - $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 este inteligent și modul de dezvoltare se activează automat, - // sau îl puteți activa pentru o anumită adresă IP decomentând următoarea linie: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Activează Tracy: "briceagul elvețian" suprem pentru depanare. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: încarcă automat toate clasele din directorul selectat - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Încarcă fișierele de configurare - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -Fișierul inițial în cazul aplicațiilor web este `index.php`, care se află în [directorul public |directory-structure#Director public www] `www/`. Acesta solicită clasei Bootstrap să inițializeze mediul și să creeze containerul DI. Apoi, obține din acesta serviciul `Application`, care pornește aplicația web: - -```php -$bootstrap = new App\Bootstrap; -// Inițializarea mediului + crearea containerului DI -$container = $bootstrap->bootWebApplication(); -// Containerul DI creează obiectul Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Pornirea aplicației Nette și procesarea cererii primite -$application->run(); -``` - -După cum se poate vedea, clasa [api:Nette\Bootstrap\Configurator] ajută la setarea mediului și la crearea containerului de dependency injection (DI), pe care o vom prezenta acum mai detaliat. - - -Modul de dezvoltare vs producție -================================ - -Nette se comportă diferit în funcție de dacă rulează pe un server de dezvoltare sau de producție: - -🛠️ Modul de dezvoltare (Development): - - Afișează bara de depanare Tracy cu informații utile (interogări SQL, timp de execuție, memorie utilizată) - - În caz de eroare, afișează o pagină de eroare detaliată cu apelurile de funcții și conținutul variabilelor - - Reînnoiește automat cache-ul la modificarea șabloanelor Latte, editarea fișierelor de configurare etc. - - -🚀 Modul de producție (Production): - - Nu afișează nicio informație de depanare, toate erorile sunt scrise în log - - În caz de eroare, afișează ErrorPresenter sau pagina generică "Server Error" - - Cache-ul nu se reînnoiește niciodată automat! - - Optimizat pentru viteză și securitate - - -Alegerea modului se face prin autodetecție, deci de obicei nu este necesar să configurați sau să comutați manual: - -- modul de dezvoltare: pe localhost (adresa IP `127.0.0.1` sau `::1`) dacă nu este prezent un proxy (adică antetul său HTTP) -- modul de producție: oriunde altundeva - -Dacă dorim să activăm modul de dezvoltare și în alte cazuri, de exemplu pentru programatorii care accesează de la o anumită adresă IP, folosim `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // se poate specifica și un array de adrese IP -``` - -Recomandăm cu tărie combinarea adresei IP cu un cookie. În cookie-ul `nette-debug` salvăm un token secret, de ex. `secret1234`, și astfel activăm modul de dezvoltare pentru programatorii care accesează de la o anumită adresă IP și au în același timp tokenul menționat în cookie: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Putem, de asemenea, să dezactivăm complet modul de dezvoltare, chiar și pentru localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Atenție, valoarea `true` activează modul de dezvoltare forțat, ceea ce nu trebuie să se întâmple niciodată pe un server de producție. - - -Instrumentul de depanare Tracy -============================== - -Pentru o depanare ușoară, vom activa și excelentul instrument [Tracy |tracy:]. În modul de dezvoltare, vizualizează erorile, iar în modul de producție, le înregistrează în directorul specificat: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Fișiere temporare -================= - -Nette utilizează cache pentru containerul DI, RobotLoader, șabloane etc. Prin urmare, este necesar să setați calea către directorul unde se va stoca cache-ul: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Pe Linux sau macOS, setați [permisiuni de scriere |nette:troubleshooting#Setarea permisiunilor pentru directoare] pentru directoarele `log/` și `temp/`. - - -RobotLoader -=========== - -De regulă, vom dori să încărcăm automat clasele folosind [RobotLoader |robot-loader:], deci trebuie să îl pornim și să îl lăsăm să încarce clasele din directorul unde este plasat `Bootstrap.php` (adică `__DIR__`), și din toate subdirectoarele: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -O abordare alternativă este să lăsăm clasele să fie încărcate doar prin [Composer |best-practices:composer] respectând PSR-4. - - -Timezone -======== - -Prin intermediul configuratorului puteți seta fusul orar implicit. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Configurarea containerului DI -============================= - -Parte a procesului de inițializare este crearea containerului DI sau a fabricii de obiecte, care este inima întregii aplicații. Este de fapt o clasă PHP, generată de Nette și salvată în directorul de cache. Fabrica produce obiectele cheie ale aplicației și, folosind fișierele de configurare, o instruim cum să le creeze și să le seteze, influențând astfel comportamentul întregii aplicații. - -Fișierele de configurare sunt de obicei scrise în formatul [NEON |neon:format]. Într-un capitol separat veți afla [ce poate fi configurat |nette:configuring]. - -.[tip] -În modul de dezvoltare, containerul se actualizează automat la fiecare modificare a codului sau a fișierelor de configurare. În modul de producție, se generează o singură dată și modificările nu sunt verificate pentru a maximiza performanța. - -Fișierele de configurare le încărcăm folosind `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Dacă dorim să adăugăm mai multe fișiere de configurare, putem apela funcția `addConfig()` de mai multe ori. - -```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'); -} -``` - -Numele `cli.php` nu este o greșeală de tipar, configurația poate fi scrisă și într-un fișier PHP, care o returnează ca array. - -De asemenea, putem adăuga alte fișiere de configurare în [secțiunea `includes` |dependency-injection:configuration#Includerea fișierelor]. - -Dacă în fișierele de configurare apar elemente cu aceleași chei, acestea vor fi suprascrise sau, în cazul [array-urilor, combinate |dependency-injection:configuration#Combinare]. Fișierul inclus ulterior are prioritate mai mare decât cel anterior. Fișierul în care este specificată secțiunea `includes` are prioritate mai mare decât fișierele incluse în el. - - -Parametri statici ------------------ - -Parametrii utilizați în fișierele de configurare pot fi definiți [în secțiunea `parameters` |dependency-injection:configuration#Parametri] și, de asemenea, pot fi transmiși (sau suprascriși) prin metoda `addStaticParameters()` (are aliasul `addParameters()`). Este important că valorile diferite ale parametrilor determină generarea altor containere DI, adică a altor clase. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -La parametrul `projectId` se poate face referire în configurație prin notația obișnuită `%projectId%`. - - -Parametri dinamici ------------------- - -În container putem adăuga și parametri dinamici, ale căror valori diferite, spre deosebire de parametrii statici, nu determină generarea de noi containere DI. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Astfel, putem adăuga simplu, de exemplu, variabile de mediu, la care se poate face referire ulterior în configurație prin notația `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Parametri impliciți -------------------- - -În fișierele de configurare puteți utiliza acești parametri statici: - -- `%appDir%` este calea absolută către directorul cu fișierul `Bootstrap.php` -- `%wwwDir%` este calea absolută către directorul cu fișierul de intrare `index.php` -- `%tempDir%` este calea absolută către directorul pentru fișiere temporare -- `%vendorDir%` este calea absolută către directorul unde Composer instalează bibliotecile -- `%rootDir%` este calea absolută către directorul rădăcină al proiectului -- `%debugMode%` indică dacă aplicația este în modul de depanare -- `%consoleMode%` indică dacă cererea a venit prin linia de comandă - - -Servicii importate ------------------- - -Acum intrăm mai în profunzime. Deși scopul containerului DI este să producă obiecte, în mod excepțional poate apărea nevoia de a introduce un obiect existent în container. Facem acest lucru definind serviciul cu flag-ul `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -Și în bootstrap introducem obiectul în container: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Medii diferite -============== - -Nu vă fie teamă să modificați clasa Bootstrap conform nevoilor dvs. Puteți adăuga parametri metodei `bootWebApplication()` pentru a distinge proiectele web. Sau putem completa cu alte metode, de exemplu `bootTestEnvironment()`, care inițializează mediul pentru testele unitare, `bootConsoleApplication()` pentru scripturile apelate din linia de comandă etc. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // inițializarea Nette Tester - $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/ro/components.texy b/application/ro/components.texy deleted file mode 100644 index e4959ac9a9..0000000000 --- a/application/ro/components.texy +++ /dev/null @@ -1,485 +0,0 @@ -Componente interactive -********************** - -
    - -Componentele sunt obiecte separate, reutilizabile, pe care le inserăm în pagini. Acestea pot fi formulare, datagrid-uri, sondaje, de fapt, orice are sens să fie folosit în mod repetat. Vom arăta: - -- cum se utilizează componentele? -- cum se scriu? -- ce sunt semnalele? - -
    - -Nette are încorporat un sistem de componente. Ceva similar ar putea fi cunoscut de veterani din Delphi sau ASP.NET Web Forms, ceva asemănător stă la baza React sau Vue.js. Cu toate acestea, în lumea framework-urilor PHP, este o caracteristică unică. - -Totuși, componentele influențează în mod fundamental abordarea creării aplicațiilor. Puteți compune paginile din unități pre-pregătite. Aveți nevoie de un datagrid în administrare? Îl găsiți pe [Componette |https://componette.org/search/component], un depozit de add-on-uri open-source (adică nu doar componente) pentru Nette și îl inserați pur și simplu în presenter. - -Puteți încorpora orice număr de componente într-un presenter. Și în unele componente puteți insera alte componente. Astfel se creează un arbore de componente, a cărui rădăcină este presenterul. - - -Metode factory -============== - -Cum se inserează componentele în presenter și cum se utilizează ulterior? De obicei, prin metode factory. - -O fabrică de componente reprezintă o modalitate elegantă de a crea componente doar în momentul în care sunt cu adevărat necesare (lazy / on demand). Întreaga magie constă în implementarea unei metode cu numele `createComponent()`, unde `` este numele componentei create, și care creează și returnează componenta. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->item; - return $poll; - } -} -``` - -Datorită faptului că toate componentele sunt create în metode separate, codul devine mai clar. - -.[note] -Numele componentelor încep întotdeauna cu literă mică, chiar dacă în numele metodei se scriu cu literă mare. - -Fabricile nu le apelăm niciodată direct, ele se apelează singure în momentul în care folosim componenta pentru prima dată. Datorită acestui fapt, componenta este creată la momentul potrivit și doar în cazul în care este cu adevărat necesară. Dacă nu folosim componenta (de exemplu, într-o cerere AJAX, când se transferă doar o parte a paginii, sau la cache-uirea șablonului), aceasta nu se creează deloc și economisim performanța serverului. - -```php .{file:DefaultPresenter.php} -// accesăm componenta și dacă a fost prima dată, -// se apelează createComponentPoll() care o creează -$poll = $this->getComponent('poll'); -// sintaxă alternativă: $poll = $this['poll']; -``` - -În șablon, este posibil să redăm componenta folosind tag-ul [{control} |#Redare]. Prin urmare, nu este necesar să transmitem manual componentele către șablon. - -```latte -

    Votați

    - -{control poll} -``` - - -Stilul Hollywood -================ - -Componentele folosesc în mod obișnuit o tehnică proaspătă, pe care ne place să o numim stilul Hollywood. Cu siguranță cunoașteți celebra frază pe care participanții la castingurile de film o aud atât de des: „Nu ne sunați, vă vom suna noi”. Și exact despre asta este vorba. - -În Nette, în loc să trebuiască să întrebați constant („a fost trimis formularul?”, „a fost valid?” sau „a apăsat utilizatorul acest buton?”), spuneți framework-ului „când se întâmplă asta, apelează această metodă” și lăsați restul muncii pe seama lui. Dacă programați în JavaScript, acest stil de programare vă este familiar. Scrieți funcții care sunt apelate atunci când apare un anumit eveniment. Și limbajul le transmite parametrii corespunzători. - -Acest lucru schimbă complet perspectiva asupra scrierii aplicațiilor. Cu cât puteți lăsa mai multe sarcini pe seama framework-ului, cu atât aveți mai puțină muncă. Și cu atât mai puțin puteți omite. - - -Scriem o componentă -=================== - -Prin termenul componentă înțelegem de obicei un descendent al clasei [api:Nette\Application\UI\Control]. (Mai precis ar fi, așadar, să folosim termenul „controls”, dar „controale” are un sens complet diferit în română și s-a impus mai degrabă „componente”.) Presenterul însuși [api:Nette\Application\UI\Presenter] este, de altfel, tot un descendent al clasei `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Redare -====== - -Știm deja că pentru redarea unei componente se folosește tag-ul `{control componentName}`. Acesta apelează de fapt metoda `render()` a componentei, în care ne ocupăm de redare. Avem la dispoziție, la fel ca în presenter, [șablonul Latte|templates] în variabila `$this->template`, căreia îi transmitem parametri. Spre deosebire de presenter, trebuie să specificăm fișierul cu șablonul și să îl lăsăm să fie redat: - -```php .{file:PollControl.php} -public function render(): void -{ - // inserăm în șablon câțiva parametri - $this->template->param = $value; - // și o redăm - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -Tag-ul `{control}` permite transmiterea parametrilor către metoda `render()`: - -```latte -{control poll $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Uneori, o componentă poate consta din mai multe părți pe care dorim să le redăm separat. Pentru fiecare dintre ele, creăm propria metodă de redare, aici în exemplu, de exemplu, `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -Și în șablon o apelăm apoi folosind: - -```latte -{control poll:paginator} -``` - -Pentru o mai bună înțelegere, este bine să știm cum se traduce acest tag în PHP. - -```latte -{control poll} -{control poll:paginator 123, 'hello'} -``` - -se traduce ca: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -Metoda `getComponent()` returnează componenta `poll` și pe această componentă apelează metoda `render()`, respectiv `renderPaginator()` dacă este specificat un alt mod de redare în tag după două puncte. - -.[caution] -Atenție, dacă oriunde în parametri apare **`=>`**, toți parametrii vor fi împachetați într-un array și transmiși ca prim argument: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -se traduce ca: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Redarea sub-componentei: - -```latte -{control cartControl-someForm} -``` - -se traduce ca: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Componentele, la fel ca presenterele, transmit automat către șabloane câteva variabile utile: - -- `$basePath` este calea URL absolută către directorul rădăcină (de ex. `/eshop`) -- `$baseUrl` este URL-ul absolut către directorul rădăcină (de ex. `http://localhost/eshop`) -- `$user` este obiectul [reprezentând utilizatorul |security:authentication] -- `$presenter` este presenterul curent -- `$control` este componenta curentă -- `$flashes` array de [mesaje |#Mesaje flash] trimise de funcția `flashMessage()` - - -Semnal -====== - -Știm deja că navigarea într-o aplicație Nette constă în legarea sau redirecționarea către perechi `Presenter:action`. Dar ce se întâmplă dacă vrem doar să executăm o acțiune pe **pagina curentă**? De exemplu, să schimbăm ordonarea coloanelor într-un tabel; să ștergem un element; să comutăm între modul luminos/întunecat; să trimitem un formular; să votăm într-un sondaj; etc. - -Acest tip de cereri se numește semnale. Și la fel cum acțiunile apelează metodele `action()` sau `render()`, semnalele apelează metodele `handle()`. În timp ce conceptul de acțiune (sau view) este legat strict doar de presentere, semnalele se referă la toate componentele. Și, prin urmare, și la presentere, deoarece `UI\Presenter` este un descendent al `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... procesarea semnalului ... -} -``` - -Un link care apelează un semnal se creează în mod obișnuit, adică în șablon prin atributul `n:href` sau tag-ul `{link}`, în cod prin metoda `link()`. Mai multe în capitolul [Crearea linkurilor URL |creating-links#Linkuri către semnal]. - -```latte -click here -``` - -Semnalul se apelează întotdeauna pe presenterul și acțiunea curentă, nu este posibil să-l apelezi pe alt presenter sau altă acțiune. - -Semnalul provoacă, așadar, reîncărcarea paginii la fel ca la cererea inițială, doar că în plus apelează metoda de gestionare a semnalului cu parametrii corespunzători. Dacă metoda nu există, se aruncă o excepție [api:Nette\Application\UI\BadSignalException], care este afișată utilizatorului ca o pagină de eroare 403 Forbidden. - - -Snippets și AJAX -================ - -Semnalele vă pot aminti puțin de AJAX: handlere care sunt apelate pe pagina curentă. Și aveți dreptate, semnalele sunt într-adevăr adesea apelate folosind AJAX și ulterior transmitem către browser doar părțile modificate ale paginii. Adică așa-numitele snippets. Mai multe informații găsiți pe [pagina dedicată AJAX |ajax]. - - -Mesaje flash -============ - -Componenta are propriul său spațiu de stocare pentru mesaje flash, independent de presenter. Acestea sunt mesaje care, de exemplu, informează despre rezultatul unei operațiuni. O caracteristică importantă a mesajelor flash este că sunt disponibile în șablon chiar și după redirecționare. Chiar și după afișare, rămân active încă 30 de secunde – de exemplu, în cazul în care utilizatorul ar reîncărca pagina din cauza unei erori de transmisie - mesajul nu dispare imediat. - -Trimiterea este gestionată de metoda [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Primul parametru este textul mesajului sau un obiect `stdClass` reprezentând mesajul. Al doilea parametru opțional este tipul său (error, warning, info etc.). Metoda `flashMessage()` returnează instanța mesajului flash ca obiect `stdClass`, căruia i se pot adăuga informații suplimentare. - -```php -$this->flashMessage('Elementul a fost șters.'); -$this->redirect(/* ... */); // și redirecționăm -``` - -În șablon, aceste mesaje sunt disponibile în variabila `$flashes` ca obiecte `stdClass`, care conțin proprietățile `message` (textul mesajului), `type` (tipul mesajului) și pot conține informațiile utilizatorului menționate anterior. Le redăm, de exemplu, astfel: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Redirecționare după semnal -========================== - -După procesarea semnalului componentei, urmează adesea o redirecționare. Este o situație similară cu formularele - după trimiterea lor, redirecționăm, de asemenea, pentru ca la reîncărcarea paginii în browser să nu se trimită din nou datele. - -```php -$this->redirect('this') // redirecționează către presenterul și acțiunea curentă -``` - -Deoarece componenta este un element reutilizabil și, de obicei, nu ar trebui să aibă o legătură directă cu presentere specifice, metodele `redirect()` și `link()` interpretează automat parametrul ca un semnal al componentei: - -```php -$this->redirect('click') // redirecționează către semnalul 'click' al aceleiași componente -``` - -Dacă aveți nevoie să redirecționați către un alt presenter sau acțiune, puteți face acest lucru prin intermediul presenterului: - -```php -$this->getPresenter()->redirect('Product:show'); // redirecționează către alt presenter/acțiune -``` - - -Parametri persistenți -===================== - -Parametrii persistenți sunt utilizați pentru a menține starea în componente între diferite cereri. Valoarea lor rămâne aceeași chiar și după ce se face clic pe un link. Spre deosebire de datele din sesiune, acestea sunt transmise în URL. Și acest lucru se întâmplă complet automat, inclusiv pentru linkurile create în alte componente de pe aceeași pagină. - -Aveți, de exemplu, o componentă pentru paginarea conținutului. Pot exista mai multe astfel de componente pe o pagină. Și dorim ca, după ce se face clic pe un link, toate componentele să rămână pe pagina lor curentă. De aceea, facem din numărul paginii (`page`) un parametru persistent. - -Crearea unui parametru persistent în Nette este extrem de simplă. Este suficient să creați o proprietate publică și să o marcați cu un atribut: (anterior se folosea `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // această linie este importantă - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // trebuie să fie publică -} -``` - -Pentru proprietate, recomandăm să specificați și tipul de date (de ex. `int`) și puteți specifica și o valoare implicită. Valorile parametrilor pot fi [validate |#Validarea parametrilor persistenți]. - -La crearea unui link, valoarea parametrului persistent poate fi modificată: - -```latte -next -``` - -Sau poate fi *resetat*, adică eliminat din URL. Atunci va lua valoarea sa implicită: - -```latte -reset -``` - - -Componente persistente -====================== - -Nu doar parametrii, ci și componentele pot fi persistente. La o astfel de componentă, parametrii săi persistenți sunt transmiși și între diferite acțiuni ale presenterului sau între mai mulți presenteri. Componentele persistente le marcăm cu o adnotare la clasa presenterului. De exemplu, astfel marcăm componentele `calendar` și `poll`: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Subcomponentele din interiorul acestor componente nu trebuie marcate, devin și ele persistente. - -În PHP 8, puteți utiliza și atribute pentru a marca componentele persistente: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Componente cu dependențe -======================== - -Cum să creăm componente cu dependențe fără a ne „murdări” presenterele care le vor utiliza? Datorită proprietăților inteligente ale containerului DI din Nette, la fel ca la utilizarea serviciilor clasice, putem lăsa majoritatea muncii pe seama framework-ului. - -Să luăm ca exemplu o componentă care are o dependență de serviciul `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // Id-ul sondajului pentru care creăm componenta - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Dacă am scrie un serviciu clasic, nu ar fi nimic de rezolvat. Containerul DI s-ar ocupa invizibil de transmiterea tuturor dependențelor. Însă, cu componentele, de obicei procedăm astfel încât creăm noua lor instanță direct în presenter în [metodele factory |#Metode factory] `createComponent…()`. Dar transmiterea tuturor dependențelor tuturor componentelor către presenter, pentru a le transmite apoi componentelor, este greoaie. Și cât cod scris… - -Întrebarea logică este, de ce nu înregistrăm pur și simplu componenta ca un serviciu clasic, nu o transmitem către presenter și apoi în metoda `createComponent…()` nu o returnăm? O astfel de abordare este însă nepotrivită, deoarece dorim să avem posibilitatea de a crea componenta chiar și de mai multe ori. - -Soluția corectă este să scriem pentru componentă o fabrică, adică o clasă care ne va crea componenta: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Astfel înregistrăm fabrica în containerul nostru în configurație: - -```neon -services: - - PollControlFactory -``` - -și în final o folosim în presenterul nostru: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // putem transmite parametrul nostru - return $this->pollControlFactory->create($pollId); - } -} -``` - -Minunat este că Nette DI poate [genera |dependency-injection:factory] astfel de fabrici simple, așa că în loc de întregul său cod, este suficient să scriem doar interfața sa: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -Și asta e tot. Nette implementează intern această interfață și o transmite către presenter, unde o putem deja utiliza. Magic, ne adaugă și parametrul `$id` și instanța clasei `PollFacade` în componenta noastră. - - -Componente în profunzime -======================== - -Componentele din Nette Application reprezintă părți reutilizabile ale aplicației web, pe care le inserăm în pagini și cărora, de altfel, le este dedicat întregul acest capitol. Ce abilități exacte are o astfel de componentă? - -1) este redabilă în șablon -2) știe [ce parte a sa |ajax#Snippets] trebuie să redea la o cerere AJAX (snippets) -3) are capacitatea de a-și salva starea în URL (parametri persistenți) -4) are capacitatea de a reacționa la acțiunile utilizatorului (semnale) -5) creează o structură ierarhică (unde rădăcina este presenterul) - -Fiecare dintre aceste funcții este gestionată de una dintre clasele liniei ereditare. Redarea (1 + 2) este responsabilitatea [api:Nette\Application\UI\Control], includerea în [ciclul de viață |presenters#Ciclul de viață al presenterului] (3, 4) a clasei [api:Nette\Application\UI\Component] și crearea structurii ierarhice (5) a claselor [Container și Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Ciclul de viață al componentei ------------------------------- - -[* lifecycle-component.svg *] *** *Ciclul de viață al componentei* .<> - - -Validarea parametrilor persistenți ----------------------------------- - -Valorile [parametrilor persistenți |#Parametri persistenți] primite din URL sunt scrise în proprietăți de către metoda `loadState()`. Aceasta verifică, de asemenea, dacă tipul de date specificat la proprietate corespunde, altfel răspunde cu eroarea 404 și pagina nu se afișează. - -Nu credeți niciodată orbește în parametrii persistenți, deoarece pot fi ușor suprascriși de utilizator în URL. Astfel, de exemplu, verificăm dacă numărul paginii `$this->page` este mai mare decât 0. O cale potrivită este să suprascriem metoda menționată `loadState()`: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // aici se setează $this->page - // urmează controlul propriu al valorii: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Procesul invers, adică colectarea valorilor din proprietățile persistente, este responsabilitatea metodei `saveState()`. - - -Semnale în profunzime ---------------------- - -Un semnal provoacă reîncărcarea paginii exact la fel ca la cererea inițială (cu excepția cazului în care este apelat prin AJAX) și apelează metoda `signalReceived($signal)`, a cărei implementare implicită în clasa `Nette\Application\UI\Component` încearcă să apeleze o metodă compusă din cuvintele `handle{signal}`. Procesarea ulterioară depinde de obiectul respectiv. Obiectele care moștenesc de la `Component` (adică `Control` și `Presenter`) reacționează încercând să apeleze metoda `handle{signal}` cu parametrii corespunzători. - -Cu alte cuvinte: se ia definiția funcției `handle{signal}` și toți parametrii care au venit cu cererea, iar argumentelor li se atribuie parametrii din URL după nume și se încearcă apelarea metodei respective. De exemplu, ca parametru `$id` se transmite valoarea din parametrul `id` din URL, ca `$something` se transmite `something` din URL, etc. Și dacă metoda nu există, metoda `signalReceived` aruncă o [excepție |api:Nette\Application\UI\BadSignalException]. - -Semnalul poate fi primit de orice componentă, presenter sau obiect care implementează interfața `SignalReceiver` și este conectat la arborele de componente. - -Principalii destinatari ai semnalelor vor fi `Presenterele` și componentele vizuale care moștenesc de la `Control`. Semnalul trebuie să servească drept semn pentru obiect că trebuie să facă ceva – sondajul trebuie să numere votul utilizatorului, blocul cu știri trebuie să se extindă și să afișeze de două ori mai multe știri, formularul a fost trimis și trebuie să proceseze datele și așa mai departe. - -URL-ul pentru semnal îl creăm folosind metoda [Component::link() |api:Nette\Application\UI\Component::link()]. Ca parametru `$destination` transmitem șirul `{signal}!` și ca `$args` un array de argumente pe care dorim să le transmitem semnalului. Semnalul se apelează întotdeauna pe presenterul și acțiunea curentă cu parametrii curenți, parametrii semnalului se adaugă doar. În plus, se adaugă chiar la început **parametrul `?do`, care specifică semnalul**. - -Formatul său este fie `{signal}`, fie `{signalReceiver}-{signal}`. `{signalReceiver}` este numele componentei în presenter. De aceea, în numele componentei nu poate exista cratimă – se folosește pentru a separa numele componentei și semnalul, însă este posibil să se imbricheze astfel mai multe componente. - -Metoda [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] verifică dacă componenta (primul argument) este destinatarul semnalului (al doilea argument). Al doilea argument poate fi omis – atunci verifică dacă componenta este destinatarul oricărui semnal. Ca al doilea parametru se poate specifica `true` și astfel se verifică dacă destinatarul este nu numai componenta specificată, ci și oricare dintre descendenții săi. - -În orice fază anterioară `handle{signal}` putem executa semnalul manual apelând metoda [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], care se ocupă de gestionarea semnalului – ia componenta care a fost desemnată ca destinatar al semnalului (dacă nu este specificat un destinatar al semnalului, acesta este presenterul însuși) și îi trimite semnalul. - -Exemplu: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Astfel, semnalul este executat prematur și nu va mai fi apelat din nou. diff --git a/application/ro/configuration.texy b/application/ro/configuration.texy deleted file mode 100644 index 1645a4868b..0000000000 --- a/application/ro/configuration.texy +++ /dev/null @@ -1,191 +0,0 @@ -Configurația aplicațiilor -************************* - -.[perex] -Prezentare generală a opțiunilor de configurare pentru aplicațiile Nette. - - -Application -=========== - -```neon -application: - # afișează panoul "Nette Application" în Tracy BlueScreen? - debugger: ... # (bool) implicit este true - - # se va apela error-presenter în caz de eroare? - # are efect doar în modul de dezvoltare - catchExceptions: ... # (bool) implicit este true - - # numele error-presenterului - errorPresenter: Error # (string|array) implicit este 'Nette:Error' - - # definește aliasuri pentru presentere și acțiuni - aliases: ... - - # definește reguli pentru traducerea numelui presenterului în clasă - mapping: ... - - # linkurile invalide nu generează avertismente? - # are efect doar în modul de dezvoltare - silentLinks: ... # (bool) implicit este false -``` - -De la versiunea `nette/application` 3.2 se poate defini o pereche de error-presentere: - -```neon -application: - errorPresenter: - 4xx: Error4xx # pentru excepția Nette\Application\BadRequestException - 5xx: Error5xx # pentru celelalte excepții -``` - -Opțiunea `silentLinks` determină cum se comportă Nette în modul de dezvoltare când generarea unui link eșuează (de exemplu, pentru că nu există presenterul etc.). Valoarea implicită `false` înseamnă că Nette va arunca o eroare `E_USER_WARNING`. Setarea la `true` va suprima acest mesaj de eroare. În mediul de producție, `E_USER_WARNING` este întotdeauna aruncat. Acest comportament poate fi, de asemenea, influențat prin setarea variabilei presenterului [$invalidLinkMode |creating-links#Linkuri invalide]. - -[Aliasurile simplifică legarea |creating-links#Aliasuri] la presenterele utilizate frecvent. - -[Maparea definește reguli |directory-structure#Maparea presenterelor], conform cărora din numele presenterului se deduce numele clasei. - - -Înregistrarea automată a presenterelor --------------------------------------- - -Nette adaugă automat presenterele ca servicii în containerul DI, ceea ce accelerează semnificativ crearea lor. Modul în care Nette localizează presenterele poate fi configurat: - -```neon -application: - # caută presentere în Composer class map? - scanComposer: ... # (bool) implicit este true - - # masca pe care trebuie să o respecte numele clasei și al fișierului - scanFilter: ... # (string) implicit este '*Presenter' - - # în ce directoare să caute presentere? - scanDirs: # (string[]|false) implicit este '%appDir%' - - %vendorDir%/mymodule -``` - -Directoarele specificate în `scanDirs` nu suprascriu valoarea implicită `%appDir%`, ci o completează, deci `scanDirs` va conține ambele căi `%appDir%` și `%vendorDir%/mymodule`. Dacă dorim să omitem directorul implicit, folosim [semnul exclamării |dependency-injection:configuration#Combinare], care suprascrie valoarea: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Scanarea directoarelor poate fi dezactivată specificând valoarea false. Nu recomandăm suprimarea completă a adăugării automate a presenterelor, deoarece altfel performanța aplicației va scădea. - - -Șabloane Latte -============== - -Prin această setare se poate influența global comportamentul Latte în componente și presentere. - -```neon -latte: - # afișează panoul Latte în Tracy Bar pentru șablonul principal (true) sau toate componentele (all)? - debugger: ... # (true|false|'all') implicit este true - - # generează șabloane cu antetul declare(strict_types=1) - strictTypes: ... # (bool) implicit este false - - # activează modul [parser strict |latte:develop#striktní režim] - strictParsing: ... # (bool) implicit este false - - # activează [verificarea codului generat |latte:develop#Kontrola vygenerovaného kódu] - phpLinter: ... # (string) implicit este null - - # setează locale - locale: cs_CZ # (string) implicit este null - - # clasa obiectului $this->template - templateClass: App\MyTemplateClass # implicit este Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Dacă utilizați Latte versiunea 3, puteți adăuga noi [extensii |latte:extending-latte#Latte Extension] folosind: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Dacă utilizați Latte versiunea 2, puteți înregistra noi tag-uri fie specificând numele clasei, fie o referință la serviciu. Implicit, se apelează metoda `install()`, dar acest lucru poate fi schimbat specificând numele altei metode: - -```neon -latte: - # înregistrarea tag-urilor Latte personalizate - macros: - - App\MyLatteMacros::register # metodă statică, classname sau callable - - @App\MyLatteMacrosFactory # serviciu cu metoda install() - - @App\MyLatteMacrosFactory::register # serviciu cu metoda register() - -services: - - App\MyLatteMacrosFactory -``` - - -Rutare -====== - -Setări de bază: - -```neon -routing: - # afișează panoul de rutare în Tracy Bar? - debugger: ... # (bool) implicit este true - - # serializează routerul în containerul DI - cache: ... # (bool) implicit este false -``` - -Rutarea o definim de obicei în clasa [RouterFactory |routing#Colecție de rute]. Alternativ, rutele pot fi definite și în configurație folosind perechi `mască: acțiune`, dar această metodă nu oferă o varietate atât de largă în setări: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Constante -========= - -Crearea constantelor PHP. - -```neon -constants: - Foobar: 'baz' -``` - -După pornirea aplicației, va fi creată constanta `Foobar`. - -.[note] -Constantele nu ar trebui să servească drept variabile disponibile global. Pentru transmiterea valorilor către obiecte, utilizați [dependency injection |dependency-injection:passing-dependencies]. - - -PHP -=== - -Setarea directivelor PHP. O prezentare generală a tuturor directivelor o găsiți pe [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -Servicii DI -=========== - -Aceste servicii sunt adăugate în containerul DI: - -| Nume | Tip | Descriere -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [lansatorul întregii aplicații |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | fabrică de presentere -| `application.###` | [api:Nette\Application\UI\Presenter] | presentere individuale -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | fabrică a obiectului `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | fabrică pentru [`$this->template` |templates] diff --git a/application/ro/creating-links.texy b/application/ro/creating-links.texy deleted file mode 100644 index e44ffb9998..0000000000 --- a/application/ro/creating-links.texy +++ /dev/null @@ -1,286 +0,0 @@ -Crearea linkurilor URL -********************** - -
    - -Crearea linkurilor în Nette este simplă, ca și cum ai arăta cu degetul. Trebuie doar să țintești și framework-ul va face toată munca pentru tine. Vom arăta: - -- cum să creezi linkuri în șabloane și în altă parte -- cum să distingi un link către pagina curentă -- ce să faci cu linkurile invalide - -
    - - -Datorită [rutării bidirecționale |routing], nu va trebui niciodată să scrieți manual adresele URL ale aplicației dvs. în șabloane sau cod, adrese care s-ar putea schimba ulterior, sau să le compuneți complicat. În link este suficient să specificați presenterul și acțiunea, să transmiteți eventualii parametri și framework-ul va genera URL-ul singur. De fapt, este foarte asemănător cu apelarea unei funcții. Acest lucru vă va plăcea. - - -În șablonul presenterului -========================= - -Cel mai adesea creăm linkuri în șabloane, iar un ajutor excelent este atributul `n:href`: - -```latte -detaliu -``` - -Observați că în loc de atributul HTML `href`, am folosit [n:atributul |latte:syntax#n:atribute] `n:href`. Valoarea sa nu este apoi URL-ul, așa cum ar fi în cazul atributului `href`, ci numele presenterului și al acțiunii. - -Click-ul pe link este, simplificat spus, ceva asemănător cu apelarea metodei `ProductPresenter::renderShow()`. Și dacă are parametri în semnătura sa, o putem apela cu argumente: - -```latte -detaliu produs -``` - -Este posibil să se transmită și parametri numiți. Următorul link transmite parametrul `lang` cu valoarea `cs`: - -```latte -detaliu produs -``` - -Dacă metoda `ProductPresenter::renderShow()` nu are `$lang` în semnătura sa, poate afla valoarea parametrului folosind `$lang = $this->getParameter('lang')` sau din [proprietate |presenters#Parametrii cererii]. - -Dacă parametrii sunt stocați într-un array, aceștia pot fi expandați cu operatorul `...` (în Latte 2.x cu operatorul `(expand)`): - -```latte -{var $args = [$product->id, lang => cs]} -detaliu produs -``` - -În linkuri se transmit automat și așa-numiții [parametri persistenți |presenters#Parametri persistenți]. - -Atributul `n:href` este foarte util pentru tag-urile HTML ``. Dacă dorim să afișăm linkul în altă parte, de exemplu în text, folosim `{link}`: - -```latte -Adresa este: {link Home:default} -``` - - -În cod -====== - -Pentru a crea un link în presenter se folosește metoda `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Parametrii pot fi transmiși și printr-un array, unde se pot specifica și parametri numiți: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); -``` - -Linkurile pot fi create și fără presenter, pentru asta există [#LinkGenerator] și metoda sa `link()`. - - -Linkuri către presenter -======================= - -Dacă ținta linkului este un presenter și o acțiune, are această sintaxă: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Formatul este suportat de toate tag-urile Latte și toate metodele presenterului care lucrează cu linkuri, adică `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` și, de asemenea, [#LinkGenerator]. Deci, chiar dacă în exemple este folosit `n:href`, ar putea fi oricare dintre funcții. - -Forma de bază este deci `Presenter:action`: - -```latte -pagina principală -``` - -Dacă facem referire la acțiunea presenterului curent, putem omite numele acestuia: - -```latte -pagina principală -``` - -Dacă ținta este acțiunea `default`, o putem omite, dar două puncte trebuie să rămână: - -```latte -pagina principală -``` - -Linkurile pot, de asemenea, să direcționeze către alte [module |directory-structure#Presentere și șabloane]. Aici, linkurile se disting între cele relative către un submodul imbricat și cele absolute. Principiul este analog cu căile de pe disc, doar că în loc de slash-uri sunt două puncte. Presupunem că presenterul curent face parte din modulul `Front`, atunci scriem: - -```latte -link către Front:Shop:Product:show -link către Admin:Product:show -``` - -Un caz special este linkul [către sine însuși |#Link către pagina curentă], când specificăm `this` ca țintă. - -```latte -refresh -``` - -Putem face referire la o anumită parte a paginii prin așa-numitul fragment după semnul diez `#`: - -```latte -link către Home:default și fragmentul #main -``` - - -Căi absolute -============ - -Linkurile generate folosind `link()` sau `n:href` sunt întotdeauna căi absolute (adică încep cu caracterul `/`), dar nu URL-uri absolute cu protocol și domeniu precum `https://domain`. - -Pentru a genera un URL absolut, adăugați două slash-uri la început (de ex. `n:href="//Home:"`). Sau puteți comuta presenterul să genereze doar linkuri absolute setând `$this->absoluteUrls = true`. - - -Link către pagina curentă -========================= - -Ținta `this` creează un link către pagina curentă: - -```latte -refresh -``` - -În același timp, se transmit și toți parametrii specificați în semnătura metodei `action()` sau `render()`, dacă `action()` nu este definită. Deci, dacă suntem pe pagina `Product:show` și `id: 123`, linkul către `this` va transmite și acest parametru. - -Desigur, este posibil să specificați parametrii direct: - -```latte -refresh -``` - -Funcția `isLinkCurrent()` verifică dacă ținta linkului este identică cu pagina curentă. Acest lucru poate fi utilizat, de exemplu, în șablon pentru a distinge linkurile etc. - -Parametrii sunt aceiași ca la metoda `link()`, dar în plus este posibil să se specifice un wildcard `*` în loc de o acțiune specifică, ceea ce înseamnă orice acțiune a presenterului respectiv. - -```latte -{if !isLinkCurrent('Admin:login')} - Conectați-vă -{/if} - -
  • - ... -
  • -``` - -În combinație cu `n:href` într-un singur element, se poate folosi o formă prescurtată: - -```latte -... -``` - -Wildcard-ul `*` poate fi folosit doar în locul acțiunii, nu și al presenterului. - -Pentru a verifica dacă ne aflăm într-un anumit modul sau submodul al acestuia, folosim metoda `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Linkuri către semnal -==================== - -Ținta linkului nu trebuie să fie doar un presenter și o acțiune, ci și un [semnal |components#Semnal] (apelează metoda `handle()`). Atunci sintaxa este următoarea: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -Semnalul este deci distins prin semnul exclamării: - -```latte -semnal -``` - -Se poate crea și un link către semnalul unei subcomponente (sau sub-subcomponente): - -```latte -semnal -``` - - -Linkuri în componentă -===================== - -Deoarece [componentele|components] sunt unități separate, reutilizabile, care nu ar trebui să aibă nicio legătură cu presenterele din jur, linkurile funcționează aici puțin diferit. Atributul Latte `n:href` și tag-ul `{link}`, precum și metodele componentei precum `link()` și altele consideră ținta linkului **întotdeauna ca fiind numele semnalului**. De aceea, nu este necesar nici măcar să se specifice semnul exclamării: - -```latte -semnal, nu acțiune -``` - -Dacă am dori să facem referire la presentere în șablonul componentei, folosim tag-ul `{plink}`: - -```latte -introducere -``` - -sau în cod - -```php -$this->getPresenter()->link('Home:default') -``` - - -Aliasuri .{data-version:v3.2.2} -=============================== - -Uneori poate fi util să atribuiți perechii Presenter:acțiune un alias ușor de reținut. De exemplu, pagina de start `Front:Home:default` să o numiți simplu `home` sau `Admin:Dashboard:default` ca `admin`. - -Aliasurile se definesc în [configurație|configuration] sub cheia `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -În linkuri se scriu apoi folosind arondul, de exemplu: - -```latte -administrare -``` - -Sunt suportate și în toate metodele care lucrează cu linkuri, cum ar fi `redirect()` și altele asemenea. - - -Linkuri invalide -================ - -Se poate întâmpla să creăm un link invalid - fie pentru că duce la un presenter inexistent, fie pentru că transmite mai mulți parametri decât acceptă metoda țintă în semnătura sa, sau când nu se poate genera un URL pentru acțiunea țintă. Cum să tratăm linkurile invalide este determinat de variabila statică `Presenter::$invalidLinkMode`. Aceasta poate lua o combinație a acestor valori (constante): - -- `Presenter::InvalidLinkSilent` - mod silențios, ca URL se returnează caracterul # -- `Presenter::InvalidLinkWarning` - se aruncă o avertizare E_USER_WARNING, care va fi înregistrată în modul de producție, dar nu va cauza întreruperea execuției scriptului -- `Presenter::InvalidLinkTextual` - avertizare vizuală, afișează eroarea direct în link -- `Presenter::InvalidLinkException` - se aruncă excepția InvalidLinkException - -Setarea implicită este `InvalidLinkWarning` în modul de producție și `InvalidLinkWarning | InvalidLinkTextual` în modul de dezvoltare. `InvalidLinkWarning` în mediul de producție nu cauzează întreruperea scriptului, dar avertizarea va fi înregistrată. În mediul de dezvoltare, [Tracy |tracy:] o va captura și va afișa un bluescreen. `InvalidLinkTextual` funcționează astfel încât returnează ca URL un mesaj de eroare care începe cu caracterele `#error:`. Pentru ca astfel de linkuri să fie vizibile la prima vedere, adăugăm în CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Dacă nu dorim să se producă avertizări în mediul de dezvoltare, putem seta modul silențios direct în [configurație|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Cum să creăm linkuri cu un confort similar cu cel al metodei `link()`, dar fără prezența unui presenter? Pentru asta există [api:Nette\Application\LinkGenerator]. - -LinkGenerator este un serviciu pe care îl puteți primi prin constructor și apoi crea linkuri folosind metoda sa `link()`. - -Spre deosebire de presentere, există o diferență. LinkGenerator creează toate linkurile direct ca URL-uri absolute. Și, în plus, nu există niciun "presenter curent", deci nu se poate specifica doar numele acțiunii `link('default')` ca țintă sau specifica căi relative către module. - -Linkurile invalide aruncă întotdeauna `Nette\Application\UI\InvalidLinkException`. diff --git a/application/ro/directory-structure.texy b/application/ro/directory-structure.texy deleted file mode 100644 index bd9646acc6..0000000000 --- a/application/ro/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Structura directoarelor aplicației -********************************** - -
    - -Cum să proiectăm o structură de directoare clară și scalabilă pentru proiectele în Nette Framework? Vom arăta practici dovedite care vă vor ajuta cu organizarea codului. Veți afla: - -- cum să **împărțiți logic** aplicația în directoare -- cum să proiectați structura astfel încât să **scaleze bine** odată cu creșterea proiectului -- care sunt **alternativele posibile** și avantajele sau dezavantajele lor - -
    - - -Este important de menționat că Nette Framework însuși nu impune nicio structură specifică. Este proiectat astfel încât să poată fi ușor adaptat la orice nevoi și preferințe. - - -Structura de bază a proiectului -=============================== - -Deși Nette Framework nu dictează nicio structură de directoare fixă, există o aranjare implicită dovedită sub forma [Web Project|https://github.com/nette/web-project]: - -/--pre -web-project/ -├── app/ ← director cu aplicația -├── assets/ ← fișiere SCSS, JS, imagini..., alternativ resources/ -├── bin/ ← scripturi pentru linia de comandă -├── config/ ← configurație -├── log/ ← erori înregistrate -├── temp/ ← fișiere temporare, cache -├── tests/ ← teste -├── vendor/ ← biblioteci instalate de Composer -└── www/ ← director public (document-root) -\-- - -Această structură poate fi modificată liber în funcție de nevoile dvs. - folderele pot fi redenumite sau mutate. Apoi este suficient doar să modificați căile relative către directoare în fișierul `Bootstrap.php` și eventual `composer.json`. Nimic mai mult nu este necesar, nicio reconfigurare complicată, nicio modificare a constantelor. Nette dispune de autodetecție inteligentă și recunoaște automat locația aplicației, inclusiv baza sa URL. - - -Principii de organizare a codului -================================= - -Când explorați pentru prima dată un proiect nou, ar trebui să vă orientați rapid în el. Imaginați-vă că deschideți directorul `app/Model/` și vedeți această structură: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -Din aceasta deduceți doar că proiectul folosește niște servicii, depozite și entități. Despre scopul real al aplicației nu aflați absolut nimic. - -Să ne uităm la o altă abordare - **organizarea pe domenii**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Aici este altfel - la prima vedere este clar că este vorba despre un magazin online. Chiar și numele directoarelor dezvăluie ce poate face aplicația - lucrează cu plăți, comenzi și produse. - -Prima abordare (organizarea după tipul claselor) aduce în practică o serie de probleme: codul care este logic legat este fragmentat în diferite foldere și trebuie să săriți între ele. De aceea, vom organiza pe domenii. - - -Spații de nume --------------- - -Este obișnuit ca structura directoarelor să corespundă spațiilor de nume din aplicație. Aceasta înseamnă că locația fizică a fișierelor corespunde namespace-ului lor. De exemplu, o clasă situată în `app/Model/Product/ProductRepository.php` ar trebui să aibă namespace-ul `App\Model\Product`. Acest principiu ajută la orientarea în cod și simplifică autoloading-ul. - - -Singular vs plural în nume --------------------------- - -Observați că pentru directoarele principale ale aplicației folosim singularul: `app`, `config`, `log`, `temp`, `www`. La fel și în interiorul aplicației: `Model`, `Core`, `Presentation`. Acest lucru se datorează faptului că fiecare dintre ele reprezintă un concept unitar. - -Similar, de exemplu, `app/Model/Product` reprezintă totul legat de produse. Nu îl vom numi `Products`, deoarece nu este un folder plin de produse (acolo ar fi fișiere `nokia.php`, `samsung.php`). Este un namespace care conține clase pentru lucrul cu produse - `ProductRepository.php`, `ProductService.php`. - -Folderul `app/Tasks` este la plural deoarece conține un set de scripturi executabile separate - `CleanupTask.php`, `ImportTask.php`. Fiecare dintre ele este o unitate separată. - -Pentru consistență, recomandăm utilizarea: -- Singularului pentru namespace-ul care reprezintă un ansamblu funcțional (chiar dacă lucrează cu mai multe entități) -- Pluralului pentru colecții de unități separate -- În caz de incertitudine sau dacă nu doriți să vă gândiți la asta, alegeți singularul - - -Director public `www/` -====================== - -Acest director este singurul accesibil de pe web (așa-numitul document-root). Adesea puteți întâlni și numele `public/` în loc de `www/` - este doar o chestiune de convenție și nu are nicio influență asupra funcționalității aplicației. Directorul conține: -- [Punctul de intrare |bootstrapping#index.php] al aplicației `index.php` -- Fișierul `.htaccess` cu reguli pentru mod_rewrite (pentru Apache) -- Fișiere statice (CSS, JavaScript, imagini) -- Fișiere încărcate - -Pentru securitatea corectă a aplicației, este esențial să aveți [configurat corect document-root |nette:troubleshooting#Cum să schimbați sau să eliminați directorul www din URL]. - -.[note] -Nu plasați niciodată folderul `node_modules/` în acest director - conține mii de fișiere care pot fi executabile și nu ar trebui să fie accesibile public. - - -Director aplicație `app/` -========================= - -Acesta este directorul principal cu codul aplicației. Structura de bază: - -/--pre -app/ -├── Core/ ← aspecte de infrastructură -├── Model/ ← logica de business -├── Presentation/ ← presentere și șabloane -├── Tasks/ ← scripturi de comandă -└── Bootstrap.php ← clasa de inițializare a aplicației -\-- - -`Bootstrap.php` este [clasa de pornire a aplicației|bootstrapping], care inițializează mediul, încarcă configurația și creează containerul DI. - -Să ne uităm acum mai detaliat la subdirectoarele individuale. - - -Presentere și șabloane -====================== - -Partea de prezentare a aplicației o avem în directorul `app/Presentation`. O alternativă este scurtul `app/UI`. Este locul pentru toți presenterele, șabloanele lor și eventualele clase ajutătoare. - -Acest strat îl organizăm pe domenii. Într-un proiect complex, care combină un magazin online, un blog și un API, structura ar arăta astfel: - -/--pre -app/Presentation/ -├── Shop/ ← frontend magazin online -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administrare -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← endpoint-uri API - └── V1/ -\-- - -Pe de altă parte, pentru un blog simplu, am folosi o împărțire: - -/--pre -app/Presentation/ -├── Front/ ← frontend web -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administrare -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, sitemap-uri etc. -\-- - -Foldere precum `Home/` sau `Dashboard/` conțin presentere și șabloane. Foldere precum `Front/`, `Admin/` sau `Api/` le numim **module**. Tehnic, sunt directoare obișnuite care servesc la împărțirea logică a aplicației. - -Fiecare folder cu un presenter conține un presenter cu același nume și șabloanele sale. De exemplu, folderul `Dashboard/` conține: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← șablon -\-- - -Această structură de directoare se reflectă în spațiile de nume ale claselor. De exemplu, `DashboardPresenter` se află în spațiul de nume `App\Presentation\Admin\Dashboard` (vezi [#Maparea presenterelor]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -La presenterul `Dashboard` din interiorul modulului `Admin` facem referire în aplicație folosind notația cu două puncte ca `Admin:Dashboard`. La acțiunea sa `default` apoi ca `Admin:Dashboard:default`. În cazul modulelor imbricate, folosim mai multe două puncte, de exemplu `Shop:Order:Detail:default`. - - -Dezvoltare flexibilă a structurii ---------------------------------- - -Unul dintre marile avantaje ale acestei structuri este cât de elegant se adaptează la nevoile în creștere ale proiectului. Ca exemplu, să luăm partea care generează feed-uri XML. La început avem o formă simplă: - -/--pre -Export/ -├── ExportPresenter.php ← un singur presenter pentru toate exporturile -├── sitemap.latte ← șablon pentru sitemap -└── feed.latte ← șablon pentru feed RSS -\-- - -Cu timpul, apar noi tipuri de feed-uri și avem nevoie de mai multă logică pentru ele... Nicio problemă! Folderul `Export/` devine pur și simplu un modul: - -/--pre -Export/ -├── Sitemap/ -│ ├── SitemapPresenter.php -│ └── sitemap.latte -└── Feed/ - ├── FeedPresenter.php - ├── zbozi.latte ← feed pentru Zboží.cz - └── heureka.latte ← feed pentru Heureka.cz -\-- - -Această transformare este complet fluidă - este suficient să creați noi subfoldere, să împărțiți codul în ele și să actualizați linkurile (de ex. de la `Export:feed` la `Export:Feed:zbozi`). Datorită acestui fapt, putem extinde treptat structura după necesități, nivelul de imbricare nu este limitat în niciun fel. - -Dacă, de exemplu, în administrare aveți mulți presenteri referitori la gestionarea comenzilor, cum ar fi `OrderDetail`, `OrderEdit`, `OrderDispatch` etc., puteți crea pentru o mai bună organizare în acest loc un modul (folder) `Order`, în care vor fi (foldere pentru) presenterele `Detail`, `Edit`, `Dispatch` și altele. - - -Amplasarea șabloanelor ----------------------- - -În exemplele anterioare am văzut că șabloanele sunt plasate direct în folderul cu presenterul: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← clasă opțională pentru șablon -└── default.latte ← șablon -\-- - -Această amplasare se dovedește în practică a fi cea mai convenabilă - aveți toate fișierele aferente la îndemână. - -Alternativ, puteți plasa șabloanele într-un subfolder `templates/`. Nette suportă ambele variante. Puteți chiar plasa șabloanele complet în afara folderului `Presentation/`. Totul despre posibilitățile de amplasare a șabloanelor găsiți în capitolul [Căutarea șabloanelor |templates#Căutarea șabloanelor]. - - -Clase ajutătoare și componente ------------------------------- - -Presenterelor și șabloanelor le aparțin adesea și alte fișiere ajutătoare. Le plasăm logic în funcție de domeniul lor de aplicare: - -1. **Direct lângă presenter** în cazul componentelor specifice pentru presenterul respectiv: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← componentă pentru listarea produselor -└── FilterForm.php ← formular pentru filtrare -\-- - -2. **Pentru modul** - recomandăm utilizarea folderului `Accessory`, care se plasează convenabil chiar la începutul alfabetului: - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← componente pentru frontend -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **Pentru întreaga aplicație** - în `Presentation/Accessory/`: -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Sau puteți plasa clase ajutătoare precum `LatteExtension.php` sau `TemplateFilters.php` în folderul de infrastructură `app/Core/Latte/`. Și componentele în `app/Components`. Alegerea depinde de obiceiurile echipei. - - -Model - inima aplicației -======================== - -Modelul conține întreaga logică de business a aplicației. Pentru organizarea sa se aplică din nou regula - structurăm pe domenii: - -/--pre -app/Model/ -├── Payment/ ← totul despre plăți -│ ├── PaymentFacade.php ← principalul punct de intrare -│ ├── PaymentRepository.php -│ ├── Payment.php ← entitate -├── Order/ ← totul despre comenzi -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← totul despre transport -\-- - -În model veți întâlni de obicei aceste tipuri de clase: - -**Facade**: reprezintă principalul punct de intrare într-un domeniu specific al aplicației. Acționează ca un orchestrator care coordonează colaborarea între diferite servicii în scopul implementării cazurilor de utilizare complete (cum ar fi "creează comandă" sau "procesează plată"). Sub stratul său de orchestrator, fațada ascunde detaliile de implementare de restul aplicației, oferind astfel o interfață curată pentru lucrul cu domeniul respectiv. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // validare - // creare comandă - // trimitere e-mail - // înregistrare în statistici - } -} -``` - -**Servicii**: se concentrează pe o operațiune specifică de business în cadrul domeniului. Spre deosebire de fațadă, care orchestrează cazuri de utilizare întregi, serviciul implementează o logică de business specifică (cum ar fi calcule de prețuri sau procesarea plăților). Serviciile sunt de obicei fără stare și pot fi utilizate fie de fațade ca blocuri de construcție pentru operațiuni mai complexe, fie direct de alte părți ale aplicației pentru sarcini mai simple. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // calcul preț - } -} -``` - -**Depozite**: asigură întreaga comunicare cu stocarea de date, de obicei o bază de date. Sarcina sa este de a încărca și salva entități și de a implementa metode pentru căutarea lor. Depozitul izolează restul aplicației de detaliile de implementare ale bazei de date și oferă o interfață orientată pe obiecte pentru lucrul cu datele. - -```php -class OrderRepository -{ - public function find(int $id): ?Order - { - } - - public function findByCustomer(int $customerId): array - { - } -} -``` - -**Entități**: obiecte care reprezintă principalele concepte de business în aplicație, care au identitatea lor și se schimbă în timp. De obicei, sunt clase mapate pe tabele de baze de date folosind ORM (cum ar fi Nette Database Explorer sau Doctrine). Entitățile pot conține reguli de business referitoare la datele lor și logică de validare. - -```php -// Entitate mapată pe tabela de bază de date 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, - ]); - } -} -``` - -**Obiecte valoare**: obiecte imuabile care reprezintă valori fără identitate proprie - de exemplu, o sumă de bani sau o adresă de e-mail. Două instanțe ale unui obiect valoare cu aceleași valori sunt considerate identice. - - -Cod de infrastructură -===================== - -Folderul `Core/` (sau și `Infrastructure/`) este casa pentru baza tehnică a aplicației. Codul de infrastructură include de obicei: - -/--pre -app/Core/ -├── Router/ ← rutare și management URL -│ └── RouterFactory.php -├── Security/ ← autentificare și autorizare -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← logare și monitorizare -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← strat de cache -│ └── FullPageCache.php -└── Integration/ ← integrare cu servicii ext. - ├── Slack/ - └── Stripe/ -\-- - -Pentru proiecte mai mici, este suficientă, desigur, o structură plată: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -Este vorba despre cod care: - -- Rezolvă infrastructura tehnică (rutare, logare, cache) -- Integrează servicii externe (Sentry, Elasticsearch, Redis) -- Oferă servicii de bază pentru întreaga aplicație (mail, bază de date) -- Este în mare parte independent de domeniul specific - cache-ul sau loggerul funcționează la fel pentru magazinul online sau blog. - -Ezitați dacă o anumită clasă aparține aici sau în model? Diferența cheie este că codul din `Core/`: - -- Nu știe nimic despre domeniu (produse, comenzi, articole) -- Este în mare parte posibil să fie transferat într-un alt proiect -- Rezolvă "cum funcționează" (cum se trimite un mail), nu "ce face" (ce mail să trimită) - -Exemplu pentru o mai bună înțelegere: - -- `App\Core\MailerFactory` - creează instanțe ale clasei pentru trimiterea e-mailurilor, rezolvă setările SMTP -- `App\Model\OrderMailer` - folosește `MailerFactory` pentru a trimite e-mailuri despre comenzi, cunoaște șabloanele lor și știe când trebuie trimise - - -Scripturi de comandă -==================== - -Aplicațiile au adesea nevoie să execute activități în afara cererilor HTTP obișnuite - fie că este vorba de procesarea datelor în fundal, întreținere sau sarcini periodice. Pentru rulare se folosesc scripturi simple în directorul `bin/`, logica de implementare propriu-zisă o plasăm apoi în `app/Tasks/` (eventual `app/Commands/`). - -Exemplu: - -/--pre -app/Tasks/ -├── Maintenance/ ← scripturi de întreținere -│ ├── CleanupCommand.php ← ștergerea datelor vechi -│ └── DbOptimizeCommand.php ← optimizarea bazei de date -├── Integration/ ← integrare cu sisteme externe -│ ├── ImportProducts.php ← import din sistemul furnizorului -│ └── SyncOrders.php ← sincronizarea comenzilor -└── Scheduled/ ← sarcini regulate - ├── NewsletterCommand.php ← trimiterea newsletterelor - └── ReminderCommand.php ← notificări clienți -\-- - -Ce aparține modelului și ce scripturilor de comandă? De exemplu, logica pentru trimiterea unui singur e-mail face parte din model, trimiterea în masă a mii de e-mailuri aparține deja `Tasks/`. - -Sarcinile le [rulăm de obicei din linia de comandă |https://blog.nette.org/en/cli-scripts-in-nette-application] sau prin cron. Pot fi rulate și prin cerere HTTP, dar trebuie să ne gândim la securitate. Presenterul care rulează sarcina trebuie securizat, de exemplu, doar pentru utilizatorii conectați sau cu un token puternic și acces de la adrese IP permise. Pentru sarcinile lungi, este necesar să se mărească limita de timp a scriptului și să se folosească `session_write_close()`, pentru a nu bloca sesiunea. - - -Alte directoare posibile -======================== - -Pe lângă directoarele de bază menționate, puteți adăuga, în funcție de nevoile proiectului, alte foldere specializate. Să ne uităm la cele mai frecvente dintre ele și la utilizarea lor: - -/--pre -app/ -├── Api/ ← logica pentru API independentă de stratul de prezentare -├── Database/ ← scripturi de migrare și seedere pentru date de test -├── Components/ ← componente vizuale partajate în întreaga aplicație -├── Event/ ← util dacă utilizați arhitectura bazată pe evenimente -├── Mail/ ← șabloane de e-mail și logica aferentă -└── Utils/ ← clase ajutătoare -\-- - -Pentru componentele vizuale partajate utilizate în presentere în întreaga aplicație, se poate folosi folderul `app/Components` sau `app/Controls`: - -/--pre -app/Components/ -├── Form/ ← componente de formular partajate -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← componente pentru listări de date -│ └── DataGrid.php -└── Navigation/ ← elemente de navigație - ├── Breadcrumbs.php - └── Menu.php -\-- - -Aici aparțin componentele care au o logică mai complexă. Dacă doriți să partajați componente între mai multe proiecte, este recomandabil să le extrageți într-un pachet composer separat. - -În directorul `app/Mail` puteți plasa gestionarea comunicării prin e-mail: - -/--pre -app/Mail/ -├── templates/ ← șabloane de e-mail -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Maparea presenterelor -===================== - -Maparea definește reguli pentru derivarea numelui clasei din numele presenterului. Le specificăm în [configurație|configuration] sub cheia `application › mapping`. - -Pe această pagină am arătat că plasăm presenterele în folderul `app/Presentation` (eventual `app/UI`). Această convenție trebuie să o comunicăm lui Nette în fișierul de configurare. Este suficientă o singură linie: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Cum funcționează maparea? Pentru o mai bună înțelegere, să ne imaginăm mai întâi o aplicație fără module. Dorim ca clasele presenterelor să se încadreze în spațiul de nume `App\Presentation`, astfel încât presenterul `Home` să fie mapat pe clasa `App\Presentation\HomePresenter`. Ceea ce realizăm cu această configurație: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Maparea funcționează astfel încât numele presenterului `Home` înlocuiește asteriscul din masca `App\Presentation\*Presenter`, obținând astfel numele final al clasei `App\Presentation\HomePresenter`. Simplu! - -Dar, după cum vedeți în exemplele din acest capitol și din altele, plasăm clasele presenterelor în subdirectoare eponime, de exemplu, presenterul `Home` se mapează pe clasa `App\Presentation\Home\HomePresenter`. Acest lucru se realizează prin dublarea celor două puncte (necesită Nette Application 3.2): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Acum trecem la maparea presenterelor în module. Pentru fiecare modul putem defini o mapare specifică: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -Conform acestei configurații, presenterul `Front:Home` se mapează pe clasa `App\Presentation\Front\Home\HomePresenter`, în timp ce presenterul `Api:OAuth` pe clasa `App\Api\OAuthPresenter`. - -Deoarece modulele `Front` și `Admin` au un mod similar de mapare și probabil vor exista mai multe astfel de module, este posibil să se creeze o regulă generală care să le înlocuiască. Astfel, în masca clasei va apărea un nou asterisc pentru modul: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Funcționează și pentru structuri de directoare mai adânc imbricate, cum ar fi, de exemplu, presenterul `Admin:User:Edit`, segmentul cu asterisc se repetă pentru fiecare nivel și rezultatul este clasa `App\Presentation\Admin\User\Edit\EditPresenter`. - -O notație alternativă este să folosim un array format din trei segmente în loc de un șir de caractere. Această notație este echivalentă cu cea anterioară: - -```neon -application: - mapping: - *: [App\Presentation, *, **Presenter] - Api: [App\Api, '', *Presenter] -``` diff --git a/application/ro/how-it-works.texy b/application/ro/how-it-works.texy deleted file mode 100644 index 63df4130f0..0000000000 --- a/application/ro/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Cum funcționează aplicațiile? -***************************** - -
    - -Tocmai citiți documentul de bază al documentației Nette. Veți afla întregul principiu de funcționare al aplicațiilor web. De la A la Z, de la momentul nașterii până la ultima suflare a scriptului PHP. După citire, veți ști: - -- cum funcționează totul -- ce sunt Bootstrap, Presenter și containerul DI -- cum arată structura directoarelor - -
    - - -Structura directoarelor -======================= - -Deschideți exemplul de schelet al unei aplicații web numit [WebProject|https://github.com/nette/web-project] și, în timp ce citiți, puteți privi fișierele despre care este vorba. - -Structura directoarelor arată cam așa: - -/--pre -web-project/ -├── app/ ← director cu aplicația -│ ├── Core/ ← clase de bază necesare pentru funcționare -│ │ └── RouterFactory.php ← configurarea adreselor URL -│ ├── Presentation/ ← presentere, șabloane & co. -│ │ ├── @layout.latte ← șablon de layout -│ │ └── Home/ ← directorul presenterului Home -│ │ ├── HomePresenter.php ← clasa presenterului Home -│ │ └── default.latte ← șablonul acțiunii default -│ └── Bootstrap.php ← clasa de inițializare Bootstrap -├── assets/ ← resurse (SCSS, TypeScript, imagini sursă) -├── bin/ ← scripturi rulate din linia de comandă -├── config/ ← fișiere de configurare -│ ├── common.neon -│ └── services.neon -├── log/ ← erori înregistrate -├── temp/ ← fișiere temporare, cache, … -├── vendor/ ← biblioteci instalate de Composer -│ ├── ... -│ └── autoload.php ← autoloading pentru toate pachetele instalate -├── www/ ← director public sau document-root al proiectului -│ ├── assets/ ← fișiere statice compilate (CSS, JS, imagini, ...) -│ ├── .htaccess ← reguli mod_rewrite -│ └── index.php ← fișierul inițial prin care se lansează aplicația -└── .htaccess ← interzice accesul la toate directoarele, cu excepția www -\-- - -Structura directoarelor poate fi modificată oricum, folderele pot fi redenumite sau mutate, este complet flexibilă. Nette dispune, în plus, de autodetecție inteligentă și recunoaște automat locația aplicației, inclusiv baza sa URL. - -Pentru aplicații puțin mai mari, putem [împărți folderele cu presentere și șabloane în subdirectoare |directory-structure#Presentere și șabloane] și clasele în spații de nume, pe care le numim module. - -Directorul `www/` reprezintă așa-numitul director public sau document-root al proiectului. Îl puteți redenumi fără a fi nevoie să setați altceva în partea de aplicație. Este necesar doar să [configurați hostingul |nette:troubleshooting#Cum să schimbați sau să eliminați directorul www din URL] astfel încât document-root să indice către acest director. - -WebProject poate fi, de asemenea, descărcat direct, inclusiv Nette, folosind [Composer |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -Pe Linux sau macOS, setați [permisiunile de scriere |nette:troubleshooting#Setarea permisiunilor pentru directoare] pentru directoarele `log/` și `temp/`. - -Aplicația WebProject este gata de rulare, nu este nevoie să configurați absolut nimic și o puteți afișa direct în browser accesând folderul `www/`. - - -Cerere HTTP -=========== - -Totul începe în momentul în care utilizatorul deschide pagina în browser. Adică atunci când browserul bate la ușa serverului cu o cerere HTTP. Cererea vizează un singur fișier PHP, care se află în directorul public `www/`, și acesta este `index.php`. Să presupunem că este vorba despre o cerere pentru adresa `https://example.com/product/123`. Datorită [setărilor adecvate ale serverului |nette:troubleshooting#Cum să configurați serverul pentru URL-uri prietenoase], chiar și acest URL este mapat pe fișierul `index.php` și acesta se execută. - -Sarcina sa este: - -1) inițializarea mediului -2) obținerea fabricii -3) pornirea aplicației Nette, care va gestiona cererea - -Ce fel de fabrică? Nu producem tractoare, ci pagini web! Aveți răbdare, se va explica imediat. - -Prin „inițializarea mediului” ne referim, de exemplu, la faptul că se activează [Tracy|tracy:], care este un instrument uimitor pentru înregistrarea sau vizualizarea erorilor. Pe serverul de producție, înregistrează erorile, pe cel de dezvoltare le afișează direct. Prin urmare, inițializarea include și decizia dacă site-ul rulează în modul de producție sau de dezvoltare. Pentru aceasta, Nette utilizează [autodetecția inteligentă |bootstrapping#Modul de dezvoltare vs producție]: dacă rulați site-ul pe localhost, rulează în modul de dezvoltare. Nu trebuie să configurați nimic și aplicația este direct pregătită atât pentru dezvoltare, cât și pentru implementarea live. Acești pași se efectuează și sunt descriși detaliat în capitolul despre [clasa Bootstrap|bootstrapping]. - -Al treilea punct (da, am sărit peste al doilea, dar vom reveni la el) este pornirea aplicației. Gestionarea cererilor HTTP este responsabilitatea clasei `Nette\Application\Application` (în continuare `Application`), așa că atunci când spunem pornirea aplicației, ne referim în mod specific la apelarea metodei cu numele sugestiv `run()` pe obiectul acestei clase. - -Nette este un mentor care vă ghidează să scrieți aplicații curate conform metodologiilor dovedite. Și una dintre cele absolut cele mai dovedite se numește **dependency injection**, prescurtat DI. În acest moment, nu vrem să vă încărcăm cu explicații despre DI, pentru asta există [un capitol separat|dependency-injection:introduction], esențial este rezultatul că obiectele cheie ne vor fi de obicei create de o fabrică de obiecte, care se numește **container DI** (prescurtat DIC). Da, aceasta este fabrica despre care am vorbit mai devreme. Și ne va produce și obiectul `Application`, de aceea avem nevoie mai întâi de container. Îl obținem folosind clasa `Configurator` și îl lăsăm să producă obiectul `Application`, apelăm pe el metoda `run()` și astfel pornește aplicația Nette. Exact acest lucru se întâmplă în fișierul [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -Clasa Application are o singură sarcină: să răspundă la cererea HTTP. - -Aplicațiile scrise în Nette sunt împărțite în multe așa-numite presentere (în alte framework-uri puteți întâlni termenul controller, este același lucru), care sunt clase, fiecare reprezentând o anumită pagină specifică a site-ului: de ex. homepage; produs într-un magazin online; formular de conectare; feed sitemap etc. Aplicația poate avea de la unul la mii de presentere. - -Application începe prin a solicita așa-numitului router să decidă căruia dintre presentere să îi transmită cererea curentă pentru gestionare. Routerul decide a cui este responsabilitatea. Se uită la URL-ul de intrare `https://example.com/product/123` și, pe baza modului în care este setat, decide că aceasta este treaba, de ex., a **presenterului** `Product`, de la care va dori ca **acțiune** afișarea (`show`) produsului cu `id: 123`. Perechea presenter + acțiune este o bună practică să fie scrisă separată prin două puncte ca `Product:show`. - -Deci, routerul a transformat URL-ul în perechea `Presenter:action` + parametri, în cazul nostru `Product:show` + `id: 123`. Cum arată un astfel de router puteți vedea în fișierul `app/Core/RouterFactory.php` și îl descriem detaliat în capitolul [Rutare |Routing]. - -Să mergem mai departe. Application cunoaște deja numele presenterului și poate continua. Prin crearea obiectului clasei `ProductPresenter`, care este codul presenterului `Product`. Mai precis, solicită containerului DI să creeze presenterul, deoarece crearea este treaba lui. - -Presenterul poate arăta, de exemplu, așa: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // obținem datele din model și le transmitem șablonului - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Gestionarea cererii este preluată de presenter. Și sarcina este clară: execută acțiunea `show` cu `id: 123`. Ceea ce, în limbajul presenterelor, înseamnă că se apelează metoda `renderShow()` și în parametrul `$id` primește `123`. - -Presenterul poate gestiona mai multe acțiuni, adică poate avea mai multe metode `render()`. Dar recomandăm proiectarea presenterelor cu una sau cât mai puține acțiuni. - -Deci, s-a apelat metoda `renderShow(123)`, al cărei cod este un exemplu fictiv, dar puteți vedea pe el cum se transmit datele către șablon, adică prin scrierea în `$this->template`. - -Ulterior, presenterul returnează un răspuns. Acesta poate fi o pagină HTML, o imagine, un document XML, trimiterea unui fișier de pe disc, JSON sau chiar o redirecționare către o altă pagină. Important este că, dacă nu spunem explicit cum să răspundă (ceea ce este cazul `ProductPresenter`), răspunsul va fi redarea șablonului cu pagina HTML. De ce? Deoarece în 99% din cazuri dorim să redăm un șablon, prin urmare presenterul consideră acest comportament ca fiind implicit și vrea să ne ușureze munca. Acesta este scopul Nette. - -Nu trebuie nici măcar să specificăm ce șablon să redăm, calea către acesta o deduce singur. În cazul acțiunii `show`, încearcă pur și simplu să încarce șablonul `show.latte` din directorul cu clasa `ProductPresenter`. De asemenea, încearcă să găsească layout-ul în fișierul `@layout.latte` (mai detaliat despre [găsirea șabloanelor |templates#Căutarea șabloanelor]). - -Și ulterior redă șabloanele. Astfel, sarcina presenterului și a întregii aplicații este finalizată și lucrarea este încheiată. Dacă șablonul nu ar exista, s-ar returna o pagină cu eroarea 404. Mai multe despre presentere puteți citi pe pagina [Presentere |presenters]. - -[* request-flow.svg *] - -Pentru siguranță, să încercăm să recapitulăm întregul proces cu un URL puțin diferit: - -1) URL-ul va fi `https://example.com` -2) inițializăm aplicația, se creează containerul și se rulează `Application::run()` -3) routerul decodează URL-ul ca perechea `Home:default` -4) se creează obiectul clasei `HomePresenter` -5) se apelează metoda `renderDefault()` (dacă există) -6) se redă șablonul, de ex. `default.latte` cu layout-ul, de ex. `@layout.latte` - - -Poate că v-ați întâlnit acum cu o mulțime de termeni noi, dar credem că au sens. Crearea aplicațiilor în Nette este o adevărată plăcere. - - -Șabloane -======== - -Deoarece am ajuns la subiectul șabloanelor, în Nette se utilizează sistemul de șabloane [Latte |latte:]. De aceea și extensiile `.latte` la șabloane. Latte se utilizează, pe de o parte, pentru că este cel mai sigur sistem de șabloane pentru PHP și, pe de altă parte, și cel mai intuitiv sistem. Nu trebuie să învățați multe lucruri noi, vă descurcați cu cunoștințele de PHP și câteva tag-uri. Totul veți afla [în documentație |templates]. - -În șablon se [creează linkuri |creating-links] către alți presenteri & acțiuni astfel: - -```latte -detaliu produs -``` - -Pur și simplu, în loc de URL-ul real, scrieți perechea cunoscută `Presenter:action` și specificați eventualii parametri. Trucul este în `n:href`, care spune că acest atribut va fi procesat de Nette. Și va genera: - -```latte -detaliu produs -``` - -Generarea URL-ului este responsabilitatea routerului menționat anterior. De fapt, routerele din Nette sunt excepționale prin faptul că pot efectua nu numai transformări din URL în perechea presenter:action, ci și invers, adică din numele presenterului + acțiunii + parametrilor să genereze un URL. Datorită acestui fapt, în Nette puteți schimba complet formele URL-urilor în întreaga aplicație finalizată, fără a schimba un singur caracter în șablon sau presenter. Doar prin modificarea routerului. De asemenea, datorită acestui fapt funcționează așa-numita canonizare, care este o altă caracteristică unică a Nette, care contribuie la un SEO mai bun (optimizarea găsirii pe internet) prin prevenirea automată a existenței conținutului duplicat la URL-uri diferite. Mulți programatori consideră acest lucru uimitor. - - -Componente interactive -====================== - -Despre presentere trebuie să vă mai spunem un lucru: au încorporat un sistem de componente. Ceva similar ar putea fi cunoscut de veterani din Delphi sau ASP.NET Web Forms, ceva asemănător stă la baza React sau Vue.js. În lumea framework-urilor PHP, este o caracteristică complet unică. - -Componentele sunt unități separate, reutilizabile, pe care le inserăm în pagini (adică presentere). Acestea pot fi [formulare |forms:in-presenter], [datagrid-uri |https://componette.org/contributte/datagrid/], meniuri, sondaje de votare, de fapt, orice are sens să fie folosit în mod repetat. Putem crea propriile componente sau putem folosi unele din [oferta imensă |https://componette.org] de componente open source. - -Componentele influențează fundamental abordarea creării aplicațiilor. Vă vor deschide noi posibilități de compunere a paginilor din unități pre-pregătite. Și, în plus, au ceva în comun cu [Hollywood-ul |components#Stilul Hollywood]. - - -Container DI și configurare -=========================== - -Containerul DI sau fabrica de obiecte este inima întregii aplicații. - -Nu vă faceți griji, nu este nicio cutie neagră magică, așa cum ar putea părea din rândurile anterioare. De fapt, este o clasă PHP destul de plictisitoare, pe care Nette o generează și o salvează în directorul de cache. Are o mulțime de metode numite precum `createServiceAbcd()` și fiecare dintre ele știe să creeze și să returneze un anumit obiect. Da, există și metoda `createServiceApplication()`, care creează `Nette\Application\Application`, de care aveam nevoie în fișierul `index.php` pentru a porni aplicația. Și există metode care creează presentere individuale. Și așa mai departe. - -Obiectelor pe care le creează containerul DI li se spune, din anumite motive, servicii. - -Ceea ce este cu adevărat special la această clasă este că nu o programați voi, ci framework-ul. El generează efectiv codul PHP și îl salvează pe disc. Voi doar dați instrucțiuni despre ce obiecte ar trebui să știe să creeze containerul și cum anume. Iar aceste instrucțiuni sunt scrise în [fișierele de configurare |bootstrapping#Configurarea containerului DI], pentru care se utilizează formatul [NEON|neon:format] și, prin urmare, au și extensia `.neon`. - -Fișierele de configurare servesc exclusiv pentru a instrui containerul DI. Deci, dacă, de exemplu, specific în secțiunea [session |http:configuration#Sesiune] opțiunea `expiration: 14 days`, atunci containerul DI, la crearea obiectului `Nette\Http\Session` reprezentând sesiunea, va apela metoda sa `setExpiration('14 days')` și astfel configurația devine realitate. - -Există un capitol întreg pregătit pentru voi, care descrie ce totul poate fi [configurat |nette:configuring] și cum să [definiți propriile servicii |dependency-injection:services]. - -Odată ce pătrundeți puțin în crearea serviciilor, veți întâlni cuvântul [autowiring |dependency-injection:autowiring]. Acesta este un truc care vă va simplifica viața într-un mod incredibil. Poate transmite automat obiectele acolo unde aveți nevoie de ele (de exemplu, în constructorii claselor voastre), fără a fi nevoie să faceți nimic. Veți descoperi că containerul DI din Nette este un mic miracol. - - -Unde să mergem mai departe? -=========================== - -Am parcurs principiile de bază ale aplicațiilor în Nette. Deocamdată foarte superficial, dar în curând veți pătrunde în profunzime și, în timp, veți crea aplicații web minunate. Unde să continuăm? Ați încercat deja tutorialul [Scriem prima aplicație|quickstart:]? - -Pe lângă cele descrise mai sus, Nette dispune de un întreg arsenal de [clase utile|utils:], [un strat de baze de date|database:], etc. Încercați să răsfoiți documentația. Sau [blogul|https://blog.nette.org]. Veți descoperi o mulțime de lucruri interesante. - -Sperăm ca framework-ul să vă aducă multă bucurie 💙 diff --git a/application/ro/multiplier.texy b/application/ro/multiplier.texy deleted file mode 100644 index c564bd5532..0000000000 --- a/application/ro/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: componente dinamice -******************************* - -.[perex] -Instrument pentru crearea dinamică a componentelor interactive - -Să pornim de la un exemplu tipic: avem o listă de produse într-un magazin online, iar pentru fiecare dorim să afișăm un formular pentru adăugarea produsului în coș. Una dintre variantele posibile este să încapsulăm întreaga listă într-un singur formular. O modalitate mult mai convenabilă ne oferă însă [api:Nette\Application\UI\Multiplier]. - -Multiplier permite definirea convenabilă a unei fabrici pentru mai multe componente. Funcționează pe principiul componentelor imbricate - fiecare componentă care moștenește de la [api:Nette\ComponentModel\Container] poate conține alte componente. - -.[tip] -Vezi capitolul despre [modelul de componente |components#Componente în profunzime] în documentație sau [prezentarea lui Honza Tvrdík|https://www.youtube.com/watch?v=8y3LLexWu-I]. - -Esența Multiplierului este că acționează în poziția de părinte, care își poate crea descendenții dinamic folosind un callback transmis în constructor. Vezi exemplul: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Număr produse:') - ->setRequired(); - $form->addSubmit('send', 'Adaugă în coș'); - return $form; - }); -} -``` - -Acum putem, în șablon, să lăsăm pur și simplu să se redea formularul pentru fiecare produs - și fiecare va fi într-adevăr o componentă unică. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -Argumentul transmis în tag-ul `{control}` este în formatul care spune: - -1. obține componenta `shopForm` -2. și din ea obține descendentul `$item->id` - -La prima apelare a punctului **1.** `shopForm` încă nu există, așa că se apelează fabrica sa `createComponentShopForm`. Pe componenta obținută (instanța Multiplierului) este apoi apelată fabrica formularului specific - care este funcția anonimă pe care am transmis-o Multiplierului în constructor. - -În următoarea iterație a foreach-ului, metoda `createComponentShopForm` nu va mai fi apelată (componenta există), dar deoarece căutăm un alt descendent al său (`$item->id` va fi diferit în fiecare iterație), funcția anonimă va fi apelată din nou și ne va returna un nou formular. - -Singurul lucru care rămâne de făcut este să ne asigurăm că formularul ne adaugă în coș într-adevăr produsul pe care trebuie - în prezent, formularul este complet identic pentru fiecare produs. Ne ajută proprietatea Multiplierului (și, în general, a fiecărei fabrici de componente din Nette Framework), și anume că fiecare fabrică primește ca prim argument numele componentei create. În cazul nostru, acesta va fi `$item->id`, care este exact informația de care avem nevoie. Este suficient, așadar, să modificăm ușor crearea formularului: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function ($itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Număr produse:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Adaugă în coș'); - return $form; - }); -} -``` diff --git a/application/ro/presenters.texy b/application/ro/presenters.texy deleted file mode 100644 index d14383c97a..0000000000 --- a/application/ro/presenters.texy +++ /dev/null @@ -1,500 +0,0 @@ -Presentere -********** - -
    - -Vom face cunoștință cu modul în care se scriu presenterele și șabloanele în Nette. După citire, veți ști: - -- cum funcționează un presenter -- ce sunt parametrii persistenți -- cum se redau șabloanele - -
    - -[Știm deja |how-it-works#Nette Application] că presenterul este o clasă care reprezintă o anumită pagină specifică a aplicației web, de ex. pagina de start; un produs într-un magazin online; formularul de conectare; feed-ul sitemap etc. Aplicația poate avea de la unul la mii de presentere. În alte framework-uri li se mai spune și controllere. - -De obicei, prin termenul presenter se înțelege un descendent al clasei [api:Nette\Application\UI\Presenter], care este potrivit pentru generarea interfețelor web și căruia ne vom dedica în restul acestui capitol. În sens general, un presenter este orice obiect care implementează interfața [api:Nette\Application\IPresenter]. - - -Ciclul de viață al presenterului -================================ - -Sarcina presenterului este de a gestiona cererea și de a returna un răspuns (care poate fi o pagină HTML, o imagine, o redirecționare etc.). - -Deci, la început i se transmite cererea. Nu este direct o cerere HTTP, ci obiectul [api:Nette\Application\Request], în care a fost transformată cererea HTTP cu ajutorul routerului. Cu acest obiect, de obicei, nu intrăm în contact, deoarece presenterul deleagă inteligent procesarea cererii către alte metode, pe care le vom prezenta acum. - -[* lifecycle.svg *] *** Ciclul de viață al presenterului .<> - -Imaginea reprezintă lista metodelor care sunt apelate succesiv de sus în jos, dacă există. Niciuna dintre ele nu trebuie să existe, putem avea un presenter complet gol, fără nicio metodă, și să construim pe el un site web static simplu. - - -`__construct()` ---------------- - -Constructorul nu face parte în totalitate din ciclul de viață al presenterului, deoarece este apelat în momentul creării obiectului. Dar îl menționăm datorită importanței sale. Constructorul (împreună cu [metoda inject|best-practices:inject-method-attribute]) servește la transmiterea dependențelor. - -Presenterul nu ar trebui să se ocupe de logica de business a aplicației, să scrie și să citească din baza de date, să efectueze calcule etc. Pentru asta există clase din stratul pe care îl numim model. De exemplu, clasa `ArticleRepository` poate avea responsabilitatea de a încărca și salva articole. Pentru ca presenterul să poată lucra cu ea, o primește [transmisă prin dependency injection |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Imediat după primirea cererii, se apelează metoda `startup()`. O puteți utiliza pentru inițializarea proprietăților, verificarea permisiunilor utilizatorului etc. Este necesar ca metoda să apeleze întotdeauna părintele `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -Similară cu metoda `render()`. În timp ce `render()` este destinată pregătirii datelor pentru un șablon specific care urmează să fie redat, în `action()` se procesează cererea fără legătură cu redarea șablonului. De exemplu, se procesează date, se conectează sau deconectează utilizatorul, și așa mai departe, și apoi [se redirecționează în altă parte |#Redirecționare]. - -Important este că `action()` se apelează înainte de `render()`, deci în ea putem eventual schimba cursul ulterior al evenimentelor, adică să schimbăm șablonul care va fi redat, și, de asemenea, metoda `render()` care va fi apelată. Și asta folosind `setView('jineView')`. - -Metodei i se transmit parametri din cerere. Este posibil și recomandat să se specifice tipurile parametrilor, de ex. `actionShow(int $id, ?string $slug = null)` - dacă parametrul `id` lipsește sau dacă nu este un integer, presenterul va returna [eroarea 404 |#Eroare 404 etc] și va încheia activitatea. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -Metoda procesează așa-numitele semnale, cu care ne vom familiariza în capitolul dedicat [componentelor |components#Semnal]. Este destinată în special componentelor și procesării cererilor AJAX. - -Metodei i se transmit parametri din cerere, ca în cazul `action()`, inclusiv verificarea tipului. - - -`beforeRender()` ----------------- - -Metoda `beforeRender`, așa cum sugerează și numele, se apelează înainte de fiecare metodă `render()`. Se utilizează pentru configurarea comună a șablonului, transmiterea variabilelor pentru layout și altele asemenea. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -Locul unde pregătim șablonul pentru redarea ulterioară, îi transmitem date etc. - -Metodei i se transmit parametri din cerere, ca în cazul `action()`, inclusiv verificarea tipului. - -```php -public function renderShow(int $id): void -{ - // obținem datele din model și le transmitem șablonului - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -Metoda `afterRender`, așa cum sugerează din nou numele, se apelează după fiecare metodă `render()`. Se utilizează mai degrabă excepțional. - - -`shutdown()` ------------- - -Se apelează la sfârșitul ciclului de viață al presenterului. - - -**Un sfat bun, înainte de a merge mai departe**. Presenterul, după cum se vede, poate gestiona mai multe acțiuni/view-uri, adică poate avea mai multe metode `render()`. Dar recomandăm proiectarea presenterelor cu una sau cât mai puține acțiuni. - - -Trimiterea răspunsului -====================== - -Răspunsul presenterului este, de regulă, [redarea unui șablon cu o pagină HTML|templates], dar poate fi și trimiterea unui fișier, JSON sau chiar o redirecționare către o altă pagină. - -Oricând în timpul ciclului de viață putem trimite un răspuns folosind una dintre următoarele metode și, în același timp, să încheiem presenterul: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` și `forward()` [redirecționează |#Redirecționare] -- `error()` încheie presenterul [din cauza unei erori |#Eroare 404 etc] -- `sendJson($data)` încheie presenterul și [trimite date |#Trimiterea JSON] în format JSON -- `sendTemplate()` încheie presenterul și imediat [redă șablonul |templates] -- `sendResponse($response)` încheie presenterul și trimite [un răspuns propriu |#Răspunsuri] -- `terminate()` încheie presenterul fără răspuns - -Dacă nu apelați niciuna dintre aceste metode, presenterul va trece automat la redarea șablonului. De ce? Deoarece în 99% din cazuri dorim să redăm un șablon, prin urmare presenterul consideră acest comportament ca fiind implicit și vrea să ne ușureze munca. - - -Crearea linkurilor -================== - -Presenterul dispune de metoda `link()`, cu ajutorul căreia se pot crea linkuri URL către alți presenteri. Primul parametru este presenterul & acțiunea țintă, urmate de argumentele transmise, care pot fi specificate ca array: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'cs']); -``` - -În șablon se creează linkuri către alți presenteri & acțiuni în acest mod: - -```latte -detaliu produs -``` - -Pur și simplu, în loc de URL-ul real, scrieți perechea cunoscută `Presenter:action` și specificați eventualii parametri. Trucul este în `n:href`, care spune că acest atribut va fi procesat de Latte și va genera URL-ul real. În Nette, nu trebuie să vă gândiți deloc la URL-uri, ci doar la presentere și acțiuni. - -Mai multe informații găsiți în capitolul [Crearea linkurilor URL|creating-links]. - - -Redirecționare -============== - -Pentru a trece la un alt presenter se utilizează metodele `redirect()` și `forward()`, care au o sintaxă foarte similară cu metoda [link() |#Crearea linkurilor]. - -Metoda `forward()` trece imediat la noul presenter fără redirecționare HTTP: - -```php -$this->forward('Product:show'); -``` - -Exemplu de așa-numită redirecționare temporară cu codul HTTP 302 (sau 303, dacă metoda cererii curente este POST): - -```php -$this->redirect('Product:show', $id); -``` - -Redirecționarea permanentă cu codul HTTP 301 se realizează astfel: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Către un alt URL în afara aplicației se poate redirecționa cu metoda `redirectUrl()`. Ca al doilea parametru se poate specifica codul HTTP, implicit este 302 (sau 303, dacă metoda cererii curente este POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Redirecționarea încheie imediat activitatea presenterului prin aruncarea așa-numitei excepții de terminare silențioasă `Nette\Application\AbortException`. - -Înainte de redirecționare se poate trimite un [mesaj flash |#Mesaje flash], adică mesaje care vor fi afișate în șablon după redirecționare. - - -Mesaje flash -============ - -Acestea sunt mesaje care informează de obicei despre rezultatul unei operațiuni. O caracteristică importantă a mesajelor flash este că sunt disponibile în șablon chiar și după redirecționare. Chiar și după afișare, rămân active încă 30 de secunde – de exemplu, în cazul în care utilizatorul ar reîncărca pagina din cauza unei erori de transmisie - mesajul nu dispare imediat. - -Este suficient să apelați metoda [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] și presenterul se va ocupa de transmiterea către șablon. Primul parametru este textul mesajului și al doilea parametru opțional este tipul său (error, warning, info etc.). Metoda `flashMessage()` returnează instanța mesajului flash, căreia i se pot adăuga informații suplimentare. - -```php -$this->flashMessage('Elementul a fost șters.'); -$this->redirect(/* ... */); // și redirecționăm -``` - -În șablon, aceste mesaje sunt disponibile în variabila `$flashes` ca obiecte `stdClass`, care conțin proprietățile `message` (textul mesajului), `type` (tipul mesajului) și pot conține informațiile utilizatorului menționate anterior. Le redăm, de exemplu, astfel: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Eroare 404 etc. -=============== - -Dacă cererea nu poate fi îndeplinită, de exemplu, pentru că articolul pe care dorim să îl afișăm nu există în baza de date, aruncăm eroarea 404 cu metoda `error(?string $message = null, int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -Codul HTTP al erorii poate fi transmis ca al doilea parametru, implicit este 404. Metoda funcționează aruncând excepția `Nette\Application\BadRequestException`, după care `Application` predă controlul error-presenterului. Acesta este un presenter a cărui sarcină este să afișeze o pagină care informează despre eroarea apărută. Setarea error-presenterului se face în [configurația application|configuration]. - - -Trimiterea JSON -=============== - -Exemplu de metodă-acțiune care trimite date în format JSON și încheie presenterul: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Parametrii cererii .{data-version:3.1.14} -========================================= - -Presenterul și, de asemenea, fiecare componentă obțin parametrii săi din cererea HTTP. Valoarea lor o aflați cu metoda `getParameter($name)` sau `getParameters()`. Valorile sunt șiruri de caractere sau array-uri de șiruri de caractere, sunt în esență date brute obținute direct din URL. - -Pentru mai mult confort, recomandăm accesarea parametrilor prin proprietăți. Este suficient să le marcați cu atributul `#[Parameter]`: - -```php -use Nette\Application\Attributes\Parameter; // această linie este importantă - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // trebuie să fie publică -} -``` - -Pentru proprietate, recomandăm să specificați și tipul de date (de ex. `string`), iar Nette va converti automat valoarea conform acestuia. Valorile parametrilor pot fi, de asemenea, [validate |#Validarea parametrilor]. - -La crearea unui link, valoarea parametrilor poate fi setată direct: - -```latte -click -``` - - -Parametri persistenți -===================== - -Parametrii persistenți sunt utilizați pentru a menține starea între diferite cereri. Valoarea lor rămâne aceeași chiar și după ce se face clic pe un link. Spre deosebire de datele din sesiune, acestea sunt transmise în URL. Și acest lucru se întâmplă complet automat, deci nu este necesar să le specificați explicit în `link()` sau `n:href`. - -Exemplu de utilizare? Aveți o aplicație multilingvă. Limba curentă este un parametru care trebuie să fie constant parte a URL-ului. Dar ar fi incredibil de obositor să îl specificați în fiecare link. Așa că îl faceți un parametru persistent `lang` și se va transmite singur. Minunat! - -Crearea unui parametru persistent în Nette este extrem de simplă. Este suficient să creați o proprietate publică și să o marcați cu un atribut: (anterior se folosea `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // această linie este importantă - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // trebuie să fie publică -} -``` - -Dacă `$this->lang` va avea valoarea, de exemplu, `'en'`, atunci și linkurile create folosind `link()` sau `n:href` vor conține parametrul `lang=en`. Și după ce se face clic pe link, va fi din nou `$this->lang = 'en'`. - -Pentru proprietate, recomandăm să specificați și tipul de date (de ex. `string`) și puteți specifica și o valoare implicită. Valorile parametrilor pot fi [validate |#Validarea parametrilor]. - -Parametrii persistenți sunt transmiși standard între toate acțiunile presenterului respectiv. Pentru a se transmite și între mai mulți presenteri, este necesar să fie definiți fie: - -- într-un strămoș comun, de la care moștenesc presenterele -- într-un trait, pe care presenterele îl utilizează: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -La crearea unui link, valoarea parametrului persistent poate fi modificată: - -```latte -detaliu în cehă -``` - -Sau poate fi *resetat*, adică eliminat din URL. Atunci va lua valoarea sa implicită: - -```latte -click aici -``` - - -Componente interactive -====================== - -Presenterele au încorporat un sistem de componente. Componentele sunt unități separate, reutilizabile, pe care le inserăm în presentere. Acestea pot fi [formulare |forms:in-presenter], datagrid-uri, meniuri, de fapt, orice are sens să fie folosit în mod repetat. - -Cum se inserează componentele în presenter și cum se utilizează ulterior? Acest lucru îl veți afla în capitolul [Componente |components]. Veți descoperi chiar și ce au în comun cu Hollywood-ul. - -Și de unde pot obține componente? Pe pagina [Componette |https://componette.org/search/component] găsiți componente open-source și, de asemenea, o serie de alte add-on-uri pentru Nette, pe care le-au plasat aici voluntari din comunitatea din jurul framework-ului. - - -Intrăm în profunzime -==================== - -.[tip] -Cu ceea ce am arătat până acum în acest capitol, probabil vă veți descurca complet. Următoarele rânduri sunt destinate celor care sunt interesați de presentere în profunzime și doresc să știe absolut totul. - - -Validarea parametrilor ----------------------- - -Valorile [parametrilor cererii |#Parametrii cererii] și ale [parametrilor persistenți |#Parametri persistenți] primite din URL sunt scrise în proprietăți de către metoda `loadState()`. Aceasta verifică, de asemenea, dacă tipul de date specificat la proprietate corespunde, altfel răspunde cu eroarea 404 și pagina nu se afișează. - -Nu credeți niciodată orbește în parametri, deoarece pot fi ușor suprascriși de utilizator în URL. Astfel, de exemplu, verificăm dacă limba `$this->lang` se află printre cele suportate. O cale potrivită este să suprascriem metoda menționată `loadState()`: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // aici se setează $this->lang - // urmează controlul propriu al valorii: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Salvarea și restaurarea cererii -------------------------------- - -Cererea pe care o gestionează presenterul este obiectul [api:Nette\Application\Request] și este returnată de metoda presenterului `getRequest()`. - -Cererea curentă poate fi salvată în sesiune sau, invers, restaurată din ea și lăsată presenterului să o execute din nou. Acest lucru este util, de exemplu, în situația în care utilizatorul completează un formular și îi expiră sesiunea de conectare. Pentru a nu pierde datele, înainte de redirecționarea către pagina de conectare, salvăm cererea curentă în sesiune folosind `$reqId = $this->storeRequest()`, care returnează identificatorul său sub forma unui șir scurt și îl transmitem ca parametru presenterului de conectare. - -După conectare, apelăm metoda `$this->restoreRequest($reqId)`, care preia cererea din sesiune și face forward către ea. Metoda verifică, în același timp, că cererea a fost creată de același utilizator care s-a conectat acum. Dacă s-ar conecta un alt utilizator sau cheia ar fi invalidă, nu face nimic și programul continuă. - -Consultați ghidul [Cum să reveniți la pagina anterioară |best-practices:restore-request]. - - -Canonizare ----------- - -Presenterele au o caracteristică cu adevărat grozavă, care contribuie la un SEO mai bun (optimizarea găsirii pe internet). Previn automat existența conținutului duplicat la URL-uri diferite. Dacă către o anumită țintă duc mai multe adrese URL, de ex. `/index` și `/index?page=1`, framework-ul o determină pe una dintre ele ca fiind primară (canonică) și le redirecționează pe celelalte către ea folosind codul HTTP 301. Datorită acestui fapt, motoarele de căutare nu vă indexează paginile de două ori și nu le diluează page rank-ul. - -Acest proces se numește canonizare. URL-ul canonic este cel generat de [router|routing], de regulă deci prima rută corespunzătoare din colecție. - -Canonizarea este activată implicit și poate fi dezactivată prin `$this->autoCanonicalize = false`. - -Redirecționarea nu are loc la o cerere AJAX sau POST, deoarece s-ar pierde date sau nu ar avea valoare adăugată din punct de vedere SEO. - -Canonizarea poate fi invocată și manual folosind metoda `canonicalize()`, căreia i se transmit, similar metodei `link()`, presenterul, acțiunea și parametrii. Creează un link și îl compară cu URL-ul curent. Dacă diferă, redirecționează către linkul generat. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // redirecționează, dacă $slug diferă de $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - - -Evenimente ----------- - -Pe lângă metodele `startup()`, `beforeRender()` și `shutdown()`, care sunt apelate ca parte a ciclului de viață al presenterului, pot fi definite și alte funcții care să fie apelate automat. Presenterul definește așa-numitele [evenimente |nette:glossary#Evenimente], ale căror handlere le adăugați în array-urile `$onStartup`, `$onRender` și `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Handlerele din array-ul `$onStartup` sunt apelate chiar înainte de metoda `startup()`, apoi `$onRender` între `beforeRender()` și `render()` și în final `$onShutdown` chiar înainte de `shutdown()`. - - -Răspunsuri ----------- - -Răspunsul returnat de presenter este un obiect care implementează interfața [api:Nette\Application\Response]. Există o serie de răspunsuri pregătite disponibile: - -- [api:Nette\Application\Responses\CallbackResponse] - trimite un callback -- [api:Nette\Application\Responses\FileResponse] - trimite un fișier -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - trimite JSON -- [api:Nette\Application\Responses\RedirectResponse] - redirecționare -- [api:Nette\Application\Responses\TextResponse] - trimite text -- [api:Nette\Application\Responses\VoidResponse] - răspuns gol - -Răspunsurile sunt trimise prin metoda `sendResponse()`: - -```php -use Nette\Application\Responses; - -// Text simplu -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Trimite fișier -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// Răspunsul va fi un callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - - -Restricționarea accesului folosind `#[Requires]` .{data-version:3.2.2} ----------------------------------------------------------------------- - -Atributul `#[Requires]` oferă opțiuni avansate pentru restricționarea accesului la presentere și metodele lor. Poate fi utilizat pentru specificarea metodelor HTTP, solicitarea unei cereri AJAX, restricționarea la aceeași origine (same origin) și accesul doar prin forward. Atributul poate fi aplicat atât claselor presenterelor, cât și metodelor individuale `action()`, `render()`, `handle()` și `createComponent()`. - -Puteți specifica aceste restricții: -- pentru metode HTTP: `#[Requires(methods: ['GET', 'POST'])]` -- solicitarea unei cereri AJAX: `#[Requires(ajax: true)]` -- acces doar din aceeași origine: `#[Requires(sameOrigin: true)]` -- acces doar prin forward: `#[Requires(forward: true)]` -- restricționare la acțiuni specifice: `#[Requires(actions: 'default')]` - -Detalii găsiți în ghidul [Cum se utilizează atributul Requires |best-practices:attribute-requires]. - - -Verificarea metodei HTTP ------------------------- - -Presenterele din Nette verifică automat metoda HTTP a fiecărei cereri primite. Motivul acestei verificări este în principal securitatea. Standard, sunt permise metodele `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. - -Dacă doriți să permiteți în plus, de exemplu, metoda `OPTIONS`, utilizați atributul `#[Requires]` (de la Nette Application v3.2): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -În versiunea 3.1, verificarea se face în `checkHttpMethod()`, care verifică dacă metoda specificată în cerere este inclusă în array-ul `$presenter->allowedMethods`. Adăugarea metodei se face astfel: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Este important de subliniat că, dacă permiteți metoda `OPTIONS`, trebuie ulterior să o gestionați corespunzător în cadrul presenterului dvs. Metoda este adesea utilizată ca așa-numită cerere preflight, pe care browserul o trimite automat înainte de cererea reală, când este necesar să se afle dacă cererea este permisă din punctul de vedere al politicii CORS (Cross-Origin Resource Sharing). Dacă permiteți metoda, dar nu implementați un răspuns corect, acest lucru poate duce la inconsecvențe și potențiale probleme de securitate. - - -Lectură suplimentară -==================== - -- [Metode și atribute inject |best-practices:inject-method-attribute] -- [Compunerea presenterelor din trait-uri |best-practices:presenter-traits] -- [Transmiterea setărilor către presentere |best-practices:passing-settings-to-presenters] -- [Cum să reveniți la pagina anterioară |best-practices:restore-request] diff --git a/application/ro/routing.texy b/application/ro/routing.texy deleted file mode 100644 index 0d580d0565..0000000000 --- a/application/ro/routing.texy +++ /dev/null @@ -1,721 +0,0 @@ -Rutare -****** - -
    - -Routerul se ocupă de tot ce ține de adresele URL, astfel încât să nu mai trebuiască să vă gândiți la ele. Vom arăta: - -- cum să setați routerul pentru ca URL-urile să fie conform așteptărilor -- vom vorbi despre SEO și redirecționare -- și vom arăta cum să scrieți propriul router - -
    - - -URL-urile mai prietenoase pentru oameni (sau și cool ori pretty URL) sunt mai utilizabile, mai ușor de reținut și contribuie pozitiv la SEO. Nette se gândește la asta și vine în întâmpinarea dezvoltatorilor. Puteți proiecta pentru aplicația dvs. exact structura de adrese URL pe care o doriți. Puteți chiar să o proiectați abia în momentul în care aplicația este deja finalizată, deoarece acest lucru se face fără intervenții în cod sau șabloane. Se definește într-un mod elegant într-un [singur loc |#Integrarea în aplicație], în router, și nu este astfel împrăștiat sub formă de adnotări în toți presenterele. - -Routerul din Nette este extraordinar prin faptul că este **bidirecțional.** Poate atât să decodeze URL-ul din cererea HTTP, cât și să creeze linkuri. Joacă, așadar, un rol esențial în [Nette Application |how-it-works#Nette Application], deoarece decide ce presenter și acțiune vor executa cererea curentă, dar este utilizat și pentru [generarea URL-urilor |creating-links] în șablon etc. - -Totuși, routerul nu este limitat doar la această utilizare, îl puteți folosi în aplicații unde nu se utilizează deloc presentere, pentru API-uri REST etc. Mai multe în secțiunea [#utilizare independentă]. - - -Colecție de rute -================ - -Cel mai plăcut mod de a defini forma adreselor URL în aplicație îl oferă clasa [api:Nette\Application\Routers\RouteList]. Definiția este formată dintr-o listă de așa-numite rute, adică măști de adrese URL și presenterele și acțiunile asociate acestora, folosind un API simplu. Rutele nu trebuie denumite în niciun fel. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -Exemplul spune că dacă deschidem în browser `https://domain.com/rss.xml`, se va afișa presenterul `Feed` cu acțiunea `rss`, dacă `https://domain.com/article/12`, se va afișa presenterul `Article` cu acțiunea `view` etc. În cazul în care nu se găsește o rută potrivită, Nette Application reacționează aruncând excepția [BadRequestException |api:Nette\Application\BadRequestException], care este afișată utilizatorului ca o pagină de eroare 404 Not Found. - - -Ordinea rutelor ---------------- - -**Ordinea** în care sunt specificate rutele individuale este **absolut crucială**, deoarece acestea sunt evaluate secvențial de sus în jos. Se aplică regula conform căreia declarăm rutele **de la cele specifice la cele generale**: - -```php -// GREȘIT: 'rss.xml' este capturat de prima rută și înțelege acest șir ca -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// CORECT -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Rutele sunt evaluate de sus în jos și la generarea linkurilor: - -```php -// GREȘIT: linkul către 'Feed:rss' va fi generat ca 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// CORECT -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Nu vom ascunde faptul că asamblarea corectă a rutelor necesită o anumită abilitate. Până când veți pătrunde în ea, [panoul de rutare |#Depanarea routerului] vă va fi un ajutor util. - - -Mască și parametri ------------------- - -Masca descrie calea relativă de la directorul rădăcină al site-ului. Cea mai simplă mască este un URL static: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Adesea, măștile conțin așa-numiții **parametri**. Aceștia sunt specificați între paranteze unghiulare (de ex. ``) și sunt transmiși către presenterul țintă, de exemplu metodei `renderShow(int $year)` sau parametrului persistent `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Exemplul spune că dacă deschidem în browser `https://example.com/chronicle/2020`, se va afișa presenterul `History` cu acțiunea `show` și parametrul `year: 2020`. - -Parametrilor le putem specifica o valoare implicită direct în mască și astfel devin opționali: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Ruta va accepta acum și URL-ul `https://example.com/chronicle/`, care va afișa din nou `History:show` cu parametrul `year: 2020`. - -Parametrul poate fi, desigur, și numele presenterului și al acțiunii. De exemplu, așa: - -```php -$router->addRoute('/', 'Home:default'); -``` - -Ruta specificată acceptă, de ex., URL-uri de forma `/article/edit` sau `/catalog/list` și le înțelege ca presentere și acțiuni `Article:edit` și `Catalog:list`. - -În același timp, atribuie parametrilor `presenter` și `action` valorile implicite `Home` și `default` și sunt, prin urmare, și opționali. Deci, ruta acceptă și URL-uri de forma `/article` și le înțelege ca `Article:default`. Sau invers, un link către `Product:default` va genera calea `/product`, un link către `Home:default` implicit va genera calea `/`. - -Masca poate descrie nu numai calea relativă de la directorul rădăcină al site-ului, ci și calea absolută, dacă începe cu un slash, sau chiar întregul URL absolut, dacă începe cu două slash-uri: - -```php -// relativ la document root -$router->addRoute('/', /* ... */); - -// cale absolută (relativă la domeniu) -$router->addRoute('//', /* ... */); - -// URL absolut inclusiv domeniul (relativ la schemă) -$router->addRoute('//.example.com//', /* ... */); - -// URL absolut inclusiv schema -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Expresii de validare --------------------- - -Pentru fiecare parametru se poate stabili o condiție de validare folosind o [expresie regulată|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. De exemplu, pentru parametrul `id` specificăm că poate lua doar cifre folosind expresia regulată `\d+`: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -Expresia regulată implicită pentru toți parametrii este `[^/]+`, adică totul cu excepția slash-ului. Dacă un parametru trebuie să accepte și slash-uri, specificăm expresia `.+`: - -```php -// acceptă https://example.com/a/b/c, path va fi 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Secvențe opționale ------------------- - -În mască se pot marca părți opționale folosind paranteze drepte. Orice parte a măștii poate fi opțională, în ea se pot afla și parametri: - -```php -$router->addRoute('[/]', /* ... */); - -// Acceptă căi: -// /cs/download => lang => cs, name => download -// /download => lang => null, name => download -``` - -Când un parametru face parte dintr-o secvență opțională, devine, desigur, și el opțional. Dacă nu are specificată o valoare implicită, atunci va fi null. - -Părțile opționale pot fi și în domeniu: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Secvențele pot fi imbricate și combinate liber: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Acceptă căi: -// /cs/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -La generarea URL-ului, se urmărește varianta cea mai scurtă, deci tot ce poate fi omis se omite. De aceea, de exemplu, ruta `index[.html]` generează calea `/index`. Inversarea comportamentului este posibilă prin specificarea unui semn de exclamare după paranteza dreaptă de deschidere: - -```php -// acceptă /hello și /hello.html, generează /hello -$router->addRoute('[.html]', /* ... */); - -// acceptă /hello și /hello.html, generează /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Parametrii opționali (adică parametrii care au o valoare implicită) fără paranteze drepte se comportă în esență ca și cum ar fi încadrați în paranteze în felul următor: - -```php -$router->addRoute('//', /* ... */); - -// corespunde acestuia: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Dacă am dori să influențăm comportamentul slash-ului final, astfel încât, de ex., în loc de `/home/` să se genereze doar `/home`, acest lucru se poate realiza astfel: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Substituenți ------------- - -În masca căii absolute putem folosi următorii substituenți și evita astfel, de ex., necesitatea de a scrie în mască domeniul, care se poate diferenția în mediul de dezvoltare și cel de producție: - -- `%tld%` = top level domain, de ex. `com` sau `org` -- `%sld%` = second level domain, de ex. `example` -- `%domain%` = domeniu fără subdomenii, de ex. `example.com` -- `%host%` = întregul host, de ex. `www.example.com` -- `%basePath%` = calea către directorul rădăcină - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Pentru o specificație mai detaliată, se poate utiliza o formă și mai extinsă, unde, pe lângă valorile implicite, putem seta și alte proprietăți ale parametrilor, cum ar fi expresia regulată de validare (vezi parametrul `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Este important de menționat că, dacă parametrii definiți în array nu sunt specificați în masca căii, valorile lor nu pot fi modificate, nici prin parametrii query specificați după semnul întrebării în URL. - - -Filtre și traduceri -------------------- - -Codul sursă al aplicației îl scriem în engleză, dar dacă site-ul trebuie să aibă URL-uri în română, atunci rutarea simplă de tipul: - -```php -$router->addRoute('/', 'Home:default'); -``` - -va genera URL-uri în engleză, cum ar fi `/product/123` sau `/cart`. Dacă dorim ca presenterele și acțiunile să fie reprezentate în URL prin cuvinte românești (de ex. `/produs/123` sau `/cos`), putem utiliza un dicționar de traducere. Pentru scrierea sa avem nevoie deja de varianta "mai vorbăreață" a celui de-al doilea parametru: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // șir în URL => presenter - 'produs' => 'Product', - 'cos' => 'Cart', - 'catalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'lista' => 'list', - ], - ], -]); -``` - -Mai multe chei ale dicționarului de traducere pot duce la același presenter. Astfel se creează diferite aliasuri pentru acesta. Ca variantă canonică (adică cea care va fi în URL-ul generat) se consideră ultima cheie. - -Tabelul de traducere poate fi utilizat în acest mod pentru orice parametru. În același timp, dacă traducerea nu există, se ia valoarea originală. Acest comportament poate fi schimbat prin adăugarea `Route::FilterStrict => true` și ruta va respinge apoi URL-ul dacă valoarea nu se află în dicționar. - -Pe lângă dicționarul de traducere sub formă de array, se pot implementa și funcții de traducere proprii. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -Funcția `Route::FilterIn` convertește între parametrul din URL și șirul care este apoi transmis presenterului, funcția `FilterOut` asigură conversia în sens invers. - -Parametrii `presenter`, `action` și `module` au deja filtre predefinite care convertesc între stilul PascalCase respectiv camelCase și kebab-case utilizat în URL. Valoarea implicită a parametrilor se scrie deja în forma transformată, deci, de exemplu, în cazul presenterului scriem ``, nu ``. - - -Filtre generale ---------------- - -Pe lângă filtrele destinate parametrilor specifici, putem defini și filtre generale, care primesc un array asociativ al tuturor parametrilor, pe care îi pot modifica în orice mod și apoi îi returnează. Filtrele generale le definim sub cheia `null`. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Filtrele generale oferă posibilitatea de a modifica comportamentul rutei în absolut orice mod. Le putem folosi, de exemplu, pentru modificarea parametrilor pe baza altor parametri. De exemplu, traducerea `` și `` pe baza valorii curente a parametrului ``. - -Dacă un parametru are definit un filtru propriu și, în același timp, există un filtru general, se execută filtrul propriu `FilterIn` înainte de cel general și, invers, filtrul general `FilterOut` înainte de cel propriu. Deci, în interiorul filtrului general, valorile parametrilor `presenter` respectiv `action` sunt scrise în stilul PascalCase respectiv camelCase. - - -Rute unidirecționale (OneWay) ------------------------------ - -Rutele unidirecționale sunt utilizate pentru a menține funcționalitatea URL-urilor vechi, pe care aplicația nu le mai generează, dar le acceptă în continuare. Le marcăm cu flag-ul `OneWay`: - -```php -// URL vechi /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); -// URL nou /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -La accesarea URL-ului vechi, presenterul redirecționează automat către noul URL, astfel încât motoarele de căutare nu vă vor indexa aceste pagini de două ori (vezi [#SEO și canonizare]). - - -Rutare dinamică cu callback-uri -------------------------------- - -Rutarea dinamică cu callback-uri vă permite să atribuiți rutelor direct funcții (callback-uri), care se execută atunci când calea respectivă este vizitată. Această funcționalitate flexibilă vă permite să creați rapid și eficient diferite puncte finale (endpoints) pentru aplicația dvs.: - -```php -$router->addRoute('test', function () { - echo 'sunteți la adresa /test'; -}); -``` - -Puteți defini, de asemenea, parametri în mască, care se vor transmite automat către callback-ul dvs.: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Bun venit la versiunea română a site-ului nostru!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - - -Module ------- - -Dacă avem mai multe rute care aparțin unui [modul |directory-structure#Presentere și șabloane] comun, utilizăm `withModule()`: - -```php -$router = new RouteList; -$router->withModule('Forum') // următoarele rute fac parte din modulul Forum - ->addRoute('rss', 'Feed:rss') // presenterul va fi Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // următoarele rute fac parte din modulul Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -O alternativă este utilizarea parametrului `module`: - -```php -// URL manage/dashboard/default se mapează pe presenterul Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - - -Subdomenii ----------- - -Colecțiile de rute le putem împărți după subdomenii: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -În numele domeniului se pot folosi și [#substituenți]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Prefix de cale --------------- - -Colecțiile de rute le putem împărți după calea din URL: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // prinde URL /eshop/rss - ->addRoute('/'); // prinde URL /eshop// -``` - - -Combinații ----------- - -Împărțirile menționate mai sus pot fi combinate între ele: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Parametri query ---------------- - -Măștile pot conține și parametri query (parametri după semnul întrebării în URL). Acestora nu li se poate defini o expresie de validare, dar li se poate schimba numele sub care sunt transmiși presenterului: - -```php -// parametrul query 'cat' dorim să îl folosim în aplicație sub numele 'categoryId' -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Parametri Foo -------------- - -Acum intrăm mai în profunzime. Parametrii Foo sunt în esență parametri nedenumiți care permit potrivirea unei expresii regulate. Un exemplu este o rută care acceptă `/index`, `/index.html`, `/index.htm` și `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -Se poate, de asemenea, defini explicit șirul care va fi utilizat la generarea URL-ului. Șirul trebuie plasat direct după semnul întrebării. Următoarea rută este similară cu cea anterioară, dar generează `/index.html` în loc de `/index`, deoarece șirul `.html` este setat ca valoare de generare: - -```php -$router->addRoute('index', /* ... */); -``` - - -Integrarea în aplicație -======================= - -Pentru a integra routerul creat în aplicație, trebuie să îi spunem despre el containerului DI. Cea mai ușoară cale este să pregătim o fabrică care va produce obiectul routerului și să comunicăm în configurația containerului că trebuie să o folosească. Să presupunem că în acest scop scriem metoda `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -În [configurație |dependency-injection:services] scriem apoi: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Orice dependențe, de exemplu de baza de date etc., sunt transmise metodei fabricii ca parametri ai săi folosind [autowiring-ul|dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Un router mult mai simplu decât colecția de rute este [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Îl folosim atunci când nu avem cerințe speciale privind forma URL-ului, dacă nu este disponibil `mod_rewrite` (sau alternativele sale) sau dacă deocamdată nu dorim să ne ocupăm de URL-uri frumoase. - -Generează adrese aproximativ în această formă: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Parametrul constructorului SimpleRouter este presenterul & acțiunea implicită către care trebuie direcționat dacă deschidem pagina fără parametri, de ex. `http://example.com/`. - -```php -// presenterul implicit va fi 'Home' și acțiunea 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Recomandăm definirea directă a SimpleRouter în [configurație |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO și canonizare -================= - -Framework-ul contribuie la SEO (optimizarea găsirii pe internet) prin prevenirea duplicării conținutului la URL-uri diferite. Dacă către o anumită țintă duc mai multe adrese, de ex. `/index` și `/index.html`, framework-ul o determină pe prima dintre ele ca fiind primară (canonică) și le redirecționează pe celelalte către ea folosind codul HTTP 301. Datorită acestui fapt, motoarele de căutare nu vă indexează paginile de două ori și nu le diluează page rank-ul. - -Acest proces se numește canonizare. URL-ul canonic este cel generat de router, adică prima rută corespunzătoare din colecție fără flag-ul OneWay. De aceea, în colecție specificăm **rutele primare primele**. - -Canonizarea este efectuată de presenter, mai multe în capitolul [canonizare |presenters#Canonizare]. - - -HTTPS -===== - -Pentru a putea utiliza protocolul HTTPS, este necesar să îl activați pe hosting și să configurați corect serverul. - -Redirecționarea întregului site către HTTPS trebuie setată la nivelul serverului, de exemplu folosind fișierul .htaccess în directorul rădăcină al aplicației noastre, și anume cu codul HTTP 301. Setarea poate varia în funcție de hosting și arată aproximativ așa: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Routerul generează URL-uri cu același protocol cu care a fost încărcată pagina, deci nu este nevoie să setați nimic în plus. - -Dacă însă, în mod excepțional, avem nevoie ca diferite rute să ruleze sub protocoale diferite, îl specificăm în masca rutei: - -```php -// Va genera adresa cu HTTP -$router->addRoute('http://%host%//', /* ... */); - -// Va genera adresa cu HTTPs -$router->addRoute('https://%host%//', /* ... */); -``` - - -Depanarea routerului -==================== - -Panoul de rutare afișat în [Tracy Bar |tracy:] este un ajutor util, care afișează lista rutelor și, de asemenea, parametrii pe care routerul i-a obținut din URL. - -Bara verde cu simbolul ✓ reprezintă ruta care a procesat URL-ul curent, cu albastru și simbolul ≈ sunt marcate rutele care ar procesa și ele URL-ul, dacă cea verde nu le-ar fi devansat. În continuare vedem presenterul & acțiunea curentă. - -[* routing-debugger.webp *] - -În același timp, dacă are loc o redirecționare neașteptată din cauza [canonizării |#SEO și canonizare], este util să vă uitați în panoul din bara *redirect*, unde veți afla cum a înțeles routerul inițial URL-ul și de ce a redirecționat. - -.[note] -La depanarea routerului, recomandăm deschiderea în browser a Developer Tools (Ctrl+Shift+I sau Cmd+Option+I) și în panoul Network dezactivarea cache-ului, pentru a nu se salva în el redirecționările. - - -Performanță -=========== - -Numărul de rute influențează viteza routerului. Numărul lor nu ar trebui să depășească în niciun caz câteva zeci. Dacă site-ul dvs. are o structură URL prea complicată, puteți scrie un [#router personalizat]. - -Dacă routerul nu are dependențe, de exemplu de baza de date, și fabrica sa nu acceptă niciun argument, putem serializa forma sa compilată direct în containerul DI și astfel accelera ușor aplicația. - -```neon -routing: - cache: true -``` - - -Router personalizat -=================== - -Următoarele rânduri sunt destinate utilizatorilor foarte avansați. Puteți crea un router propriu și să îl integrați complet natural în colecția de rute. Routerul este o implementare a interfeței [api:Nette\Routing\Router] cu două metode: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -Metoda `match` procesează cererea curentă [$httpRequest |http:request], din care se poate obține nu numai URL-ul, ci și antetele etc., într-un array care conține numele presenterului și parametrii săi. Dacă nu poate procesa cererea, returnează null. La procesarea cererii, trebuie să returnăm cel puțin presenterul și acțiunea. Numele presenterului este complet și conține și eventualele module: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -Metoda `constructUrl`, dimpotrivă, asamblează din array-ul de parametri URL-ul absolut rezultat. Pentru aceasta poate utiliza informații din parametrul [`$refUrl`|api:Nette\Http\UrlScript], care este URL-ul curent. - -În colecția de rute îl adăugați folosind `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Utilizare independentă -====================== - -Prin utilizare independentă înțelegem utilizarea capacităților routerului într-o aplicație care nu utilizează Nette Application și presentere. Se aplică aproape tot ce am arătat în acest capitol, cu aceste diferențe: - -- pentru colecții de rute folosim clasa [api:Nette\Routing\RouteList] -- ca simple router clasa [api:Nette\Routing\SimpleRouter] -- deoarece nu există perechea `Presenter:action`, folosim [notația extinsă |#Notație extinsă] - -Deci, din nou, creăm o metodă care ne va asambla routerul, de ex.: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Dacă utilizați un container DI, ceea ce recomandăm, din nou adăugăm metoda în configurație și apoi obținem routerul împreună cu cererea HTTP din container: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Sau creăm obiectele direct: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Acum rămâne doar să punem routerul la treabă: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // nu a fost găsită o rută corespunzătoare, trimitem eroarea 404 - exit; -} - -// procesăm parametrii obținuți -$controller = $params['controller']; -// ... -``` - -Și invers, folosim routerul pentru a asambla un link: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/router}} diff --git a/application/ro/templates.texy b/application/ro/templates.texy deleted file mode 100644 index 206ecd3fa6..0000000000 --- a/application/ro/templates.texy +++ /dev/null @@ -1,323 +0,0 @@ -Șabloane -******** - -.[perex] -Nette utilizează sistemul de șabloane [Latte |latte:]. Pe de o parte, pentru că este cel mai sigur sistem de șabloane pentru PHP și, pe de altă parte, și cel mai intuitiv sistem. Nu trebuie să învățați multe lucruri noi, vă descurcați cu cunoștințele de PHP și câteva tag-uri. - -Este obișnuit ca pagina să fie compusă dintr-un șablon de layout + șablonul acțiunii respective. Așa poate arăta, de exemplu, un șablon de layout, observați blocurile `{block}` și tag-ul `{include}`: - -```latte - - - - {block title}My App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -Și acesta va fi șablonul acțiunii: - -```latte -{block title}Homepage{/block} - -{block content} -

    Homepage

    -... -{/block} -``` - -Acesta definește blocul `content`, care se va insera în locul `{include content}` din layout, și, de asemenea, re-definește blocul `title`, care va suprascrie `{block title}` din layout. Încercați să vă imaginați rezultatul. - - -Căutarea șabloanelor --------------------- - -Nu trebuie să specificați în presentere ce șablon trebuie redat, framework-ul deduce singur calea și vă scutește de scris. - -Dacă utilizați o structură de directoare unde fiecare presenter are propriul director, plasați pur și simplu șablonul în acest director sub numele acțiunii (resp. view), adică pentru acțiunea `default` utilizați șablonul `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Dacă utilizați o structură unde presenterele sunt împreună într-un singur director și șabloanele în folderul `templates`, salvați-l fie în fișierul `..latte`, fie `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← prima variantă - └── Home/ - └── default.latte ← a doua variantă -\-- - -Directorul `templates` poate fi plasat și cu un nivel mai sus, adică la același nivel cu directorul cu clasele presenterelor. - -Dacă șablonul nu este găsit, presenterul răspunde cu [eroarea 404 - page not found |presenters#Eroare 404 etc]. - -View-ul îl schimbați folosind `$this->setView('jineView')`. De asemenea, se poate specifica direct fișierul cu șablonul folosind `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Fișierele unde se caută șabloanele pot fi modificate prin suprascrierea metodei [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], care returnează un array de nume posibile de fișiere. - - -Căutarea șablonului de layout ------------------------------ - -Nette caută automat și fișierul cu layout-ul. - -Dacă utilizați o structură de directoare unde fiecare presenter are propriul director, plasați layout-ul fie în folderul cu presenterul, dacă este specific doar pentru el, fie cu un nivel mai sus, dacă este comun pentru mai mulți presenteri: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← layout comun - └── Home/ - ├── @layout.latte ← doar pentru presenterul Home - ├── HomePresenter.php - └── default.latte -\-- - -Dacă utilizați o structură unde presenterele sunt împreună într-un singur director și șabloanele în folderul `templates`, layout-ul va fi așteptat în aceste locații: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← layout comun - ├── Home.@layout.latte ← doar pentru Home, prima variantă - └── Home/ - └── @layout.latte ← doar pentru Home, a doua variantă -\-- - -Dacă presenterul se află într-un modul, se va căuta și la niveluri de directoare superioare, în funcție de imbricarea modulului. - -Numele layout-ului poate fi schimbat folosind `$this->setLayout('layoutAdmin')` și atunci se va aștepta în fișierul `@layoutAdmin.latte`. De asemenea, se poate specifica direct fișierul cu șablonul layout-ului folosind `$this->setLayout('/path/to/template.latte')`. - -Folosind `$this->setLayout(false)` sau tag-ul `{layout none}` în interiorul șablonului, căutarea layout-ului se dezactivează. - -.[note] -Fișierele unde se caută șabloanele de layout pot fi modificate prin suprascrierea metodei [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], care returnează un array de nume posibile de fișiere. - - -Variabile în șablon -------------------- - -Variabilele le transmitem șablonului scriindu-le în `$this->template` și apoi le avem disponibile în șablon ca variabile locale: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Astfel de simplu putem transmite șabloanelor orice variabile. Însă, la dezvoltarea aplicațiilor robuste, este mai util să ne limităm. De exemplu, prin definirea explicită a listei de variabile pe care șablonul le așteaptă și a tipurilor lor. Datorită acestui fapt, PHP ne va putea verifica tipurile, IDE-ul ne va sugera corect și analiza statică va dezvălui erorile. - -Și cum definim o astfel de listă? Simplu, sub forma unei clase și a proprietăților sale. O numim similar cu presenterul, doar cu `Template` la sfârșit: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // și alte variabile -} -``` - -Obiectul `$this->template` din presenter va fi acum o instanță a clasei `ArticleTemplate`. Deci, PHP va verifica tipurile declarate la scriere. Și începând cu versiunea PHP 8.2 va avertiza și la scrierea într-o variabilă inexistentă, în versiunile anterioare se poate obține același lucru folosind trait-ul [Nette\SmartObject |utils:smartobject]. - -Adnotarea `@property-read` este destinată IDE-ului și analizei statice, datorită ei va funcționa sugerarea, vezi "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -De luxul sugerării vă puteți bucura și în șabloane, este suficient să instalați în PhpStorm plugin-ul pentru Latte și să specificați la începutul șablonului numele clasei, mai multe în articolul "Latte: cum să folosiți sistemul de tipuri":https://blog.nette.org/ro/latte-how-to-use-type-system: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Astfel funcționează și șabloanele în componente, este suficient doar să respectați convenția de nume și pentru componenta, de ex. `FifteenControl`, să creați clasa șablonului `FifteenTemplate`. - -Dacă aveți nevoie să creați `$template` ca instanță a altei clase, utilizați metoda `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Variabile implicite -------------------- - -Presenterele și componentele transmit automat către șabloane câteva variabile utile: - -- `$basePath` este calea URL absolută către directorul rădăcină (de ex. `/eshop`) -- `$baseUrl` este URL-ul absolut către directorul rădăcină (de ex. `http://localhost/eshop`) -- `$user` este obiectul [reprezentând utilizatorul |security:authentication] -- `$presenter` este presenterul curent -- `$control` este componenta sau presenterul curent -- `$flashes` array de [mesaje |presenters#Mesaje flash] trimise de funcția `flashMessage()` - -Dacă utilizați propria clasă de șablon, aceste variabile se transmit dacă creați proprietăți pentru ele. - - -Crearea linkurilor ------------------- - -În șablon se creează linkuri către alți presenteri & acțiuni în acest mod: - -```latte -detaliu produs -``` - -Atributul `n:href` este foarte util pentru tag-urile HTML ``. Dacă dorim să afișăm linkul în altă parte, de exemplu în text, folosim `{link}`: - -```latte -Adresa este: {link Home:default} -``` - -Mai multe informații găsiți în capitolul [Crearea linkurilor URL|creating-links]. - - -Filtre personalizate, tag-uri etc. ----------------------------------- - -Sistemul de șabloane Latte poate fi extins cu filtre, funcții, tag-uri etc. personalizate. Acest lucru se poate face direct în metoda `render` sau `beforeRender()`: - -```php -public function beforeRender(): void -{ - // adăugarea unui filtru - $this->template->addFilter('foo', /* ... */); - - // sau configurăm direct obiectul Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -Latte în versiunea 3 oferă o modalitate mai avansată și anume crearea unei [extensii |latte:extending-latte#Latte Extension] pentru fiecare proiect web. Un exemplu fragmentar al unei astfel de clase: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -O înregistrăm folosind [configurația |configuration#Șabloane Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Traducere ---------- - -Dacă programați o aplicație multilingvă, probabil veți avea nevoie să afișați unele texte din șablon în diferite limbi. Nette Framework definește în acest scop o interfață pentru traducere [api:Nette\Localization\Translator], care are o singură metodă `translate()`. Aceasta primește mesajul `$message`, care de obicei este un șir de caractere, și orice alți parametri. Sarcina este de a returna șirul tradus. În Nette nu există nicio implementare implicită, puteți alege în funcție de nevoile dvs. dintre mai multe soluții gata făcute, pe care le găsiți pe [Componette |https://componette.org/search/localization]. În documentația lor veți afla cum să configurați translatorul. - -Șabloanelor li se poate seta un traducător, pe care îl [primim transmis |dependency-injection:passing-dependencies], prin metoda `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Translatorul poate fi setat alternativ folosind [configurația |configuration#Șabloane Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Apoi, traducătorul poate fi utilizat, de exemplu, ca filtru `|translate`, inclusiv cu parametri suplimentari, care sunt transmiși metodei `translate()` (vezi `foo, bar`): - -```latte -{='Coș'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Sau ca tag cu underscore: - -```latte -{_'Coș'} -{_$item} -{_$item, foo, bar} -``` - -Pentru traducerea unei secțiuni a șablonului există un tag pereche `{translate}` (de la Latte 2.11, anterior se folosea tag-ul `{_}`): - -```latte -{translate}Comandă{/translate} -{translate foo, bar}Comandă{/translate} -``` - -Translatorul este apelat standard în timpul rulării la redarea șablonului. Latte versiunea 3, însă, poate traduce toate textele statice deja în timpul compilării șablonului. Astfel se economisește performanță, deoarece fiecare șir se traduce o singură dată și traducerea rezultată se scrie în forma compilată. În directorul cu cache se creează astfel mai multe versiuni compilate ale șablonului, una pentru fiecare limbă. Pentru aceasta este suficient doar să specificați limba ca al doilea parametru: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Prin text static se înțelege, de exemplu, `{_'hello'}` sau `{translate}hello{/translate}`. Textele nestatice, cum ar fi `{_$foo}`, se vor traduce în continuare în timpul rulării. diff --git a/application/sl/@home.texy b/application/sl/@home.texy deleted file mode 100644 index ead8f24c66..0000000000 --- a/application/sl/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application je jedro ogrodja Nette, ki prinaša zmogljiva orodja za ustvarjanje sodobnih spletnih aplikacij. Ponuja vrsto izjemnih lastnosti, ki znatno olajšajo razvoj ter izboljšajo varnost in vzdržljivost kode. - - -Namestitev ----------- - -Knjižnico prenesete in namestite z orodjem [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Zakaj izbrati Nette Application? --------------------------------- - -Nette je bil vedno pionir na področju spletnih tehnologij. - -**Dvosmerni usmerjevalnik (Router):** Nette ima napreden sistem usmerjanja, ki je edinstven po svoji dvosmernosti - ne samo da prevaja URL-je v akcije aplikacije, ampak lahko tudi povratno generira URL naslove. To pomeni, da: -- Lahko kadarkoli spremenite strukturo URL-jev celotne aplikacije brez potrebe po urejanju predlog -- URL-ji so samodejno kanonizirani, kar izboljšuje SEO -- Usmerjanje je definirano na enem mestu, ne pa razpršeno v anotacijah - -**Komponente in signali:** Vgrajen komponentni sistem, navdihnjen z Delphi in React.js, je med PHP ogrodji popolnoma izjemen: -- Omogoča ustvarjanje ponovno uporabnih UI elementov -- Podpira hierarhično sestavljanje komponent -- Ponuja elegantno obdelavo AJAX zahtev s pomočjo signalov -- Bogata knjižnica pripravljenih komponent na [Componette](https://componette.org) - -**AJAX in odrezki (snippets):** Nette je predstavil revolucionaren način dela z AJAX-om že leta 2009, dolgo pred podobnimi rešitvami, kot sta Hotwire za Ruby on Rails ali Symfony UX Turbo: -- Odrezki omogočajo posodabljanje samo delov strani brez potrebe po pisanju JavaScripta -- Samodejna integracija s komponentnim sistemom -- Pametna invalidacija delov strani -- Minimalna količina prenesenih podatkov - -**Intuitivne predloge [Latte|latte:]:** Najvarnejši sistem predlog za PHP z naprednimi funkcijami: -- Samodejna zaščita pred XSS s kontekstno občutljivim ubežanjem znakov -- Razširljivost s pomočjo lastnih filtrov, funkcij in značk -- Dedovanje predlog in odrezki za AJAX -- Odlična podpora za PHP 8.x s sistemom tipov - -**Dependency Injection:** Nette v celoti izkorišča Dependency Injection: -- Samodejno posredovanje odvisnosti (autowiring) -- Konfiguracija s pomočjo preglednega formata NEON -- Podpora za tovarne komponent - - -Glavne prednosti ----------------- - -- **Varnost**: Samodejna obramba pred [ranljivostmi|nette:vulnerability-protection] kot so XSS, CSRF itd. -- **Produktivnost**: Manj pisanja, več funkcij zahvaljujoč pametnemu načrtovanju -- **Razhroščevanje**: [Tracy razhroščevalnik|tracy:] z usmerjevalno ploščo -- **Zmogljivost**: Pameten predpomnilnik, leno nalaganje komponent -- **Fleksibilnost**: Enostavna prilagoditev URL-jev tudi po zaključku aplikacije -- **Komponente**: Edinstven sistem ponovno uporabnih UI elementov -- **Sodobno**: Polna podpora za PHP 8.4+ in sistem tipov - - -Prvi koraki ------------ - -1. [Kako delujejo aplikacije? |how-it-works] - Razumevanje osnovne arhitekture -2. [Presenterji |presenters] - Delo s presenterji in akcijami -3. [Predloge |templates] - Ustvarjanje predlog v Latte -4. [Usmerjanje |routing] - Konfiguracija URL naslovov -5. [Interaktivne komponente |components] - Uporaba komponentnega sistema - - -Združljivost s PHP ------------------- - -| različica | združljivo s 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 - -Velja za zadnjo patch različico. diff --git a/application/sl/@left-menu.texy b/application/sl/@left-menu.texy deleted file mode 100644 index 5e3ed359c7..0000000000 --- a/application/sl/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Kako delujejo aplikacije? |how-it-works] -- [Bootstrapping] -- [Presenterji |presenters] -- [Predloge |templates] -- [Struktura imenikov |directory-structure] -- [Usmerjanje |routing] -- [Ustvarjanje URL povezav |creating-links] -- [Interaktivne komponente |components] -- [AJAX & odrezki |ajax] -- [Multiplier |multiplier] -- [Konfiguracija |configuration] - - -Nadaljnje branje -**************** -- [Zakaj uporabljati Nette? |www:10-reasons-why-nette] -- [Namestitev |nette:installation] -- [Napišimo prvo aplikacijo! |quickstart:] -- [Navodila in postopki |best-practices:] -- [Reševanje težav |nette:troubleshooting] diff --git a/application/sl/@meta.texy b/application/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/application/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/application/sl/ajax.texy b/application/sl/ajax.texy deleted file mode 100644 index b64566c406..0000000000 --- a/application/sl/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & odrezki -************** - -
    - -V dobi sodobnih spletnih aplikacij, kjer je funkcionalnost pogosto razdeljena med strežnikom in brskalnikom, je AJAX nujen povezovalni element. Kakšne možnosti nam na tem področju ponuja Nette Framework? -- pošiljanje delov predloge, t.i. odrezkov -- posredovanje spremenljivk med PHP in JavaScriptom -- orodja za razhroščevanje AJAX zahtevkov - -
    - - -AJAX zahtevek -============= - -AJAX zahtevek se v bistvu ne razlikuje od klasičnega HTTP zahtevka. Pokliče se presenter z določenimi parametri. Od presenterja pa je odvisno, kako se bo na zahtevek odzval - lahko vrne podatke v formatu JSON, pošlje del HTML kode, XML dokument itd. - -Na strani brskalnika inicializiramo AJAX zahtevek s funkcijo `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // obdelava odgovora -}); -``` - -Na strani strežnika prepoznamo AJAX zahtevek z metodo `$httpRequest->isAjax()` storitve [enkapsulirajoč HTTP zahtevek |http:request]. Za zaznavanje uporablja HTTP glavo `X-Requested-With`, zato je pomembno, da jo pošiljamo. V okviru presenterja lahko uporabimo metodo `$this->isAjax()`. - -Če želite poslati podatke v formatu JSON, uporabite metodo [`sendJson()` |presenters#Pošiljanje odgovora]. Metoda prav tako zaključi delovanje presenterja. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Če nameravate odgovoriti s posebno predlogo, namenjeno za AJAX, lahko to storite na naslednji način: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Odrezki -======= - -Najmočnejše sredstvo, ki ga Nette ponuja za povezovanje strežnika s klientom, so odrezki. Zahvaljujoč njim lahko iz navadne aplikacije naredite AJAX aplikacijo z minimalnim naporom in nekaj vrsticami kode. Kako vse skupaj deluje, prikazuje primer Fifteen, katerega kodo najdete na [GitHubu |https://github.com/nette-examples/fifteen]. - -Odrezki omogočajo posodabljanje samo delov strani, namesto da bi se celotna stran ponovno nalagala. To ni samo hitrejše in učinkovitejše, ampak zagotavlja tudi udobnejšo uporabniško izkušnjo. Odrezki vas lahko spominjajo na Hotwire za Ruby on Rails ali Symfony UX Turbo. Zanimivo je, da je Nette predstavil odrezke že 14 let prej. - -Kako odrezki delujejo? Ob prvem nalaganju strani (ne-AJAX zahtevek) se naloži celotna stran, vključno z vsemi odrezki. Ko uporabnik interagira s stranjo (npr. klikne na gumb, pošlje obrazec itd.), se namesto nalaganja celotne strani sproži AJAX zahtevek. Koda v presenterju izvede akcijo in odloči, katere odrezke je treba posodobiti. Nette te odrezke izriše in jih pošlje v obliki polja v formatu JSON. Obdelovalna koda v brskalniku prejete odrezke vstavi nazaj v stran. Prenaša se torej samo koda spremenjenih odrezkov, kar prihrani pasovno širino in pospeši nalaganje v primerjavi s prenosom vsebine celotne strani. - - -Naja ----- - -Za obdelavo odrezkov na strani brskalnika služi [knjižnica Naja |https://naja.js.org]. To [namestite |https://naja.js.org/#/guide/01-install-setup-naja] kot node.js paket (za uporabo z aplikacijami Webpack, Rollup, Vite, Parcel in drugimi): - -```shell -npm install naja -``` - -…ali pa jo neposredno vstavite v predlogo strani: - -```latte - -``` - -Najprej je treba knjižnico [inicializirati |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: - -```js -naja.initialize(); -``` - -Da bi se iz navadne povezave (signala) ali pošiljanja obrazca ustvaril AJAX zahtevek, je dovolj označiti ustrezno povezavo, obrazec ali gumb z razredom `ajax`: - -```latte -Go - -
    - -
    - -ali - -
    - -
    -``` - - -Prekresljevanje odrezkov ------------------------- - -Vsak objekt razreda [Control |components] (vključno s samim Presenterjem) beleži, ali so se zgodile spremembe, ki zahtevajo njegovo prekreslitev. Za to služi metoda `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // po prijavi je treba prekresliti relevantni del - $this->redrawControl(); - // ... -} -``` - -Nette omogoča še natančnejši nadzor nad tem, kaj se mora prekresliti. Navedena metoda namreč lahko kot argument sprejme ime odrezka. Tako lahko razveljavimo (razumite: prisilimo prekreslitev) na ravni delov predloge. Če se razveljavi celotna komponenta, se prekresli tudi vsak njen odrezek: - -```php -// razveljavi odrezek 'header' -$this->redrawControl('header'); -``` - - -Odrezki v Latte ---------------- - -Uporaba odrezkov v Latte je izjemno enostavna. Če želite definirati del predloge kot odrezek, ga preprosto ovijte z značkama `{snippet}` in `{/snippet}`: - -```latte -{snippet header} -

    Hello ...

    -{/snippet} -``` - -Odrezek ustvari v HTML strani element `
    ` s posebnim generiranim `id`. Pri prekreslitvi odrezka se nato posodobi vsebina tega elementa. Zato je nujno, da se ob prvotnem izrisu strani izrišejo tudi vsi odrezki, čeprav so lahko na začetku prazni. - -Lahko ustvarite tudi odrezek z drugim elementom kot `
    ` s pomočjo n:atributa: - -```latte -
    -

    Hello ...

    -
    -``` - - -Območja odrezkov ----------------- - -Imena odrezkov so lahko tudi izrazi: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Tako dobimo več odrezkov `item-0`, `item-1` itd. Če bi neposredno razveljavili dinamični odrezek (na primer `item-1`), se ne bi prekreslilo nič. Razlog je ta, da odrezki res delujejo kot izrezki in se izrisujejo samo neposredno oni sami. Vendar v predlogi dejansko ni nobenega odrezka z imenom `item-1`. Ta nastane šele z izvajanjem kode v okolici odrezka, torej zanke foreach. Zato označimo del predloge, ki se mora izvesti, s pomočjo značke `{snippetArea}`: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -In pustimo prekresliti tako sam odrezek kot tudi celotno nadrejeno območje: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Hkrati je priporočljivo zagotoviti, da polje `$items` vsebuje samo tiste elemente, ki se morajo prekresliti. - -Če v predlogo s pomočjo značke `{include}` vključujemo drugo predlogo, ki vsebuje odrezke, je treba vključitev predloge ponovno vključiti v `snippetArea` in jo razveljaviti skupaj z odrezkom: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Odrezki v komponentah ---------------------- - -Odrezke lahko ustvarjate tudi v [komponentah|components] in Nette jih bo samodejno prekresljeval. Vendar obstaja določena omejitev: za prekreslitev odrezkov kliče metodo `render()` brez parametrov. Torej posredovanje parametrov v predlogi ne bo delovalo: - -```latte -OK -{control productGrid} - -ne bo delovalo: -{control productGrid $arg, $arg} -{control productGrid:paginator} -``` - - -Pošiljanje uporabniških podatkov --------------------------------- - -Skupaj z odrezki lahko klientu pošljete poljubne druge podatke. Dovolj je, da jih zapišete v objekt `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Uspeh'; - } -} -``` - - -Posredovanje parametrov -======================= - -Če komponenti s pomočjo AJAX zahtevka pošiljamo parametre, bodisi parametre signala ali persistentne parametre, moramo pri zahtevku navesti njihovo globalno ime, ki vsebuje tudi ime komponente. Celotno ime parametra vrne metoda `getParameterId()`. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -In `handle` metoda z ustreznimi parametri v komponenti: - -```php -public function handleFoo(int $bar): void -{ -} -``` diff --git a/application/sl/bootstrapping.texy b/application/sl/bootstrapping.texy deleted file mode 100644 index c529fd023f..0000000000 --- a/application/sl/bootstrapping.texy +++ /dev/null @@ -1,297 +0,0 @@ -Bootstrapping -************* - -
    - -Bootstrapping je proces inicializacije okolja aplikacije, ustvarjanja vsebnika za vstavljanje odvisnosti (DI) in zagona aplikacije. Razpravljali bomo o: - -- kako razred Bootstrap inicializira okolje -- kako so aplikacije konfigurirane z uporabo NEON datotek -- kako razlikovati med produkcijskim in razvojnim načinom -- kako ustvariti in konfigurirati DI vsebnik - -
    - - -Aplikacije, bodisi spletne ali skripti, zagnani iz ukazne vrstice, začnejo svoje delovanje z neko obliko inicializacije okolja. V davnih časih je za to skrbel datoteka z imenom, na primer `include.inc.php`, ki jo je prvotna datoteka vključila. V sodobnih Nette aplikacijah jo je nadomestil razred `Bootstrap`, ki ga kot del aplikacije najdete v datoteki `app/Bootstrap.php`. Lahko izgleda na primer takole: - -```php -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Konfigurator je odgovoren za nastavitev okolja aplikacije in storitev. - $this->configurator = new Configurator; - // Nastavi mapo za začasne datoteke, ki jih generira Nette (npr. prevedene predloge) - $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 pameten in razvojni način se vklopi samodejno, - // ali pa ga lahko omogočite za določen IP naslov z odkomentiranjem naslednje vrstice: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Aktivira Tracy: ultimativni "švicarski nož" za razhroščevanje. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: samodejno naloži vse razrede v izbrani mapi - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Naloži konfiguracijske datoteke - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -Prvotna datoteka je v primeru spletnih aplikacij `index.php`, ki se nahaja v [javni mapi |directory-structure#Javna mapa www] `www/`. Ta si pusti od razreda Bootstrap inicializirati okolje in izdelati DI vsebnik. Nato iz njega pridobi storitev `Application`, ki zažene spletno aplikacijo: - -```php -$bootstrap = new App\Bootstrap; -// Inicializacija okolja + ustvarjanje DI vsebnika -$container = $bootstrap->bootWebApplication(); -// DI vsebnik ustvari objekt Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Zagon aplikacije Nette in obdelava dohodnega zahtevka -$application->run(); -``` - -Kot je vidno, pri nastavitvi okolja in ustvarjanju dependency injection (DI) vsebnika pomaga razred [api:Nette\Bootstrap\Configurator], ki si ga bomo zdaj podrobneje predstavili. - - -Razvojni vs produkcijski način -============================== - -Nette se obnaša različno glede na to, ali teče na razvojnem ali produkcijskem strežniku: - -🛠️ Razvojni način (Development): - - Prikazuje Tracy debugbar z uporabnimi informacijami (SQL poizvedbe, čas izvajanja, uporabljeni pomnilnik) - - Ob napaki prikaže podrobno stran z napako s klici funkcij in vsebino spremenljivk - - Samodejno obnavlja predpomnilnik ob spremembi Latte predlog, urejanju konfiguracijskih datotek itd. - - -🚀 Produkcijski način (Production): - - Ne prikazuje nobenih informacij za razhroščevanje, vse napake zapisuje v dnevnik - - Ob napaki prikaže ErrorPresenter ali splošno stran "Server Error" - - Predpomnilnik se nikoli samodejno ne obnavlja! - - Optimiziran za hitrost in varnost - - -Izbira načina se izvaja s samodejnim zaznavanjem, zato običajno ni treba ničesar konfigurirati ali ročno preklapljati: - -- razvojni način: na localhostu (IP naslov `127.0.0.1` ali `::1`) če ni prisoten proxy (tj. njegova HTTP glava) -- produkcijski način: povsod drugje - -Če želimo razvojni način omogočiti tudi v drugih primerih, na primer programerjem, ki dostopajo z določenega IP naslova, uporabimo `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // lahko navedemo tudi polje IP naslovov -``` - -Vsekakor priporočamo kombiniranje IP naslova s piškotkom. V piškotek `nette-debug` shranimo skrivni žeton, npr. `secret1234`, in na ta način aktiviramo razvojni način za programerje, ki dostopajo z določenega IP naslova in imajo hkrati v piškotku omenjeni žeton: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Razvojni način lahko tudi popolnoma izklopimo, tudi za localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Pozor, vrednost `true` vklopi razvojni način na trdo, kar se nikoli ne sme zgoditi na produkcijskem strežniku. - - -Orodje za razhroščevanje Tracy -============================== - -Za enostavno razhroščevanje še vklopimo odlično orodje [Tracy |tracy:]. V razvojnem načinu vizualizira napake in v produkcijskem načinu napake beleži v navedeno mapo: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Začasne datoteke -================ - -Nette uporablja predpomnilnik za DI vsebnik, RobotLoader, predloge itd. Zato je treba nastaviti pot do mape, kamor se bo predpomnilnik shranjeval: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -Na Linuxu ali macOS nastavite mapama `log/` in `temp/` [pravice za pisanje |nette:troubleshooting#Nastavitev pravic map]. - - -RobotLoader -=========== - -Praviloma bomo želeli samodejno nalagati razrede s pomočjo [RobotLoaderja |robot-loader:], zato ga moramo zagnati in mu pustiti, da nalaga razrede iz mape, kjer se nahaja `Bootstrap.php` (tj. `__DIR__`), in vseh podmap: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Alternativni pristop je, da pustimo razrede nalagati samo prek [Composerja |best-practices:composer] ob upoštevanju PSR-4. - - -Časovni pas -=========== - -Prek konfiguratorja lahko nastavite privzeti časovni pas. - -```php -$this->configurator->setTimeZone('Europe/Prague'); -``` - - -Konfiguracija DI vsebnika -========================= - -Del zagonskega procesa je ustvarjanje DI vsebnika ali tovarne objektov, kar je srce celotne aplikacije. Gre pravzaprav za PHP razred, ki ga Nette generira in shrani v mapo s predpomnilnikom. Tovarna izdeluje ključne objekte aplikacije in s pomočjo konfiguracijskih datotek ji naročamo, kako naj jih ustvarja in nastavlja, s čimer vplivamo na obnašanje celotne aplikacije. - -Konfiguracijske datoteke se običajno zapisujejo v formatu [NEON |neon:format]. V ločenem poglavju boste izvedeli, [kaj vse je mogoče konfigurirati |nette:configuring]. - -.[tip] -V razvojnem načinu se vsebnik samodejno posodablja ob vsaki spremembi kode ali konfiguracijskih datotek. V produkcijskem načinu se generira samo enkrat in spremembe se zaradi maksimizacije zmogljivosti ne preverjajo. - -Konfiguracijske datoteke naložimo s pomočjo `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Če želimo dodati več konfiguracijskih datotek, lahko funkcijo `addConfig()` pokličemo večkrat. - -```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'); -} -``` - -Ime `cli.php` ni napaka, konfiguracija je lahko zapisana tudi v PHP datoteki, ki jo vrne kot polje. - -Prav tako lahko dodamo druge konfiguracijske datoteke v [odsek `includes` |dependency-injection:configuration#Vključevanje datotek]. - -Če se v konfiguracijskih datotekah pojavijo elementi z enakimi ključi, bodo prepisani ali v primeru [polj združeni |dependency-injection:configuration#Združevanje]. Kasneje vključena datoteka ima višjo prioriteto kot prejšnja. Datoteka, v kateri je naveden odsek `includes`, ima višjo prioriteto kot v njej vključene datoteke. - - -Statični parametri ------------------- - -Parametre, uporabljene v konfiguracijskih datotekah, lahko definiramo [v odseku `parameters` |dependency-injection:configuration#Parametri] in jih tudi posredujemo (ali prepišemo) z metodo `addStaticParameters()` (ima alias `addParameters()`). Pomembno je, da različne vrednosti parametrov povzročijo generiranje dodatnih DI vsebnikov, torej dodatnih razredov. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -Na parameter `projectId` se lahko v konfiguraciji sklicujemo z običajnim zapisom `%projectId%`. - - -Dinamični parametri -------------------- - -V vsebnik lahko dodamo tudi dinamične parametre, katerih različne vrednosti za razliko od statičnih parametrov ne povzročijo generiranja novih DI vsebnikov. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Preprosto lahko tako dodamo npr. okoljske spremenljivke, na katere se nato lahko v konfiguraciji sklicujemo z zapisom `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Privzeti parametri ------------------- - -V konfiguracijskih datotekah lahko uporabite te statične parametre: - -- `%appDir%` je absolutna pot do mape z datoteko `Bootstrap.php` -- `%wwwDir%` je absolutna pot do mape z vhodno datoteko `index.php` -- `%tempDir%` je absolutna pot do mape za začasne datoteke -- `%vendorDir%` je absolutna pot do mape, kamor Composer namešča knjižnice -- `%rootDir%` je absolutna pot do korenskega direktorija projekta -- `%debugMode%` označuje, ali je aplikacija v načinu za razhroščevanje -- `%consoleMode%` označuje, ali je zahtevek prišel prek ukazne vrstice - - -Uvožene storitve ----------------- - -Zdaj gremo globlje. Čeprav je smisel DI vsebnika izdelovati objekte, lahko izjemoma nastane potreba, da v vsebnik vstavimo obstoječi objekt. To storimo tako, da storitev definiramo z zastavico `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -In v bootstrapu v vsebnik vstavimo objekt: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Različna okolja -=============== - -Ne bojte se prilagoditi razreda Bootstrap svojim potrebam. Metodi `bootWebApplication()` lahko dodate parametre za razlikovanje spletnih projektov. Ali pa lahko dopolnimo druge metode, na primer `bootTestEnvironment()`, ki inicializira okolje za enotne teste, `bootConsoleApplication()` za skripte, klicane iz ukazne vrstice itd. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // inicializacija Nette Testerja - $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/sl/components.texy b/application/sl/components.texy deleted file mode 100644 index 71001e3566..0000000000 --- a/application/sl/components.texy +++ /dev/null @@ -1,485 +0,0 @@ -Interaktivne komponente -*********************** - -
    - -Komponente so samostojni ponovno uporabni objekti, ki jih vstavljamo v strani. Lahko so obrazci, podatkovne mreže, ankete, pravzaprav karkoli, kar ima smisel uporabljati večkrat. Pokazali si bomo: - -- kako uporabljati komponente? -- kako jih pisati? -- kaj so signali? - -
    - -Nette ima vgrajen komponentni sistem. Nekaj podobnega se lahko spomnijo veterani iz Delphi ali ASP.NET Web Forms, na nečem oddaljeno podobnem temeljita React ali Vue.js. Vendar pa je v svetu PHP ogrodij to edinstvena zadeva. - -Pri tem komponente bistveno vplivajo na pristop k ustvarjanju aplikacij. Strani lahko namreč sestavljate iz vnaprej pripravljenih enot. Potrebujete v administraciji podatkovno mrežo? Najdete jo na [Componette |https://componette.org/search/component], repozitoriju odprtokodnih dodatkov (torej ne samo komponent) za Nette in jo preprosto vstavite v presenter. - -V presenter lahko vključite poljubno število komponent. In v nekatere komponente lahko vstavljate druge komponente. Tako nastane komponentno drevo, katerega koren je presenter. - - -Tovarniške metode -================= - -Kako se komponente vstavljajo v presenter in nato uporabljajo? Običajno s pomočjo tovarniških metod. - -Tovarna komponent predstavlja eleganten način, kako komponente ustvarjati šele takrat, ko so dejansko potrebne (lazy / on demand). Celotna čarovnija temelji na implementaciji metode z imenom `createComponent()`, kjer je `` ime ustvarjene komponente, in ki komponento ustvari ter vrne. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->item; - return $poll; - } -} -``` - -Zahvaljujoč temu, da so vse komponente ustvarjene v ločenih metodah, koda pridobi na preglednosti. - -.[note] -Imena komponent se vedno začnejo z malo začetnico, čeprav se v imenu metode pišejo z veliko. - -Tovarn nikoli ne kličemo neposredno, pokličejo se same takrat, ko komponento prvič uporabimo. Zahvaljujoč temu je komponenta ustvarjena v pravem trenutku in samo v primeru, ko je dejansko potrebna. Če komponente ne uporabimo (na primer pri AJAX zahtevku, ko se prenaša samo del strani, ali pri predpomnjenju predloge), se sploh ne ustvari in prihranimo zmogljivost strežnika. - -```php .{file:DefaultPresenter.php} -// dostopimo do komponente in če je bilo to prvič, -// se pokliče createComponentPoll(), ki jo ustvari -$poll = $this->getComponent('poll'); -// alternativna sintaksa: $poll = $this['poll']; -``` - -V predlogi je mogoče izrisati komponento s pomočjo značke [{control} |#Izrisovanje]. Zato ni potrebno ročno posredovati komponent v predlogo. - -```latte -

    Glasujte

    - -{control poll} -``` - - -Hollywood style -=============== - -Komponente običajno uporabljajo eno svežo tehniko, ki ji radi rečemo Hollywood style. Zagotovo poznate krilatico, ki jo tako pogosto slišijo udeleženci filmskih avdicij: "Ne kličite nas, mi bomo poklicali vas." In prav za to gre. - -V Nette namreč namesto tega, da bi se morali nenehno spraševati ("je bil obrazec poslan?", "je bil veljaven?" ali "je uporabnik pritisnil ta gumb?"), poveste ogrodju "ko se to zgodi, pokliči to metodo" in nadaljnje delo prepustite njemu. Če programirate v JavaScriptu, ta slog programiranja dobro poznate. Pišete funkcije, ki se kličejo, ko nastopi določen dogodek. In jezik jim posreduje ustrezne parametre. - -To popolnoma spremeni pogled na pisanje aplikacij. Več nalog kot lahko prepustite ogrodju, manj dela imate vi. In manj stvari lahko na primer pozabite. - - -Pišemo komponento -================= - -Pod pojmom komponenta običajno mislimo na potomca razreda [api:Nette\Application\UI\Control]. (Natančneje bi bilo torej uporabljati izraz "controls", vendar "kontrole" imajo v slovenščini popolnoma drugačen pomen in se je bolj uveljavil izraz "komponente".) Sam presenter [api:Nette\Application\UI\Presenter] je mimogrede tudi potomec razreda `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Izrisovanje -=========== - -Že vemo, da se za izris komponente uporablja značka `{control componentName}`. Ta pravzaprav pokliče metodo `render()` komponente, v kateri poskrbimo za izris. Na voljo imamo, popolnoma enako kot v presenterju, [Latte predlogo|templates] v spremenljivki `$this->template`, v katero posredujemo parametre. Za razliko od presenterja moramo navesti datoteko s predlogo in jo pustiti izrisati: - -```php .{file:PollControl.php} -public function render(): void -{ - // vstavimo v predlogo nekaj parametrov - $this->template->param = $value; - // in jo izrišemo - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -Značka `{control}` omogoča posredovanje parametrov v metodo `render()`: - -```latte -{control poll $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Včasih se lahko komponenta sestoji iz več delov, ki jih želimo izrisovati ločeno. Za vsakega od njih si ustvarimo lastno metodo za izris, tukaj v primeru na primer `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -In v predlogi jo nato pokličemo s pomočjo: - -```latte -{control poll:paginator} -``` - -Za boljše razumevanje je dobro vedeti, kako se ta značka prevede v PHP. - -```latte -{control poll} -{control poll:paginator 123, 'hello'} -``` - -se prevede kot: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -Metoda `getComponent()` vrne komponento `poll` in nad to komponento kliče metodo `render()`, oz. `renderPaginator()`, če je drugačen način izrisovanja naveden v znački za dvopičjem. - -.[caution] -Pozor, če se kjerkoli v parametrih pojavi **`=>`**, bodo vsi parametri zapakirani v polje in posredovani kot prvi argument: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -se prevede kot: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Izris podkomponente: - -```latte -{control cartControl-someForm} -``` - -se prevede kot: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Komponente, enako kot presenterji, samodejno posredujejo v predloge nekaj uporabnih spremenljivk: - -- `$basePath` je absolutna URL pot do korenskega direktorija (npr. `/eshop`) -- `$baseUrl` je absolutni URL do korenskega direktorija (npr. `http://localhost/eshop`) -- `$user` je objekt [ki predstavlja uporabnika |security:authentication] -- `$presenter` je trenutni presenter -- `$control` je trenutna komponenta -- `$flashes` polje [sporočil |#Flash sporočila] poslanih s funkcijo `flashMessage()` - - -Signal -====== - -Že vemo, da navigacija v Nette aplikaciji temelji na povezovanju ali preusmerjanju na pare `Presenter:action`. Kaj pa, če želimo samo izvesti akcijo na **trenutni strani**? Na primer spremeniti razvrščanje stolpcev v tabeli; izbrisati element; preklopiti svetel/temen način; poslati obrazec; glasovati v anketi; itd. - -Tej vrsti zahtevkov rečemo signali. In podobno kot akcije sprožijo metode `action()` ali `render()`, signali kličejo metode `handle()`. Medtem ko je pojem akcije (ali view) povezan izključno s presenterji, se signali nanašajo na vse komponente. In torej tudi na presenterje, ker je `UI\Presenter` potomec `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... obdelava signala ... -} -``` - -Povezavo, ki pokliče signal, ustvarimo na običajen način, torej v predlogi z atributom `n:href` ali značko `{link}`, v kodi z metodo `link()`. Več v poglavju [Ustvarjanje URL povezav |creating-links#Povezave na signal]. - -```latte -kliknite tukaj -``` - -Signal se vedno kliče na trenutnem presenterju in akciji, ni ga mogoče poklicati na drugem presenterju ali drugi akciji. - -Signal torej povzroči ponovno nalaganje strani popolnoma enako kot pri prvotnem zahtevku, le da dodatno pokliče obdelovalno metodo signala z ustreznimi parametri. Če metoda ne obstaja, se sproži izjema [api:Nette\Application\UI\BadSignalException], ki se uporabniku prikaže kot stran z napako 403 Forbidden. - - -Odrezki in AJAX -=============== - -Signali vas morda nekoliko spominjajo na AJAX: obdelovalci, ki se kličejo na trenutni strani. In imate prav, signali se res pogosto kličejo s pomočjo AJAX-a in nato v brskalnik prenesemo samo spremenjene dele strani. Ali t.i. odrezke. Več informacij najdete na [strani, namenjeni AJAX-u |ajax]. - - -Flash sporočila -=============== - -Komponenta ima svoje lastno shrambo flash sporočil, neodvisno od presenterja. Gre za sporočila, ki na primer obveščajo o rezultatu operacije. Pomembna značilnost flash sporočil je, da so v predlogi na voljo tudi po preusmeritvi. Tudi po prikazu ostanejo živa še nadaljnjih 30 sekund – na primer za primer, če bi zaradi napačnega prenosa uporabnik osvežil stran - sporočilo mu torej ne izgine takoj. - -Pošiljanje zagotavlja metoda [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Prvi parameter je besedilo sporočila ali objekt `stdClass`, ki predstavlja sporočilo. Neobvezni drugi parameter je njegov tip (error, warning, info ipd.). Metoda `flashMessage()` vrne instanco flash sporočila kot objekt `stdClass`, kateremu je mogoče dodajati dodatne informacije. - -```php -$this->flashMessage('Element je bil izbrisan.'); -$this->redirect(/* ... */); // in preusmerimo -``` - -Predlogi so ta sporočila na voljo v spremenljivki `$flashes` kot objekti `stdClass`, ki vsebujejo lastnosti `message` (besedilo sporočila), `type` (tip sporočila) in lahko vsebujejo že omenjene uporabniške informacije. Izrišemo jih na primer takole: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Preusmeritev po signalu -======================= - -Po obdelavi signala komponente pogosto sledi preusmeritev. To je podobna situacija kot pri obrazcih - po njihovem pošiljanju prav tako preusmerjamo, da ob osvežitvi strani v brskalniku ne pride do ponovnega pošiljanja podatkov. - -```php -$this->redirect('this'); // preusmeri na trenutni presenter in akcijo -``` - -Ker je komponenta ponovno uporaben element in običajno ne bi smela imeti neposredne povezave s konkretnimi presenterji, metodi `redirect()` in `link()` samodejno interpretirata parameter kot signal komponente: - -```php -$this->redirect('click'); // preusmeri na signal 'click' iste komponente -``` - -Če potrebujete preusmeriti na drug presenter ali akcijo, lahko to storite prek presenterja: - -```php -$this->getPresenter()->redirect('Product:show'); // preusmeri na drug presenter/akcijo -``` - - -Persistentni parametri -====================== - -Persistentni parametri služijo za ohranjanje stanja v komponentah med različnimi zahtevki. Njihova vrednost ostane enaka tudi po kliku na povezavo. Za razliko od podatkov v seji se prenašajo v URL-ju. In to popolnoma samodejno, vključno s povezavami, ustvarjenimi v drugih komponentah na isti strani. - -Imate na primer komponento za paginacijo vsebine. Takšnih komponent je lahko na strani več. In želimo si, da po kliku na povezavo ostanejo vse komponente na svoji trenutni strani. Zato iz številke strani (`page`) naredimo persistentni parameter. - -Ustvarjanje persistentnega parametra je v Nette izjemno enostavno. Dovolj je ustvariti javno lastnost in jo označiti z atributom: (prej se je uporabljalo `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // ta vrstica je pomembna - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // mora biti public -} -``` - -Pri lastnosti priporočamo navedbo tudi podatkovnega tipa (npr. `int`) in lahko navedete tudi privzeto vrednost. Vrednosti parametrov je mogoče [validirati |#Validacija persistentnih parametrov]. - -Pri ustvarjanju povezave lahko persistentnemu parametru spremenite vrednost: - -```latte -naslednja -``` - -Ali pa ga lahko *ponastavite*, tj. odstranite iz URL-ja. Potem bo prevzel svojo privzeto vrednost: - -```latte -ponastavi -``` - - -Persistentne komponente -======================= - -Ne samo parametri, tudi komponente so lahko persistentne. Pri takšni komponenti se njeni persistentni parametri prenašajo tudi med različnimi akcijami presenterja ali med več presenterji. Persistentne komponente označimo z anotacijo pri razredu presenterja. Na primer, tako označimo komponente `calendar` in `poll`: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Podkomponent znotraj teh komponent ni treba označevati, postale bodo persistentne tudi one. - -V PHP 8 lahko za označevanje persistentnih komponent uporabite tudi atribute: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Komponente z odvisnostmi -======================== - -Kako ustvarjati komponente z odvisnostmi, ne da bi si "onesnažili" presenterje, ki jih bodo uporabljali? Zahvaljujoč pametnim lastnostim DI vsebnika v Nette lahko, enako kot pri uporabi klasičnih storitev, večino dela prepustimo ogrodju. - -Vzemimo za primer komponento, ki ima odvisnost od storitve `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // Id ankete, za katero ustvarjamo komponento - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Če bi pisali klasično storitev, ne bi bilo kaj reševati. Za posredovanje vseh odvisnosti bi nevidno poskrbel DI vsebnik. Vendar pa s komponentami običajno ravnamo tako, da njihovo novo instanco ustvarjamo neposredno v presenterju v [tovarniških metodah |#Tovarniške metode] `createComponent…()`. Toda posredovanje vseh odvisnosti vseh komponent v presenter, da bi jih nato posredovali komponentam, je okorno. In toliko napisane kode… - -Logično vprašanje je, zakaj preprosto ne registriramo komponente kot klasične storitve, je ne posredujemo v presenter in nato v metodi `createComponent…()` ne vračamo? Takšen pristop pa je neprimeren, ker želimo imeti možnost komponento ustvariti tudi večkrat. - -Pravilna rešitev je napisati za komponento tovarno, torej razred, ki nam bo komponento ustvaril: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Tako tovarno registriramo v naš vsebnik v konfiguraciji: - -```neon -services: - - PollControlFactory -``` - -in na koncu jo uporabimo v našem presenterju: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // lahko posredujemo naš parameter - return $this->pollControlFactory->create($pollId); - } -} -``` - -Odlično je, da Nette DI takšne preproste tovarne zna [generirati |dependency-injection:factory], tako da namesto njene celotne kode zadostuje napisati samo njen vmesnik: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -In to je vse. Nette notranje ta vmesnik implementira in ga posreduje v presenter, kjer ga že lahko uporabljamo. Čarobno nam prav v našo komponento doda tudi parameter `$id` in instanco razreda `PollFacade`. - - -Komponente v globino -==================== - -Komponente v Nette Application predstavljajo ponovno uporabne dele spletne aplikacije, ki jih vstavljamo v strani in katerim je posvečeno celotno to poglavje. Kakšne natančno sposobnosti ima takšna komponenta? - -1) je izrisljiva v predlogi -2) ve, [kateri svoj del |ajax#Odrezki] mora izrisati pri AJAX zahtevku (odrezki) -3) ima sposobnost shranjevanja svojega stanja v URL (persistentni parametri) -4) ima sposobnost odzivanja na uporabniške akcije (signali) -5) ustvarja hierarhično strukturo (kjer je koren presenter) - -Vsako od teh funkcij zagotavlja kateri od razredov dedne linije. Za izrisovanje (1 + 2) skrbi [api:Nette\Application\UI\Control], za vključitev v [življenjski cikel |presenters#Življenjski cikel presenterja] (3, 4) razred [api:Nette\Application\UI\Component] in za ustvarjanje hierarhične strukture (5) razreda [Container in Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Življenjski cikel komponente ----------------------------- - -[* lifecycle-component.svg *] *** *Življenjski cikel komponente* .<> - - -Validacija persistentnih parametrov ------------------------------------ - -Vrednosti [persistentnih parametrov |#Persistentni parametri], prejetih iz URL-ja, zapisuje v lastnosti metoda `loadState()`. Ta tudi preverja, ali ustreza podatkovni tip, naveden pri lastnosti, sicer odgovori z napako 404 in stran se ne prikaže. - -Nikoli slepo ne verjemite persistentnim parametrom, ker jih lahko uporabnik enostavno prepiše v URL-ju. Tako na primer preverimo, ali je številka strani `$this->page` večja od 0. Primerna pot je prepisati omenjeno metodo `loadState()`: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // tukaj se nastavi $this->page - // sledi lastno preverjanje vrednosti: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Nasprotni proces, torej zbiranje vrednosti iz persistentnih lastnosti, ima na skrbi metoda `saveState()`. - - -Signali v globino ------------------ - -Signal povzroči ponovno nalaganje strani popolnoma enako kot pri prvotnem zahtevku (razen v primeru, ko je klican z AJAX-om) in pokliče metodo `signalReceived($signal)`, katere privzeta implementacija v razredu `Nette\Application\UI\Component` poskuša poklicati metodo, sestavljeno iz besed `handle{signal}`. Nadaljnja obdelava je odvisna od danega objekta. Objekti, ki dedujejo od `Component` (tzn. `Control` in `Presenter`), se odzovejo tako, da poskušajo poklicati metodo `handle{signal}` z ustreznimi parametri. - -Z drugimi besedami: vzame se definicija funkcije `handle{signal}` in vsi parametri, ki so prišli z zahtevkom, ter se argumentom glede na ime dodelijo parametri iz URL-ja in poskuša poklicati dano metodo. Npr. kot parameter `$id` se posreduje vrednost iz parametra `id` v URL-ju, kot `$something` se posreduje `something` iz URL-ja itd. In če metoda ne obstaja, metoda `signalReceived` sproži [izjemo |api:Nette\Application\UI\BadSignalException]. - -Signal lahko sprejme katerakoli komponenta, presenter ali objekt, ki implementira vmesnik `SignalReceiver` in je priključen v drevo komponent. - -Med glavne prejemnike signalov bodo spadali `Presenterji` in vizualne komponente, ki dedujejo od `Control`. Signal naj bi služil kot znak za objekt, da mora nekaj narediti – anketa si mora zabeležiti glas od uporabnika, blok z novicami se mora razširiti in prikazati dvakrat toliko novic, obrazec je bil poslan in mora obdelati podatke in podobno. - -URL za signal ustvarimo s pomočjo metode [Component::link() |api:Nette\Application\UI\Component::link()]. Kot parameter `$destination` posredujemo niz `{signal}!` in kot `$args` polje argumentov, ki jih želimo signalu posredovati. Signal se vedno kliče na trenutnem presenterju in akciji s trenutnimi parametri, parametri signala se samo dodajo. Poleg tega se takoj na začetku doda **parameter `?do`, ki določa signal**. - -Njegov format je bodisi `{signal}` ali `{signalReceiver}-{signal}`. `{signalReceiver}` je ime komponente v presenterju. Zato v imenu komponente ne sme biti vezaja – uporablja se za ločevanje imena komponente in signala, vendar je mogoče tako ugnezditi več komponent. - -Metoda [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] preveri, ali je komponenta (prvi argument) prejemnik signala (drugi argument). Drugi argument lahko izpustimo – potem ugotavlja, ali je komponenta prejemnik kateregakoli signala. Kot drugi parameter lahko navedemo `true` in s tem preverimo, ali je prejemnik ne samo navedena komponenta, ampak tudi katerikoli njen potomec. - -V katerikoli fazi pred `handle{signal}` lahko signal izvedemo ročno s klicem metode [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], ki prevzame skrb za obdelavo signala – vzame komponento, ki se je določila kot prejemnik signala (če ni določen prejemnik signala, je to presenter sam) in ji pošlje signal. - -Primer: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -S tem je signal izveden predčasno in se ne bo več ponovno klical. diff --git a/application/sl/configuration.texy b/application/sl/configuration.texy deleted file mode 100644 index 4f92a881d4..0000000000 --- a/application/sl/configuration.texy +++ /dev/null @@ -1,191 +0,0 @@ -Konfiguracija aplikacij -*********************** - -.[perex] -Pregled konfiguracijskih možnosti za Nette Aplikacije. - - -Application -=========== - -```neon -application: - # prikazati ploščo "Nette Application" v Tracy BlueScreen? - debugger: ... # (bool) privzeto je true - - # ali se bo ob napaki klical error-presenter? - # učinkuje samo v razvojnem načinu - catchExceptions: ... # (bool) privzeto je true - - # ime error-presenterja - errorPresenter: Error # (string|array) privzeto je 'Nette:Error' - - # definira aliase za presenterje in akcije - aliases: ... - - # definira pravila za prevajanje imena presenterja v razred - mapping: ... - - # ali napačne povezave ne generirajo opozoril? - # učinkuje samo v razvojnem načinu - silentLinks: ... # (bool) privzeto je false -``` - -Od `nette/application` različice 3.2 je mogoče definirati par error-presenterjev: - -```neon -application: - errorPresenter: - 4xx: Error4xx # za izjemo Nette\Application\BadRequestException - 5xx: Error5xx # za ostale izjeme -``` - -Možnost `silentLinks` določa, kako se Nette obnaša v razvojnem načinu, ko generiranje povezave ne uspe (na primer zato, ker presenter ne obstaja itd.). Privzeta vrednost `false` pomeni, da Nette sproži napako `E_USER_WARNING`. Nastavitev na `true` bo to sporočilo o napaki potlačila. V produkcijskem okolju se `E_USER_WARNING` vedno sproži. Na to obnašanje lahko vplivamo tudi z nastavitvijo spremenljivke presenterja [$invalidLinkMode |creating-links#Neveljavne povezave]. - -[Aliasi poenostavljajo povezovanje |creating-links#Aliasi] na pogosto uporabljene presenterje. - -[Mapiranje definira pravila |directory-structure#Mapiranje presenterjev], po katerih se iz imena presenterja izpelje ime razreda. - - -Samodejna registracija presenterjev ------------------------------------ - -Nette samodejno dodaja presenterje kot storitve v DI vsebnik, kar bistveno pospeši njihovo ustvarjanje. Kako Nette presenterje išče, je mogoče konfigurirati: - -```neon -application: - # iskati presenterje v Composer class map? - scanComposer: ... # (bool) privzeto je true - - # maska, ki ji morata ustrezati ime razreda in datoteke - scanFilter: ... # (string) privzeto je '*Presenter' - - # v katerih mapah iskati presenterje? - scanDirs: # (string[]|false) privzeto je '%appDir%' - - %vendorDir%/mymodule -``` - -Mape, navedene v `scanDirs`, ne prepišejo privzete vrednosti `%appDir%`, ampak jo dopolnjujejo, `scanDirs` bo torej vseboval obe poti `%appDir%` in `%vendorDir%/mymodule`. Če bi želeli privzeto mapo izpustiti, uporabimo [klicaj |dependency-injection:configuration#Združevanje], ki vrednost prepiše: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Skeniranje map lahko izklopimo z navedbo vrednosti false. Ne priporočamo popolne potlačitve samodejnega dodajanja presenterjev, ker sicer pride do zmanjšanja zmogljivosti aplikacije. - - -Predloge Latte -============== - -S to nastavitvijo lahko globalno vplivamo na obnašanje Latte v komponentah in presenterjih. - -```neon -latte: - # prikazati ploščo Latte v Tracy Baru za glavno predlogo (true) ali vse komponente (all)? - debugger: ... # (true|false|'all') privzeto je true - - # generira predloge z glavo declare(strict_types=1) - strictTypes: ... # (bool) privzeto je false - - # vklopi način [strogega razčlenjevalnika |latte:develop#striktní režim] - strictParsing: ... # (bool) privzeto je false - - # aktivira [preverjanje generirane kode |latte:develop#Kontrola vygenerovaného kódu] - phpLinter: ... # (string) privzeto je null - - # nastavi locale - locale: cs_CZ # (string) privzeto je null - - # razred objekta $this->template - templateClass: App\MyTemplateClass # privzeto je Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Če uporabljate Latte različice 3, lahko dodajate nove [razširitve |latte:extending-latte#Latte Extension] s pomočjo: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Če uporabljate Latte različice 2, lahko registrirate nove značke bodisi z navedbo imena razreda ali s sklicem na storitev. Kot privzeta se kliče metoda `install()`, vendar to lahko spremenite tako, da navedete ime druge metode: - -```neon -latte: - # registracija uporabniških Latte značk - macros: - - App\MyLatteMacros::register # statična metoda, ime razreda ali klicna funkcija - - @App\MyLatteMacrosFactory # storitev z metodo install() - - @App\MyLatteMacrosFactory::register # storitev z metodo register() - -services: - - App\MyLatteMacrosFactory -``` - - -Usmerjanje -========== - -Osnovne nastavitve: - -```neon -routing: - # prikazati usmerjevalno ploščo v Tracy Baru? - debugger: ... # (bool) privzeto je true - - # serializira usmerjevalnik v DI vsebnik - cache: ... # (bool) privzeto je false -``` - -Usmerjanje običajno definiramo v razredu [RouterFactory |routing#Zbirka poti]. Alternativno lahko poti definiramo tudi v konfiguraciji s pomočjo parov `maska: akcija`, vendar ta način ne ponuja tako široke variabilnosti v nastavitvah: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Konstante -========= - -Ustvarjanje PHP konstant. - -```neon -constants: - Foobar: 'baz' -``` - -Po zagonu aplikacije bo ustvarjena konstanta `Foobar`. - -.[note] -Konstante ne bi smele služiti kot nekakšne globalno dostopne spremenljivke. Za posredovanje vrednosti v objekte uporabite [dependency injection |dependency-injection:passing-dependencies]. - - -PHP -=== - -Nastavitev direktiv PHP. Pregled vseh direktiv najdete na [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -Storitve DI -=========== - -Te storitve se dodajajo v DI vsebnik: - -| Ime | Tip | Opis -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [zaganjalnik celotne aplikacije |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | tovarna za presenterje -| `application.###` | [api:Nette\Application\UI\Presenter] | posamezni presenterji -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | tovarna objekta `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | tovarna za [`$this->template` |templates] diff --git a/application/sl/creating-links.texy b/application/sl/creating-links.texy deleted file mode 100644 index 0de3e87efa..0000000000 --- a/application/sl/creating-links.texy +++ /dev/null @@ -1,286 +0,0 @@ -Ustvarjanje URL povezav -*********************** - -
    - -Ustvarjanje povezav v Nette je preprosto, kot kazanje s prstom. Dovolj je le nameriti in ogrodje bo že samo opravilo vse delo. Pokazali si bomo: - -- kako ustvarjati povezave v predlogah in drugje -- kako razlikovati povezavo na trenutno stran -- kaj storiti z neveljavnimi povezavami - -
    - - -Zahvaljujoč [dvosmernemu usmerjanju |routing] vam nikoli ne bo treba v predloge ali kodo trdo kodirati URL naslovov vaše aplikacije, ki se lahko kasneje spremenijo, ali jih zapleteno sestavljati. V povezavi je dovolj navesti presenter in akcijo, posredovati morebitne parametre in ogrodje bo že samo generiralo URL. Pravzaprav je to zelo podobno, kot ko kličete funkcijo. To vam bo všeč. - - -V predlogi presenterja -====================== - -Najpogosteje ustvarjamo povezave v predlogah in odličen pomočnik je atribut `n:href`: - -```latte -podrobnosti -``` - -Opazite, da smo namesto HTML atributa `href` uporabili [n:atribut |latte:syntax#n:atributi] `n:href`. Njegova vrednost potem ni URL, kot bi bilo v primeru atributa `href`, ampak ime presenterja in akcije. - -Klik na povezavo je, poenostavljeno rečeno, nekaj takega kot klicanje metode `ProductPresenter::renderShow()`. In če ima v svoji signaturi parametre, jo lahko kličemo z argumenti: - -```latte -podrobnosti izdelka -``` - -Možno je posredovati tudi imenovane parametre. Naslednja povezava posreduje parameter `lang` z vrednostjo `cs`: - -```latte -podrobnosti izdelka -``` - -Če metoda `ProductPresenter::renderShow()` nima `$lang` v svoji signaturi, lahko vrednost parametra ugotovi s pomočjo `$lang = $this->getParameter('lang')` ali iz [lastnosti |presenters#Parametri zahtevka]. - -Če so parametri shranjeni v polju, jih lahko razvijemo z operatorjem `...` (v Latte 2.x z operatorjem `(expand)`): - -```latte -{var $args = [$product->id, lang => cs]} -podrobnosti izdelka -``` - -V povezavah se samodejno prenašajo tudi t.i. [persistentni parametri |presenters#Persistentni parametri]. - -Atribut `n:href` je zelo priročen za HTML značke ``. Če želimo povezavo izpisati drugje, na primer v besedilu, uporabimo `{link}`: - -```latte -Naslov je: {link Home:default} -``` - - -V kodi -====== - -Za ustvarjanje povezave v presenterju služi metoda `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Parametre lahko posredujemo tudi s pomočjo polja, kjer lahko navedemo tudi imenovane parametre: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); -``` - -Povezave lahko ustvarjamo tudi brez presenterja, za to je tu [##LinkGenerator] in njegova metoda `link()`. - - -Povezave na presenter -===================== - -Če je cilj povezave presenter in akcija, ima to sintakso: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Format podpirajo vse značke Latte in vse metode presenterja, ki delajo s povezavami, torej `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()` in tudi [##LinkGenerator]. Torej, čeprav je v primerih uporabljen `n:href`, bi lahko bila tam katerakoli od funkcij. - -Osnovna oblika je torej `Presenter:action`: - -```latte -domača stran -``` - -Če povezujemo na akcijo trenutnega presenterja, lahko njegovo ime izpustimo: - -```latte -domača stran -``` - -Če je cilj akcija `default`, jo lahko izpustimo, vendar dvopičje mora ostati: - -```latte -domača stran -``` - -Povezave lahko vodijo tudi v druge [module |directory-structure#Presenterji in predloge]. Tukaj se povezave razlikujejo na relativne v ugnezden podmodul ali absolutne. Princip je analogen potem na disku, le da so namesto poševnic dvopičja. Predpostavimo, da je trenutni presenter del modula `Front`, potem zapišemo: - -```latte -povezava na Front:Shop:Product:show -povezava na Admin:Product:show -``` - -Poseben primer je povezava [nase |#Povezava na trenutno stran], ko kot cilj navedemo `this`. - -```latte -osveži -``` - -Povezovati lahko na določen del strani prek t.i. fragmenta za znakom lojtre `#`: - -```latte -povezava na Home:default in fragment #main -``` - - -Absolutne poti -============== - -Povezave, generirane s pomočjo `link()` ali `n:href`, so vedno absolutne poti (tj. začnejo se z znakom `/`), vendar ne absolutni URL-ji s protokolom in domeno kot `https://domain`. - -Za generiranje absolutnega URL-ja dodajte na začetek dve poševnici (npr. `n:href="//Home:"`). Ali pa lahko preklopite presenter, da generira samo absolutne povezave z nastavitvijo `$this->absoluteUrls = true`. - - -Povezava na trenutno stran -========================== - -Cilj `this` ustvari povezavo na trenutno stran: - -```latte -osveži -``` - -Hkrati se prenašajo tudi vsi parametri, navedeni v signaturi metode `action()` ali `render()`, če `action()` ni definirana. Torej, če smo na strani `Product:show` in `id: 123`, povezava na `this` prenese tudi ta parameter. - -Seveda je mogoče parametre specificirati neposredno: - -```latte -osveži -``` - -Funkcija `isLinkCurrent()` ugotavlja, ali je cilj povezave enak trenutni strani. To lahko uporabimo na primer v predlogi za razlikovanje povezav ipd. - -Parametri so enaki kot pri metodi `link()`, poleg tega pa je mogoče namesto konkretne akcije navesti nadomestni znak `*`, ki pomeni katerokoli akcijo danega presenterja. - -```latte -{if !isLinkCurrent('Admin:login')} - Prijavite se -{/if} - -
  • - ... -
  • -``` - -V kombinaciji z `n:href` v enem elementu se da uporabiti skrajšana oblika: - -```latte -... -``` - -Nadomestni znak `*` lahko uporabimo samo namesto akcije, ne pa presenterja. - -Za ugotavljanje, ali smo v določenem modulu ali njegovem podmodulu, uporabimo metodo `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Povezave na signal -================== - -Cilj povezave ni nujno samo presenter in akcija, ampak tudi [signal |components#Signal] (kličejo metodo `handle()`). Potem je sintaksa naslednja: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -Signal torej loči klicaj: - -```latte -signal -``` - -Lahko ustvarimo tudi povezavo na signal podkomponente (ali pod-podkomponente): - -```latte -signal -``` - - -Povezave v komponenti -===================== - -Ker so [komponente|components] samostojne ponovno uporabne enote, ki ne bi smele imeti nobenih povezav z okoliškimi presenterji, tukaj povezave delujejo nekoliko drugače. Atribut Latte `n:href` in značka `{link}` ter metode komponent, kot je `link()` in druge, obravnavajo cilj povezave **vedno kot ime signala**. Zato ni treba niti navajati klicaja: - -```latte -signal, ne akcija -``` - -Če bi želeli v predlogi komponente povezovati na presenterje, uporabimo za to značko `{plink}`: - -```latte -domov -``` - -ali v kodi - -```php -$this->getPresenter()->link('Home:default') -``` - - -Aliasi .{data-version:v3.2.2} -============================= - -Včasih se lahko zgodi, da je koristno paru Presenter:akcija dodeliti lahko zapomnljiv alias. Na primer, domačo stran `Front:Home:default` poimenovati preprosto kot `home` ali `Admin:Dashboard:default` kot `admin`. - -Aliasi se definirajo v [konfiguraciji|configuration] pod ključem `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -V povezavah se nato zapisujejo s pomočjo afne, na primer: - -```latte -administracija -``` - -Podprti so tudi v vseh metodah, ki delajo s povezavami, kot je `redirect()` in podobno. - - -Neveljavne povezave -=================== - -Lahko se zgodi, da ustvarimo neveljavno povezavo - bodisi zato, ker vodi na neobstoječ presenter, ali zato, ker posreduje več parametrov, kot jih ciljna metoda sprejema v svoji signaturi, ali ko za ciljno akcijo ni mogoče generirati URL-ja. Kako ravnati z neveljavnimi povezavami, določa statična spremenljivka `Presenter::$invalidLinkMode`. Ta lahko prevzame kombinacijo teh vrednosti (konstant): - -- `Presenter::InvalidLinkSilent` - tihi način, kot URL se vrne znak # -- `Presenter::InvalidLinkWarning` - sproži se opozorilo E_USER_WARNING, ki bo v produkcijskem načinu zabeleženo, vendar ne bo povzročilo prekinitve izvajanja skripta -- `Presenter::InvalidLinkTextual` - vizualno opozorilo, napako izpiše neposredno v povezavo -- `Presenter::InvalidLinkException` - sproži se izjema InvalidLinkException - -Privzeta nastavitev je `InvalidLinkWarning` v produkcijskem načinu in `InvalidLinkWarning | InvalidLinkTextual` v razvojnem. `InvalidLinkWarning` v produkcijskem okolju ne povzroči prekinitve skripta, vendar bo opozorilo zabeleženo. V razvojnem okolju ga ujame [Tracy |tracy:] in prikaže bluescreen. `InvalidLinkTextual` deluje tako, da kot URL vrne sporočilo o napaki, ki se začne z znaki `#error:`. Da bi bile takšne povezave na prvi pogled očitne, si dodamo v CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Če ne želimo, da se v razvojnem okolju producirajo opozorila, lahko nastavimo tihi način neposredno v [konfiguraciji|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Kako ustvarjati povezave s podobnim udobjem kot ima metoda `link()`, vendar brez prisotnosti presenterja? Za to je tu [api:Nette\Application\LinkGenerator]. - -LinkGenerator je storitev, ki si jo lahko pustite posredovati prek konstruktorja in nato ustvarjate povezave z njegovo metodo `link()`. - -V primerjavi s presenterji je tu razlika. LinkGenerator ustvarja vse povezave takoj kot absolutne URL-je. In nadalje ne obstaja noben "trenutni presenter", zato ni mogoče kot cilj navesti samo ime akcije `link('default')` ali navajati relativne poti do modulov. - -Neveljavne povezave vedno sprožijo `Nette\Application\UI\InvalidLinkException`. diff --git a/application/sl/directory-structure.texy b/application/sl/directory-structure.texy deleted file mode 100644 index fcf4a9205d..0000000000 --- a/application/sl/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Struktura mape aplikacije -************************* - -
    - -Kako zasnovati pregledno in razširljivo strukturo map za projekte v Nette Framework? Pokazali si bomo preverjene prakse, ki vam bodo pomagale pri organizaciji kode. Izvedeli boste: - -- kako **logično razčleniti** aplikacijo v mape -- kako strukturo zasnovati tako, da **dobro skalira** z rastjo projekta -- kakšne so **možne alternative** in njihove prednosti ali slabosti - -
    - - -Pomembno je omeniti, da Nette Framework sam po sebi ne vztraja pri nobeni konkretni strukturi. Zasnovan je tako, da se ga da enostavno prilagoditi kakršnimkoli potrebam in preferencam. - - -Osnovna struktura projekta -========================== - -Čeprav Nette Framework ne narekuje nobene fiksne strukture map, obstaja preverjena privzeta ureditev v obliki [Web Project|https://github.com/nette/web-project]: - -/--pre -web-project/ -├── app/ ← mapa z aplikacijo -├── assets/ ← datoteke SCSS, JS, slike..., alternativno resources/ -├── bin/ ← skripti za ukazno vrstico -├── config/ ← konfiguracija -├── log/ ← zabeležene napake -├── temp/ ← začasne datoteke, predpomnilnik -├── tests/ ← testi -├── vendor/ ← knjižnice, nameščene s Composerjem -└── www/ ← javna mapa (document-root) -\-- - -To strukturo lahko poljubno urejate glede na svoje potrebe - mape preimenujete ali premaknete. Nato je dovolj le urediti relativne poti do map v datoteki `Bootstrap.php` in po potrebi `composer.json`. Nič več ni potrebno, nobene zapletene rekonfiguracije, nobenih sprememb konstant. Nette razpolaga s pametnim samodejnim zaznavanjem in samodejno prepozna lokacijo aplikacije, vključno z njeno osnovno URL. - - -Principi organizacije kode -========================== - -Ko prvič raziskujete nov projekt, bi se morali v njem hitro znajti. Predstavljajte si, da odprete mapo `app/Model/` in vidite to strukturo: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -Iz nje razberete le to, da projekt uporablja neke storitve, repozitorije in entitete. O dejanskem namenu aplikacije ne izveste ničesar. - -Poglejmo si drugačen pristop - **organizacijo po domenah**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Tukaj je drugače - na prvi pogled je jasno, da gre za spletno trgovino. Že sama imena map razkrivajo, kaj aplikacija zna - dela s plačili, naročili in izdelki. - -Prvi pristop (organizacija po tipu razredov) v praksi prinaša vrsto težav: koda, ki logično sodi skupaj, je razdrobljena v različne mape in morate med njimi preskakovati. Zato bomo organizirali po domenah. - - -Imenski prostori ----------------- - -Običajno je, da struktura map ustreza imenskim prostorom v aplikaciji. To pomeni, da fizična lokacija datotek ustreza njihovemu imenskemu prostoru. Na primer, razred, ki se nahaja v `app/Model/Product/ProductRepository.php`, bi moral imeti imenski prostor `App\Model\Product`. Ta princip pomaga pri orientaciji v kodi in poenostavlja samodejno nalaganje. - - -Ednina vs množina v imenih --------------------------- - -Opazite, da pri glavnih mapah aplikacije uporabljamo ednino: `app`, `config`, `log`, `temp`, `www`. Enako tudi znotraj aplikacije: `Model`, `Core`, `Presentation`. To je zato, ker vsaka od njih predstavlja en celovit koncept. - -Podobno na primer `app/Model/Product` predstavlja vse v zvezi z izdelki. Ne bomo ga poimenovali `Products`, ker ne gre za mapo, polno izdelkov (to bi bile tam datoteke `nokia.php`, `samsung.php`). To je imenski prostor, ki vsebuje razrede za delo z izdelki - `ProductRepository.php`, `ProductService.php`. - -Mapa `app/Tasks` je v množini zato, ker vsebuje nabor samostojnih izvedljivih skriptov - `CleanupTask.php`, `ImportTask.php`. Vsak od njih je samostojna enota. - -Za doslednost priporočamo uporabo: -- Ednine za imenski prostor, ki predstavlja funkcionalno celoto (čeprav dela z več entitetami) -- Množine za zbirke samostojnih enot -- V primeru negotovosti ali če o tem ne želite razmišljati, izberite ednino - - -Javna mapa `www/` -================= - -Ta mapa je edina dostopna s spleta (t.i. document-root). Pogosto se lahko srečate tudi z imenom `public/` namesto `www/` - to je le vprašanje konvencije in na funkcionalnost aplikacije nima vpliva. Mapa vsebuje: -- [Vstopno točko |bootstrapping#index.php] aplikacije `index.php` -- Datoteko `.htaccess` s pravili za mod_rewrite (pri Apache) -- Statične datoteke (CSS, JavaScript, slike) -- Naložene datoteke - -Za pravilno varnost aplikacije je ključno imeti pravilno [konfiguriran document-root |nette:troubleshooting#Kako spremeniti ali odstraniti mapo www iz URL-ja]. - -.[note] -Nikoli ne postavljajte v to mapo mape `node_modules/` - vsebuje na tisoče datotek, ki so lahko izvedljive in ne bi smele biti javno dostopne. - - -Aplikacijska mapa `app/` -======================== - -To je glavna mapa z aplikacijsko kodo. Osnovna struktura: - -/--pre -app/ -├── Core/ ← infrastrukturne zadeve -├── Model/ ← poslovna logika -├── Presentation/ ← presenterji in predloge -├── Tasks/ ← ukazni skripti -└── Bootstrap.php ← zagonski razred aplikacije -\-- - -`Bootstrap.php` je [zagonski razred aplikacije|bootstrapping], ki inicializira okolje, nalaga konfiguracijo in ustvarja DI vsebnik. - -Poglejmo si zdaj posamezne podmape podrobneje. - - -Presenterji in predloge -======================= - -Predstavitveni del aplikacije imamo v mapi `app/Presentation`. Alternativa je kratko `app/UI`. To je mesto za vse presenterje, njihove predloge in morebitne pomožne razrede. - -To plast organiziramo po domenah. V kompleksnem projektu, ki združuje spletno trgovino, blog in API, bi struktura izgledala takole: - -/--pre -app/Presentation/ -├── Shop/ ← spletna trgovina frontend -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← blog -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administracija -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← API končne točke - └── V1/ -\-- - -Nasprotno pa bi pri preprostem blogu uporabili členitev: - -/--pre -app/Presentation/ -├── Front/ ← frontend spletnega mesta -│ ├── Home/ -│ └── Post/ -├── Admin/ ← administracija -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, sitemaps itd. -\-- - -Mape kot `Home/` ali `Dashboard/` vsebujejo presenterje in predloge. Mape kot `Front/`, `Admin/` ali `Api/` imenujemo **moduli**. Tehnično gre za običajne mape, ki služijo za logično členitev aplikacije. - -Vsaka mapa s presenterjem vsebuje enako poimenovan presenter in njegove predloge. Na primer, mapa `Dashboard/` vsebuje: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← predloga -\-- - -Ta struktura map se odraža v imenskih prostorih razredov. Na primer, `DashboardPresenter` se nahaja v imenskem prostoru `App\Presentation\Admin\Dashboard` (glej [##mapiranje presenterjev]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -Na presenter `Dashboard` znotraj modula `Admin` se v aplikaciji sklicujemo s pomočjo dvopične notacije kot na `Admin:Dashboard`. Na njegovo akcijo `default` potem kot na `Admin:Dashboard:default`. V primeru ugnezdenih modulov uporabljamo več dvopičij, na primer `Shop:Order:Detail:default`. - - -Fleksibilen razvoj strukture ----------------------------- - -Ena od velikih prednosti te strukture je, kako elegantno se prilagaja rastočim potrebam projekta. Kot primer si vzemimo del, ki generira XML vire. Na začetku imamo preprosto obliko: - -/--pre -Export/ -├── ExportPresenter.php ← en presenter za vse izvoze -├── sitemap.latte ← predloga za sitemap -└── feed.latte ← predloga za RSS vir -\-- - -Sčasoma se dodajo nove vrste virov in zanje potrebujemo več logike... Noben problem! Mapa `Export/` preprosto postane modul: - -/--pre -Export/ -├── Sitemap/ -│ ├── SitemapPresenter.php -│ └── sitemap.latte -└── Feed/ - ├── FeedPresenter.php - ├── zbozi.latte ← vir za Zboží.cz - └── heureka.latte ← vir za Heureka.cz -\-- - -Ta transformacija je popolnoma gladka - dovolj je ustvariti nove podmape, razdeliti kodo vanje in posodobiti povezave (npr. iz `Export:feed` na `Export:Feed:zbozi`). Zahvaljujoč temu lahko strukturo postopoma širimo glede na potrebe, raven gnezdenja ni nikakor omejena. - -Če na primer v administraciji imate veliko presenterjev, ki se nanašajo na upravljanje naročil, kot so `OrderDetail`, `OrderEdit`, `OrderDispatch` itd., lahko za boljšo organiziranost na tem mestu ustvarite modul (mapo) `Order`, v katerem bodo (mape za) presenterje `Detail`, `Edit`, `Dispatch` in drugi. - - -Lokacija predlog ----------------- - -V prejšnjih primerih smo videli, da so predloge nameščene neposredno v mapi s presenterjem: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← izbirni razred za predlogo -└── default.latte ← predloga -\-- - -Ta lokacija se v praksi izkaže za najudobnejšo - vse povezane datoteke imate takoj pri roki. - -Alternativno lahko predloge namestite v podmapo `templates/`. Nette podpira obe varianti. Celo predloge lahko namestite tudi popolnoma izven mape `Presentation/`. Vse o možnostih lokacije predlog najdete v poglavju [Iskanje predlog |templates#Iskanje predlog]. - - -Pomožni razredi in komponente ------------------------------ - -K presenterjem in predlogam pogosto spadajo tudi druge pomožne datoteke. Namestimo jih logično glede na njihovo področje delovanja: - -1. **Neposredno pri presenterju** v primeru specifičnih komponent za dani presenter: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← komponenta za izpis izdelkov -└── FilterForm.php ← obrazec za filtriranje -\-- - -2. **Za modul** - priporočamo uporabo mape `Accessory`, ki se namesti pregledno takoj na začetku abecede: - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← komponente za frontend -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **Za celotno aplikacijo** - v `Presentation/Accessory/`: -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Ali pa lahko pomožne razrede kot `LatteExtension.php` ali `TemplateFilters.php` namestite v infrastrukturno mapo `app/Core/Latte/`. In komponente v `app/Components`. Izbira je odvisna od navad ekipe. - - -Model - srce aplikacije -======================= - -Model vsebuje vso poslovno logiko aplikacije. Za njegovo organizacijo velja spet pravilo - strukturiramo po domenah: - -/--pre -app/Model/ -├── Payment/ ← vse v zvezi s plačili -│ ├── PaymentFacade.php ← glavna vstopna točka -│ ├── PaymentRepository.php -│ ├── Payment.php ← entiteta -├── Order/ ← vse v zvezi z naročili -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← vse v zvezi z dostavo -\-- - -V modelu se tipično srečate s temi tipi razredov: - -**Fasade**: predstavljajo glavno vstopno točko v konkretno domeno v aplikaciji. Delujejo kot orkestrator, ki koordinira sodelovanje med različnimi storitvami za namen implementacije celotnih primerov uporabe (kot "ustvari naročilo" ali "obdelaj plačilo"). Pod svojo orkestracijsko plastjo fasada skriva implementacijske podrobnosti pred preostankom aplikacije, s čimer zagotavlja čist vmesnik za delo z dano domeno. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // validacija - // ustvarjanje naročila - // pošiljanje e-pošte - // zapisovanje v statistiko - } -} -``` - -**Storitve**: osredotočajo se na specifično poslovno operacijo znotraj domene. Za razliko od fasade, ki orkestrira celotne primere uporabe, storitev implementira konkretno poslovno logiko (kot izračuni cen ali obdelava plačil). Storitve so tipično brez stanja in jih lahko uporabljajo bodisi fasade kot gradniki za kompleksnejše operacije ali neposredno drugi deli aplikacije za enostavnejše naloge. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // izračun cene - } -} -``` - -**Repozitoriji**: zagotavljajo vso komunikacijo s podatkovnim skladiščem, tipično podatkovno bazo. Njegova naloga je nalaganje in shranjevanje entitet ter implementacija metod za njihovo iskanje. Repozitorij loči preostanek aplikacije od implementacijskih podrobnosti podatkovne baze in zagotavlja objektno usmerjen vmesnik za delo s podatki. - -```php -class OrderRepository -{ - public function find(int $id): ?Order - { - } - - public function findByCustomer(int $customerId): array - { - } -} -``` - -**Entitete**: objekti, ki predstavljajo glavne poslovne koncepte v aplikaciji, ki imajo svojo identiteto in se spreminjajo s časom. Tipično gre za razrede, preslikane na tabele podatkovne baze s pomočjo ORM (kot Nette Database Explorer ali Doctrine). Entitete lahko vsebujejo poslovna pravila, ki se nanašajo na njihove podatke, in validacijsko logiko. - -```php -// Entiteta, preslikana na tabelo podatkovne baze 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, - ]); - } -} -``` - -**Vrednostni objekti**: nespremenljivi objekti, ki predstavljajo vrednosti brez lastne identitete - na primer denarni znesek ali e-poštni naslov. Dve instanci vrednostnega objekta z enakimi vrednostmi se štejeta za identični. - - -Infrastrukturna koda -==================== - -Mapa `Core/` (ali tudi `Infrastructure/`) je dom za tehnično osnovo aplikacije. Infrastrukturna koda tipično vključuje: - -/--pre -app/Core/ -├── Router/ ← usmerjanje in upravljanje URL-jev -│ └── RouterFactory.php -├── Security/ ← avtentikacija in avtorizacija -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← dnevniško beleženje in nadzor -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← plast predpomnjenja -│ └── FullPageCache.php -└── Integration/ ← integracija z zunanjimi storitvami - ├── Slack/ - └── Stripe/ -\-- - -Pri manjših projektih seveda zadostuje ravna členitev: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -Gre za kodo, ki: - -- Rešuje tehnično infrastrukturo (usmerjanje, beleženje, predpomnjenje) -- Integrira zunanje storitve (Sentry, Elasticsearch, Redis) -- Zagotavlja osnovne storitve za celotno aplikacijo (pošta, podatkovna baza) -- Je večinoma neodvisna od konkretne domene - predpomnilnik ali logger deluje enako za spletno trgovino ali blog. - -Se sprašujete, ali določen razred spada sem ali v model? Ključna razlika je v tem, da koda v `Core/`: - -- Ne ve nič o domeni (izdelki, naročila, članki) -- Je večinoma mogoče prenesti v drug projekt -- Rešuje "kako deluje" (kako poslati pošto), ne pa "kaj dela" (kakšno pošto poslati) - -Primer za boljše razumevanje: - -- `App\Core\MailerFactory` - ustvarja instance razreda za pošiljanje e-pošte, rešuje SMTP nastavitve -- `App\Model\OrderMailer` - uporablja `MailerFactory` za pošiljanje e-pošte o naročilih, pozna njihove predloge in ve, kdaj se morajo poslati - - -Ukazni skripti -============== - -Aplikacije pogosto potrebujejo izvajanje dejavnosti izven običajnih HTTP zahtevkov - bodisi gre za obdelavo podatkov v ozadju, vzdrževanje ali periodične naloge. Za zagon služijo preprosti skripti v mapi `bin/`, samo implementacijsko logiko pa namestimo v `app/Tasks/` (po potrebi `app/Commands/`). - -Primer: - -/--pre -app/Tasks/ -├── Maintenance/ ← vzdrževalni skripti -│ ├── CleanupCommand.php ← brisanje starih podatkov -│ └── DbOptimizeCommand.php ← optimizacija podatkovne baze -├── Integration/ ← integracija z zunanjimi sistemi -│ ├── ImportProducts.php ← uvoz iz dobaviteljskega sistema -│ └── SyncOrders.php ← sinhronizacija naročil -└── Scheduled/ ← redne naloge - ├── NewsletterCommand.php ← pošiljanje novičnikov - └── ReminderCommand.php ← obvestila strankam -\-- - -Kaj spada v model in kaj v ukazne skripte? Na primer, logika za pošiljanje enega e-poštnega sporočila je del modela, množično pošiljanje tisočev e-poštnih sporočil pa že spada v `Tasks/`. - -Naloge običajno [zaženemo iz ukazne vrstice |https://blog.nette.org/en/cli-scripts-in-nette-application] ali prek crona. Lahko jih zaženemo tudi prek HTTP zahtevka, vendar je treba misliti na varnost. Presenter, ki nalogo zažene, je treba zavarovati, na primer samo za prijavljene uporabnike ali z močnim žetonom in dostopom z dovoljenih IP naslovov. Pri dolgih nalogah je treba povečati časovno omejitev skripta in uporabiti `session_write_close()`, da se ne zaklene seja. - - -Druge možne mape -================ - -Poleg omenjenih osnovnih map lahko glede na potrebe projekta dodate druge specializirane mape. Poglejmo si najpogostejše izmed njih in njihovo uporabo: - -/--pre -app/ -├── Api/ ← logika za API, neodvisna od predstavitvene plasti -├── Database/ ← migracijski skripti in sejalci za testne podatke -├── Components/ ← deljene vizualne komponente po celotni aplikaciji -├── Event/ ← uporabno, če uporabljate arhitekturo, vodeno z dogodki -├── Mail/ ← e-poštne predloge in povezana logika -└── Utils/ ← pomožni razredi -\-- - -Za deljene vizualne komponente, uporabljene v presenterjih po celotni aplikaciji, lahko uporabite mapo `app/Components` ali `app/Controls`: - -/--pre -app/Components/ -├── Form/ ← deljene komponente obrazcev -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← komponente za izpise podatkov -│ └── DataGrid.php -└── Navigation/ ← navigacijski elementi - ├── Breadcrumbs.php - └── Menu.php -\-- - -Sem spadajo komponente, ki imajo kompleksnejšo logiko. Če želite komponente deliti med več projekti, je priporočljivo, da jih izločite v samostojen composer paket. - -V mapo `app/Mail` lahko namestite upravljanje e-poštne komunikacije: - -/--pre -app/Mail/ -├── templates/ ← e-poštne predloge -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Mapiranje presenterjev -====================== - -Mapiranje definira pravila za izpeljavo imena razreda iz imena presenterja. Specificiramo jih v [konfiguraciji|configuration] pod ključem `application › mapping`. - -Na tej strani smo si pokazali, da presenterje nameščamo v mapo `app/Presentation` (po potrebi `app/UI`). To konvencijo moramo Nette sporočiti v konfiguracijski datoteki. Dovolj je ena vrstica: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Kako mapiranje deluje? Za boljše razumevanje si najprej predstavljajmo aplikacijo brez modulov. Želimo, da razredi presenterjev spadajo v imenski prostor `App\Presentation`, da se presenter `Home` preslika na razred `App\Presentation\HomePresenter`. Kar dosežemo s to konfiguracijo: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Mapiranje deluje tako, da ime presenterja `Home` nadomesti zvezdico v maski `App\Presentation\*Presenter`, s čimer dobimo končno ime razreda `App\Presentation\HomePresenter`. Preprosto! - -Kot pa vidite v primerih v tem in drugih poglavjih, razrede presenterjev nameščamo v istoimenske podmape, na primer presenter `Home` se preslika na razred `App\Presentation\Home\HomePresenter`. To dosežemo z podvojitvijo dvopičja (zahteva Nette Application 3.2): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Zdaj pristopimo k mapiranju presenterjev v module. Za vsak modul lahko definiramo specifično mapiranje: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -Glede na to konfiguracijo se presenter `Front:Home` preslika na razred `App\Presentation\Front\Home\HomePresenter`, medtem ko se presenter `Api:OAuth` na razred `App\Api\OAuthPresenter`. - -Ker imata modula `Front` in `Admin` podoben način mapiranja in takšnih modulov bo najverjetneje več, je mogoče ustvariti splošno pravilo, ki jih nadomesti. V masko razreda tako pride nova zvezdica za modul: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Deluje tudi za globlje ugnezdene strukture map, kot je na primer presenter `Admin:User:Edit`, se segment z zvezdico ponovi za vsako raven in rezultat je razred `App\Presentation\Admin\User\Edit\EditPresenter`. - -Alternativni zapis je namesto niza uporabiti polje, sestavljeno iz treh segmentov. Ta zapis je ekvivalenten prejšnjemu: - -```neon -application: - mapping: - *: [App\Presentation, *, **Presenter] - Api: [App\Api, '', *Presenter] -``` diff --git a/application/sl/how-it-works.texy b/application/sl/how-it-works.texy deleted file mode 100644 index e472bbcea2..0000000000 --- a/application/sl/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Kako delujejo aplikacije? -************************* - -
    - -Pravkar berete osnovno listino dokumentacije Nette. Spoznali boste celoten princip delovanja spletnih aplikacij. Lepo od A do Ž, od trenutka nastanka do zadnjega izdiha skripta PHP. Po branju boste vedeli: - -- kako vse skupaj deluje -- kaj so Bootstrap, Presenter in DI vsebnik -- kako izgleda struktura map - -
    - - -Struktura map -============= - -Odpri si primer ogrodja spletne aplikacije imenovane [WebProject|https://github.com/nette/web-project] in med branjem lahko gledaš datoteke, o katerih je govora. - -Struktura map izgleda nekako takole: - -/--pre -web-project/ -├── app/ ← mapa z aplikacijo -│ ├── Core/ ← osnovni razredi, potrebni za delovanje -│ │ └── RouterFactory.php ← konfiguracija URL naslovov -│ ├── Presentation/ ← presenterji, predloge & co. -│ │ ├── @layout.latte ← predloga postavitve -│ │ └── Home/ ← mapa presenterja Home -│ │ ├── HomePresenter.php ← razred presenterja Home -│ │ └── default.latte ← predloga akcije default -│ └── Bootstrap.php ← zagonski razred Bootstrap -├── assets/ ← viri (SCSS, TypeScript, izvorne slike) -├── bin/ ← skripti, zagnani iz ukazne vrstice -├── config/ ← konfiguracijske datoteke -│ ├── common.neon -│ └── services.neon -├── log/ ← zabeležene napake -├── temp/ ← začasne datoteke, predpomnilnik, … -├── vendor/ ← knjižnice, nameščene s Composerjem -│ ├── ... -│ └── autoload.php ← samodejno nalaganje vseh nameščenih paketov -├── www/ ← javna mapa ali document-root projekta -│ ├── assets/ ← sestavljene statične datoteke (CSS, JS, slike, ...) -│ ├── .htaccess ← pravila mod_rewrite -│ └── index.php ← začetna datoteka, s katero se aplikacija zažene -└── .htaccess ← prepoveduje dostop do vseh map razen www -\-- - -Strukturo map lahko kakorkoli spreminjate, mape preimenujete ali premaknete, je popolnoma fleksibilna. Nette poleg tega razpolaga s pametnim samodejnim zaznavanjem in samodejno prepozna lokacijo aplikacije, vključno z njeno osnovno URL. - -Pri nekoliko večjih aplikacijah lahko mape s presenterji in predlogami [razčlenimo v podmape |directory-structure#Presenterji in predloge] in razrede v imenske prostore, ki jim rečemo moduli. - -Mapa `www/` predstavlja t.i. javno mapo ali document-root projekta. Lahko jo preimenujete brez potrebe po kakršnemkoli dodatnem nastavljanju na strani aplikacije. Le potrebno je [konfigurirati gostovanje |nette:troubleshooting#Kako spremeniti ali odstraniti mapo www iz URL-ja] tako, da document-root kaže v to mapo. - -WebProject si lahko tudi takoj prenesete vključno z Nette in to s pomočjo [Composerja |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -Na Linuxu ali macOS nastavite mapama `log/` in `temp/` [pravice za pisanje |nette:troubleshooting#Nastavitev pravic map]. - -Aplikacija WebProject je pripravljena za zagon, ni treba ničesar konfigurirati in jo lahko takoj prikažete v brskalniku z dostopom do mape `www/`. - - -HTTP zahtevek -============= - -Vse se začne v trenutku, ko uporabnik v brskalniku odpre stran. Torej, ko brskalnik potrka na strežnik s HTTP zahtevkom. Zahtevek cilja na eno samo PHP datoteko, ki se nahaja v javni mapi `www/`, in to je `index.php`. Recimo, da gre za zahtevek na naslov `https://example.com/product/123`. Zahvaljujoč primerni [nastavitvi strežnika |nette:troubleshooting#Kako nastaviti strežnik za lepe URL-je] se tudi ta URL preslika na datoteko `index.php` in ta se izvede. - -Njegova naloga je: - -1) inicializirati okolje -2) pridobiti tovarno -3) zagnati Nette aplikacijo, ki bo obdelala zahtevek - -Kakšno tovarno? Saj ne izdelujemo traktorjev, ampak spletne strani! Počakajte, takoj se bo pojasnilo. - -Z besedami »inicializacija okolja« mislimo na primer to, da se aktivira [Tracy|tracy:], kar je čudovito orodje za beleženje ali vizualizacijo napak. Na produkcijskem strežniku napake beleži, na razvojnem jih takoj prikaže. Zato k inicializaciji spada tudi odločitev, ali spletno mesto teče v produkcijskem ali razvojnem načinu. Za to Nette uporablja [pametno samodejno zaznavanje |bootstrapping#Razvojni vs produkcijski način]: če spletno mesto zaženete na localhostu, teče v razvojnem načinu. Ni vam treba ničesar konfigurirati in aplikacija je takoj pripravljena tako za razvoj kot za ostro uporabo. Ti koraki se izvajajo in so podrobno opisani v poglavju o [razredu Bootstrap|bootstrapping]. - -Tretja točka (da, drugo smo preskočili, a se bomo vrnili k njej) je zagon aplikacije. Obdelavo HTTP zahtevkov ima v Nette na skrbi razred `Nette\Application\Application` (v nadaljevanju `Application`), zato ko rečemo zagnati aplikacijo, mislimo konkretno na klicanje metode s primernim imenom `run()` na objektu tega razreda. - -Nette je mentor, ki vas vodi k pisanju čistih aplikacij po preverjenih metodologijah. In ena od tistih popolnoma najbolj preverjenih se imenuje **dependency injection**, skrajšano DI. V tem trenutku vas ne želimo obremenjevati z razlago DI, za to je tu [ločeno poglavje|dependency-injection:introduction], bistven je posledica, da nam bo ključne objekte običajno ustvarjala tovarna objektov, ki se ji reče **DI vsebnik** (skrajšano DIC). Da, to je tista tovarna, o kateri je bila pred kratkim govora. In izdelala nam bo tudi objekt `Application`, zato potrebujemo najprej vsebnik. Pridobimo ga s pomočjo razreda `Configurator` in mu pustimo izdelati objekt `Application`, na njem pokličemo metodo `run()` in s tem se zažene Nette aplikacija. Točno to se dogaja v datoteki [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -Razred Application ima eno samo nalogo: odgovoriti na HTTP zahtevek. - -Aplikacije, napisane v Nette, se členijo v veliko t.i. presenterjev (v drugih ogrodjih se lahko srečate z izrazom controller, gre za isto stvar), kar so razredi, od katerih vsak predstavlja neko konkretno stran spletnega mesta: npr. domačo stran; izdelek v spletni trgovini; prijavni obrazec; sitemap vir itd. Aplikacija lahko ima od enega do tisoč presenterjev. - -Application začne tako, da prosi t.i. usmerjevalnik (router), naj odloči, kateremu od presenterjev predati trenutni zahtevek v obdelavo. Usmerjevalnik odloči, čigava je to odgovornost. Pogleda vhodni URL `https://example.com/product/123` in na podlagi tega, kako je nastavljen, odloči, da je to delo npr. za **presenter** `Product`, od katerega bo želel kot **akcijo** prikaz (`show`) izdelka z `id: 123`. Par presenter + akcija je dobra navada zapisovati ločeno z dvopičjem kot `Product:show`. - -Torej je usmerjevalnik transformiral URL v par `Presenter:action` + parametri, v našem primeru `Product:show` + `id: 123`. Kako takšen usmerjevalnik izgleda, si lahko pogledate v datoteki `app/Core/RouterFactory.php` in ga podrobno opisujemo v poglavju [Usmerjanje |routing]. - -Pojdimo naprej. Application že pozna ime presenterja in lahko nadaljuje naprej. S tem, da izdela objekt razreda `ProductPresenter`, kar je koda presenterja `Product`. Natančneje rečeno, prosi DI vsebnik, naj presenter izdela, ker je za izdelovanje tu on. - -Presenter lahko izgleda na primer takole: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // pridobimo podatke iz modela in jih posredujemo predlogi - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Obdelavo zahtevka prevzame presenter. In naloga je jasna: izvedi akcijo `show` z `id: 123`. Kar v jeziku presenterjev pomeni, da se pokliče metoda `renderShow()` in v parametru `$id` dobi `123`. - -Presenter lahko obravnava več akcij, torej ima več metod `render()`. Vendar priporočamo načrtovanje presenterjev z eno ali čim manj akcijami. - -Torej, poklicala se je metoda `renderShow(123)`, katere koda je sicer izmišljen primer, vendar lahko na njej vidite, kako se posredujejo podatki v predlogo, torej z zapisom v `$this->template`. - -Nato presenter vrne odgovor. Ta je lahko HTML stran, slika, XML dokument, pošiljanje datoteke z diska, JSON ali pa preusmeritev na drugo stran. Pomembno je, da če eksplicitno ne povemo, kako naj odgovori (kar je primer `ProductPresenter`), bo odgovor izris predloge s HTML stranjo. Zakaj? Ker v 99 % primerov želimo izrisati predlogo, zato presenter to obnašanje jemlje kot privzeto in nam želi olajšati delo. To je smisel Nette. - -Ni nam treba niti navajati, katero predlogo izrisati, pot do nje si izpelje sam. V primeru akcije `show` preprosto poskusi naložiti predlogo `show.latte` v mapi z razredom `ProductPresenter`. Prav tako poskusi poiskati postavitev v datoteki `@layout.latte` (podrobneje o [iskanju predlog |templates#Iskanje predlog]). - -In nato predloge izriše. S tem je naloga presenterja in celotne aplikacije končana in delo je zaključeno. Če predloga ne bi obstajala, se vrne stran z napako 404. Več o presenterjih preberite na strani [Presenterji |presenters]. - -[* request-flow.svg *] - -Za vsak slučaj, poskusimo si ponoviti celoten proces z nekoliko drugačnim URL-jem: - -1) URL bo `https://example.com` -2) zaženemo aplikacijo, ustvari se vsebnik in zažene `Application::run()` -3) usmerjevalnik dekodira URL kot par `Home:default` -4) ustvari se objekt razreda `HomePresenter` -5) pokliče se metoda `renderDefault()` (če obstaja) -6) izriše se predloga npr. `default.latte` s postavitvijo npr. `@layout.latte` - - -Morda ste se zdaj srečali z veliko novimi pojmi, vendar verjamemo, da imajo smisel. Ustvarjanje aplikacij v Nette je izjemno prijetno. - - -Predloge -======== - -Ko smo že pri predlogah, v Nette se uporablja sistem predlog [Latte |latte:]. Zato tudi končnice `.latte` pri predlogah. Latte se uporablja delsno zato, ker gre za najbolj varen sistem predlog za PHP, in hkrati tudi najbolj intuitiven sistem. Ni se vam treba učiti veliko novega, zadostuje znanje PHP in nekaj značk. Vse boste izvedeli [v dokumentaciji |templates]. - -V predlogi se [ustvarjajo povezave |creating-links] na druge presenterje & akcije takole: - -```latte -podrobnosti izdelka -``` - -Preprosto namesto realnega URL-ja napišete znani par `Presenter:action` in navedete morebitne parametre. Trik je v `n:href`, ki pravi, da ta atribut obdela Nette. In generira: - -```latte -podrobnosti izdelka -``` - -Generiranje URL-jev ima na skrbi že prej omenjeni usmerjevalnik. Namreč usmerjevalniki v Nette so izjemni s tem, da znajo izvajati ne samo transformacije iz URL-ja v par presenter:action, ampak tudi obratno, torej iz imena presenterja + akcije + parametrov generirati URL. Zahvaljujoč temu lahko v Nette popolnoma spremenite oblike URL-jev v celotni končani aplikaciji, ne da bi spremenili en sam znak v predlogi ali presenterju. Samo s tem, da uredite usmerjevalnik. Prav tako zahvaljujoč temu deluje t.i. kanonizacija, kar je še ena edinstvena lastnost Nette, ki prispeva k boljšemu SEO (optimizaciji najdljivosti na internetu) s tem, da samodejno preprečuje obstoj podvojene vsebine na različnih URL-jih. Veliko programerjev to šteje za osupljivo. - - -Interaktivne komponente -======================= - -O presenterjih vam moramo povedati še eno stvar: imajo vgrajen komponentni sistem. Nekaj podobnega se lahko spomnijo veterani iz Delphi ali ASP.NET Web Forms, na nečem oddaljeno podobnem temeljita React ali Vue.js. V svetu PHP ogrodij gre za popolnoma edinstveno zadevo. - -Komponente so samostojne ponovno uporabne celote, ki jih vstavljamo v strani (torej presenterje). Lahko so [obrazci |forms:in-presenter], [podatkovne mreže |https://componette.org/contributte/datagrid/], meniji, glasovalne ankete, pravzaprav karkoli, kar ima smisel uporabljati večkrat. Lahko ustvarjamo lastne komponente ali uporabljamo nekatere iz [ogromne ponudbe |https://componette.org] odprtokodnih komponent. - -Komponente bistveno vplivajo na pristop k ustvarjanju aplikacij. Odprle vam bodo nove možnosti sestavljanja strani iz vnaprej pripravljenih enot. In poleg tega imajo nekaj skupnega s [Hollywoodom |components#Hollywood style]. - - -DI vsebnik in konfiguracija -=========================== - -DI vsebnik ali tovarna objektov je srce celotne aplikacije. - -Ne skrbite, ni to nobena čarobna črna škatla, kot bi se morda lahko zdelo iz prejšnjih vrstic. Pravzaprav je to ena precej dolgočasna PHP klasa, ki jo generira Nette in shrani v mapo s predpomnilnikom. Ima veliko metod, poimenovanih kot `createServiceAbcd()`, in vsaka od njih zna izdelati in vrniti nek objekt. Da, tam je tudi metoda `createServiceApplication()`, ki izdela `Nette\Application\Application`, ki smo ga potrebovali v datoteki `index.php` za zagon aplikacije. In tam so metode, ki izdelujejo posamezne presenterje. In tako naprej. - -Objektom, ki jih DI vsebnik ustvarja, se iz nekega razloga reče storitve. - -Kar je pri tem razredu resnično posebno, je to, da ga ne programirate vi, ampak ogrodje. On dejansko generira PHP kodo in jo shrani na disk. Vi samo dajete navodila, kakšne objekte naj zna vsebnik izdelovati in kako natančno. In ta navodila so zapisana v [konfiguracijskih datotekah |bootstrapping#Konfiguracija DI vsebnika], za katere se uporablja format [NEON|neon:format] in zato imajo tudi končnico `.neon`. - -Konfiguracijske datoteke služijo izključno za navodila DI vsebnika. Torej, ko na primer navedem v sekciji [session |http:configuration#Seja] možnost `expiration: 14 days`, DI vsebnik pri ustvarjanju objekta `Nette\Http\Session`, ki predstavlja sejo, pokliče njegovo metodo `setExpiration('14 days')` in s tem konfiguracija postane resničnost. - -Tu je za vas pripravljeno celo poglavje, ki opisuje, kaj vse je mogoče [konfigurirati |nette:configuring] in kako [definirati lastne storitve |dependency-injection:services]. - -Ko se malo poglobite v ustvarjanje storitev, boste naleteli na besedo [autowiring |dependency-injection:autowiring]. To je iznajdba, ki vam bo na neverjeten način poenostavila življenje. Zna samodejno posredovati objekte tja, kjer jih potrebujete (na primer v konstruktorjih vaših razredov), ne da bi morali karkoli narediti. Ugotovili boste, da je DI vsebnik v Nette mali čudež. - - -Kam naprej? -=========== - -Prešli smo osnovne principe aplikacij v Nette. Zaenkrat zelo površno, vendar boste kmalu prodrli v globino in sčasoma ustvarili čudovite spletne aplikacije. Kam nadaljevati? Ste že preizkusili vadnico [Pišemo prvo aplikacijo|quickstart:]? - -Poleg zgoraj opisanega Nette razpolaga s celim arzenalom [uporabnih razredov|utils:], [podatkovno plastjo|database:], itd. Poskusite si samo tako preklikati dokumentacijo. Ali [blog|https://blog.nette.org]. Odkrili boste veliko zanimivega. - -Naj vam ogrodje prinese veliko veselja 💙 diff --git a/application/sl/multiplier.texy b/application/sl/multiplier.texy deleted file mode 100644 index 90d9b2588d..0000000000 --- a/application/sl/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: dinamične komponente -******************************** - -.[perex] -Orodje za dinamično ustvarjanje interaktivnih komponent - -Izhajajmo iz tipičnega primera: imejmo seznam blaga v spletni trgovini, pri čemer bomo pri vsakem želeli izpisati obrazec za dodajanje blaga v košarico. Ena od možnih variant je oviti celoten izpis v en obrazec. Veliko udobnejši način pa nam ponuja [api:Nette\Application\UI\Multiplier]. - -Multiplier omogoča udobno definiranje tovarniške metode za več komponent. Deluje na principu ugnezdenih komponent - vsaka komponenta, ki deduje od [api:Nette\ComponentModel\Container], lahko vsebuje druge komponente. - -.[tip] -Glej poglavje o [komponentnem modelu |components#Komponente v globino] v dokumentaciji ali [predavanje Honze Tvrdíka|https://www.youtube.com/watch?v=8y3LLexWu-I]. - -Bistvo Multiplierja je, da nastopa v vlogi starša, ki si svoje potomce lahko ustvarja dinamično s pomočjo povratnega klica (callback), predanega v konstruktorju. Glej primer: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Število kosov:') - ->setRequired(); - $form->addSubmit('send', 'Dodaj v košarico'); - return $form; - }); -} -``` - -Zdaj lahko v predlogi enostavno pri vsakem blagu pustimo izrisati obrazec - in vsak bo resnično edinstvena komponenta. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -Argument, predan v znački `{control}`, je v formatu, ki pravi: - -1. pridobi komponento `shopForm` -2. in iz nje pridobi potomca `$item->id` - -Pri prvem klicu točke **1.** `shopForm` še ne obstaja, zato se pokliče njegova tovarna `createComponentShopForm`. Na pridobljeni komponenti (instanci Multiplierja) je nato poklicana tovarna konkretnega obrazca - kar je anonimna funkcija, ki smo jo Multiplierju v konstruktorju predali. - -V naslednji iteraciji foreacha metoda `createComponentShopForm` ne bo več klicana (komponenta obstaja), ker pa iščemo njenega drugega potomca (`$item->id` bo v vsaki iteraciji drugačen), bo ponovno poklicana anonimna funkcija in nam vrnila nov obrazec. - -Edino, kar preostane, je zagotoviti, da nam obrazec v košarico doda resnično tisto blago, ki ga mora - trenutno je obrazec pri vsakem blagu popolnoma enak. Pomagala nam bo lastnost Multiplierja (in na splošno vsake tovarne na komponento v Nette Frameworku), in sicer ta, da vsaka tovarna kot svoj prvi argument dobi ime tvořené komponenty. V našem primeru bo to `$item->id`, kar je točno tisti podatek, ki ga potrebujemo. Dovolj je torej rahlo prilagoditi tvorbu obrazca: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function ($itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Število kosov:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Dodaj v košarico'); - return $form; - }); -} -``` diff --git a/application/sl/presenters.texy b/application/sl/presenters.texy deleted file mode 100644 index aea7c3d715..0000000000 --- a/application/sl/presenters.texy +++ /dev/null @@ -1,500 +0,0 @@ -Presenterji -*********** - -
    - -Spoznali bomo, kako se v Nette pišejo presenterji in predloge. Po branju boste vedeli: - -- kako deluje presenter -- kaj so persistentni parametri -- kako se rišejo predloge - -
    - -[Že vemo |how-it-works#Nette Application], da je presenter razred, ki predstavlja neko konkretno stran spletne aplikacije, npr. domačo stran; izdelek v spletni trgovini; prijavni obrazec; sitemap vir itd. Aplikacija lahko ima od enega do tisoč presenterjev. V drugih ogrodjih jim rečejo tudi kontrolerji. - -Običajno se pod pojmom presenter misli na potomca razreda [api:Nette\Application\UI\Presenter], ki je primeren za generiranje spletnih vmesnikov in kateremu se bomo posvetili v preostanku tega poglavja. V splošnem smislu je presenter katerikoli objekt, ki implementira vmesnik [api:Nette\Application\IPresenter]. - - -Življenjski cikel presenterja -============================= - -Naloga presenterja je obdelati zahtevek in vrniti odgovor (kar je lahko HTML stran, slika, preusmeritev itd.). - -Torej na začetku mu je predan zahtevek. To ni neposredno HTTP zahtevek, ampak objekt [api:Nette\Application\Request], v katerega je bil HTTP zahtevek preoblikovan s pomočjo usmerjevalnika. S tem objektom običajno ne pridemo v stik, saj presenter obdelavo zahtevka pametno delegira v druge metode, ki si jih bomo zdaj pokazali. - -[* lifecycle.svg *] *** *Življenjski cikel presenterja* .<> - -Slika predstavlja seznam metod, ki se postopoma od zgoraj navzdol kličejo, če obstajajo. Nobena od njih ni nujno, da obstaja, lahko imamo popolnoma prazen presenter brez ene same metode in na njem zgradimo preprosto statično spletno stran. - - -`__construct()` ---------------- - -Konstruktor ne spada povsem v življenjski cikel presenterja, ker se kliče v trenutku ustvarjanja objekta. Vendar ga navajamo zaradi pomembnosti. Konstruktor (skupaj z [metodo inject|best-practices:inject-method-attribute]) služi za posredovanje odvisnosti. - -Presenter ne bi smel opravljati poslovne logike aplikacije, pisati in brati iz podatkovne baze, izvajati izračunov itd. Za to so razredi iz plasti, ki jo označujemo kot model. Na primer, razred `ArticleRepository` lahko skrbi za nalaganje in shranjevanje člankov. Da bi lahko presenter z njim delal, si ga pusti [posredovati s pomočjo dependency injection |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Takoj po prejemu zahtevka se pokliče metoda `startup()`. Lahko jo uporabite za inicializacijo lastnosti, preverjanje uporabniških dovoljenj itd. Zahtevano je, da metoda vedno pokliče prednika `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -Podobno metodi `render()`. Medtem ko je `render()` namenjena pripravi podatkov za konkretno predlogo, ki se nato izriše, se v `action()` obdeluje zahtevek brez povezave z izrisovanjem predloge. Na primer, obdelajo se podatki, prijavi ali odjavi uporabnik, in tako naprej, nato pa [preusmeri drugam |#Preusmerjanje]. - -Pomembno je, da se `action()` kliče prej kot `render()`, tako da lahko v njej morebiti spremenimo nadaljnji potek dogodkov, tj. spremenimo predlogo, ki se bo risala, in tudi metodo `render()`, ki se bo klicala. In to s pomočjo `setView('jineView')`. - -Metodi se posredujejo parametri iz zahtevka. Možno in priporočljivo je navesti tipe parametrov, npr. `actionShow(int $id, ?string $slug = null)` - če bo parameter `id` manjkal ali če ne bo integer, bo presenter vrnil [napako 404 |#Napaka 404 in podobno] in zaključil delovanje. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -Metoda obdeluje t.i. signale, s katerimi se bomo seznanili v poglavju, posvečenem [komponentam |components#Signal]. Namenjena je namreč predvsem komponentam in obdelavi AJAX zahtevkov. - -Metodi se posredujejo parametri iz zahtevka, kot v primeru `action()`, vključno s tipsko kontrolo. - - -`beforeRender()` ----------------- - -Metoda `beforeRender`, kot že ime pove, se kliče pred vsako metodo `render()`. Uporablja se za skupno konfiguracijo predloge, posredovanje spremenljivk za postavitev in podobno. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -Mesto, kjer pripravljamo predlogo za nadaljnje izrisovanje, ji posredujemo podatke itd. - -Metodi se posredujejo parametri iz zahtevka, kot v primeru `action()`, vključno s tipsko kontrolo. - -```php -public function renderShow(int $id): void -{ - // pridobimo podatke iz modela in jih posredujemo predlogi - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -Metoda `afterRender`, kot ime spet pove, se kliče za vsako metodo `render()`. Uporablja se bolj izjemoma. - - -`shutdown()` ------------- - -Kliče se na koncu življenjskega cikla presenterja. - - -**Dober nasvet, preden gremo naprej**. Presenter, kot je vidno, lahko obravnava več akcij/view, torej ima več metod `render()`. Vendar priporočamo načrtovanje presenterjev z eno ali čim manj akcijami. - - -Pošiljanje odgovora -=================== - -Odgovor presenterja je praviloma [izris predloge s HTML stranjo|templates], lahko pa je tudi pošiljanje datoteke, JSON ali pa preusmeritev na drugo stran. - -Kadarkoli med življenjskim ciklom lahko z eno od naslednjih metod pošljemo odgovor in hkrati zaključimo presenter: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` in `forward()` [preusmeri |#Preusmerjanje] -- `error()` zaključi presenter [zaradi napake |#Napaka 404 in podobno] -- `sendJson($data)` presenter zaključi in [pošlje podatke |#Pošiljanje JSON] v formatu JSON -- `sendTemplate()` presenter zaključi in takoj [izriše predlogo |templates] -- `sendResponse($response)` presenter zaključi in pošlje [lastni odgovor |#Odgovori] -- `terminate()` presenter zaključi brez odgovora - -Če nobene od teh metod ne pokličete, bo presenter samodejno pristopil k izrisu predloge. Zakaj? Ker v 99 % primerov želimo izrisati predlogo, zato presenter to obnašanje jemlje kot privzeto in nam želi olajšati delo. - - -Ustvarjanje povezav -=================== - -Presenter razpolaga z metodo `link()`, s pomočjo katere lahko ustvarjamo URL povezave na druge presenterje. Prvi parameter je ciljni presenter & akcija, sledijo posredovani argumenti, ki so lahko navedeni kot polje: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'sl']); -``` - -V predlogi se ustvarjajo povezave na druge presenterje & akcije na ta način: - -```latte -podrobnosti izdelka -``` - -Preprosto namesto realnega URL-ja napišete znani par `Presenter:action` in navedete morebitne parametre. Trik je v `n:href`, ki pravi, da ta atribut obdela Latte in generira realni URL. V Nette tako sploh ni treba razmišljati o URL-jih, samo o presenterjih in akcijah. - -Več informacij najdete v poglavju [Ustvarjanje URL povezav |creating-links]. - - -Preusmerjanje -============= - -Za prehod na drug presenter služita metodi `redirect()` in `forward()`, ki imata zelo podobno sintakso kot metoda [link() |#Ustvarjanje povezav]. - -Metoda `forward()` preide na nov presenter takoj brez HTTP preusmeritve: - -```php -$this->forward('Product:show'); -``` - -Primer t.i. začasne preusmeritve s HTTP kodo 302 (ali 303, če je metoda trenutnega zahtevka POST): - -```php -$this->redirect('Product:show', $id); -``` - -Trajno preusmeritev s HTTP kodo 301 dosežete takole: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -Na drug URL izven aplikacije lahko preusmerite z metodo `redirectUrl()`. Kot drugi parameter lahko navedete HTTP kodo, privzeta je 302 (ali 303, če je metoda trenutnega zahtevka POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Preusmeritev takoj zaključi delovanje presenterja s sprožitvijo t.i. tihe zaključne izjeme `Nette\Application\AbortException`. - -Pred preusmeritvijo lahko pošljete [flash message |#Flash sporočila], torej sporočila, ki bodo po preusmeritvi prikazana v predlogi. - - -Flash sporočila -=============== - -Gre za sporočila, ki običajno obveščajo o rezultatu neke operacije. Pomembna značilnost flash sporočil je, da so v predlogi na voljo tudi po preusmeritvi. Tudi po prikazu ostanejo živa še nadaljnjih 30 sekund – na primer za primer, če bi zaradi napačnega prenosa uporabnik osvežil stran - sporočilo mu torej ne izgine takoj. - -Dovolj je poklicati metodo [flashMessage() |api:Nette\Application\UI\Control::flashMessage()] in za posredovanje v predlogo poskrbi presenter. Prvi parameter je besedilo sporočila in neobvezni drugi parameter je njegov tip (error, warning, info ipd.). Metoda `flashMessage()` vrne instanco flash sporočila, kateremu je mogoče dodajati dodatne informacije. - -```php -$this->flashMessage('Element je bil izbrisan.'); -$this->redirect(/* ... */); // in preusmerimo -``` - -Predlogi so ta sporočila na voljo v spremenljivki `$flashes` kot objekti `stdClass`, ki vsebujejo lastnosti `message` (besedilo sporočila), `type` (tip sporočila) in lahko vsebujejo že omenjene uporabniške informacije. Izrišemo jih na primer takole: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Napaka 404 in podobno -===================== - -Če zahteve ni mogoče izpolniti, na primer zato, ker članek, ki ga želimo prikazati, ne obstaja v podatkovni bazi, sprožimo napako 404 z metodo `error(?string $message = null, int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -HTTP kodo napake lahko predamo kot drugi parameter, privzeta je 404. Metoda deluje tako, da sproži izjemo `Nette\Application\BadRequestException`, nato pa `Application` preda nadzor error-presenterju. Kar je presenter, katerega naloga je prikazati stran, ki obvešča o nastali napaki. Nastavitev error-preseterja se izvaja v [konfiguraciji application|configuration]. - - -Pošiljanje JSON -=============== - -Primer action-metode, ki pošlje podatke v formatu JSON in zaključi presenter: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Parametri zahtevka .{data-version:3.1.14} -========================================= - -Presenter in tudi vsaka komponenta pridobiva iz HTTP zahtevka svoje parametre. Njihovo vrednost ugotovite z metodo `getParameter($name)` ali `getParameters()`. Vrednosti so nizi ali polja nizov, gre v bistvu za surove podatke, pridobljene neposredno iz URL-ja. - -Za večje udobje priporočamo, da parametre zpřístupnite prek lastnosti. Dovolj je, da jih označite z atributom `#[Parameter]`: - -```php -use Nette\Application\Attributes\Parameter; // ta vrstica je pomembna - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // mora biti public -} -``` - -Pri lastnosti priporočamo navedbo tudi podatkovnega tipa (npr. `string`) in Nette glede na to vrednost samodejno preoblikuje. Vrednosti parametrov lahko tudi [validirate |#Validacija parametrov]. - -Pri ustvarjanju povezave lahko parametrom vrednost neposredno nastavite: - -```latte -klikni -``` - - -Persistentni parametri -====================== - -Persistentni parametri služijo za ohranjanje stanja med različnimi zahtevki. Njihova vrednost ostane enaka tudi po kliku na povezavo. Za razliko od podatkov v seji se prenašajo v URL-ju. In to popolnoma samodejno, ni jih torej treba eksplicitno navajati v `link()` ali `n:href`. - -Primer uporabe? Imate večjezično aplikacijo. Trenutni jezik je parameter, ki mora biti nenehno del URL-ja. Vendar bi bilo izjemno utrujajoče ga v vsaki povezavi navajati. Zato ga naredite za persistentni parameter `lang` in se bo prenašal sam. Odlično! - -Ustvarjanje persistentnega parametra je v Nette izjemno enostavno. Dovolj je ustvariti javno lastnost in jo označiti z atributom: (prej se je uporabljalo `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // ta vrstica je pomembna - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // mora biti public -} -``` - -Če bo `$this->lang` imel vrednost na primer `'en'`, bodo tudi povezave, ustvarjene s pomočjo `link()` ali `n:href`, vsebovale parameter `lang=en`. In po kliku na povezavo bo spet `$this->lang = 'en'`. - -Pri lastnosti priporočamo navedbo tudi podatkovnega tipa (npr. `string`) in lahko navedete tudi privzeto vrednost. Vrednosti parametrov lahko [validirate |#Validacija parametrov]. - -Persistentni parametri se standardno prenašajo med vsemi akcijami danega presenterja. Da bi se prenašali tudi med več presenterji, jih je treba definirati bodisi: - -- v skupnem predniku, od katerega presenterji dedujejo -- v traiti, ki jo presenterji uporabijo: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -Pri ustvarjanju povezave lahko persistentnemu parametru spremenite vrednost: - -```latte -podrobnosti v slovenščini -``` - -Nebo jej lze *vyresetovat*, tj. odstranit z URL. Pak bude nabývat svou výchozí hodnotu: - -```latte -klikni -``` - - -Interaktivne komponente -======================= - -Presenterji imajo vgrajen komponentni sistem. Komponente so samostojne ponovno uporabne celote, ki jih vstavljamo v presenterje. Lahko so [obrazci |forms:in-presenter], podatkovne mreže, meniji, pravzaprav karkoli, kar ima smisel uporabljati večkrat. - -Kako se komponente vstavljajo v presenter in nato uporabljajo? To boste izvedeli v poglavju [Komponente |components]. Celo ugotovili boste, kaj imajo skupnega s Hollywoodom. - -In kje lahko dobim komponente? Na strani [Componette |https://componette.org/search/component] najdete odprtokodne komponente in tudi vrsto drugih dodatkov za Nette, ki so jih sem postavili prostovoljci iz skupnosti okoli ogrodja. - - -Gremo v globino -=============== - -.[tip] -S tem, kar smo si doslej v tem poglavju pokazali, si boste najverjetneje popolnoma zadostovali. Naslednje vrstice so namenjene tistim, ki se zanimajo za presenterje v globino in želijo vedeti popolnoma vse. - - -Validacija parametrov ---------------------- - -Vrednosti [parametrov zahtevka |#Parametri zahtevka] in [persistentnih parametrov |#Persistentni parametri], prejetih iz URL-ja, zapisuje v lastnosti metoda `loadState()`. Ta tudi kontroluje, zda odpovídá datový typ uvedený u property, jinak odpoví chybou 404 a stránka se nezobrazí. - -Nikoli slepo ne verjemite parametrom, saj jih lahko uporabnik enostavno prepiše v URL-ju. Tako na primer preverimo, ali je jezik `$this->lang` med podprtimi. Primerna pot je prepisati omenjeno metodo `loadState()`: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // tukaj se nastavi $this->lang - // sledi lastno preverjanje vrednosti: - if (!in_array($this->lang, ['en', 'sl'])) { // 'cs' spremenjeno v 'sl' - $this->error(); - } - } -} -``` - - -Shranjevanje in obnovitev zahtevka ----------------------------------- - -Zahtevek, ki ga obravnava presenter, je objekt [api:Nette\Application\Request] in ga vrača metoda presenterja `getRequest()`. - -Trenutni zahtevek lahko shranimo v sejo ali pa ga iz nje obnovimo in pustimo, da ga presenter ponovno izvede. To je koristno na primer v situaciji, ko uporabnik izpolnjuje obrazec in mu poteče prijava. Da ne bi izgubil podatkov, pred preusmeritvijo na prijavno stran trenutni zahtevek shranimo v sejo s pomočjo `$reqId = $this->storeRequest()`, ki vrne njegov identifikator v obliki kratkega niza in ga predamo kot parameter prijavnemu presenterju. - -Po prijavi pokličemo metodo `$this->restoreRequest($reqId)`, ki zahtevek prevzame iz seje in preusmeri nanj. Metoda pri tem preveri, da je zahtevek ustvaril isti uporabnik, kot se je zdaj prijavil. Če bi se prijavil drug uporabnik ali bi bil ključ neveljaven, ne naredi nič in program nadaljuje naprej. - -Poglejte si navodilo [Kako se vrniti na prejšnjo stran |best-practices:restore-request]. - - -Kanonizacija ------------- - -Presenterji imajo eno resnično odlično lastnost, ki prispeva k boljšemu SEO (optimizaciji najdljivosti na internetu). Samodejno preprečujejo obstoj podvojene vsebine na različnih URL-jih. Če do določenega cilja vodi več URL naslovov, npr. `/index` in `/index?page=1`, ogrodje določi enega od njih za primarnega (kanoničnega) in ostale nanj preusmeri s pomočjo HTTP kode 301. Zahvaljujoč temu vam iskalniki strani ne indeksirajo dvakrat in ne razpršijo njihovega page ranka. - -Temu procesu rečemo kanonizacija. Kanonični URL je tisti, ki ga generira [usmerjevalnik |routing], praviloma torej prva ustrezna pot v zbirki. - -Kanonizacija je privzeto vklopljena in jo lahko izklopite prek `$this->autoCanonicalize = false`. - -Do preusmeritve ne pride pri AJAX ali POST zahtevku, ker bi prišlo do izgube podatkov ali pa to ne bi imelo dodane vrednosti z vidika SEO. - -Kanonizacijo lahko sprožite tudi ročno s pomočjo metode `canonicalize()`, kateri se podobno kot metodi `link()` predajo presenter, akcija in parametri. Izdelala bo povezavo in jo primerjala s trenutnim URL naslovom. Če se razlikujeta, preusmeri na generirano povezavo. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // preusmeri, če se $slug razlikuje od $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - - -Dogodki -------- - -Poleg metod `startup()`, `beforeRender()` in `shutdown()`, ki se kličejo kot del življenjskega cikla presenterja, lahko definiramo še druge funkcije, ki naj se samodejno pokličejo. Presenter definira t.i. [dogodke |nette:glossary#Dogodki eventi], katerih obdelovalce dodate v polja `$onStartup`, `$onRender` in `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Obdelovalci v polju `$onStartup` se kličejo tik pred metodo `startup()`, nato `$onRender` med `beforeRender()` in `render()` in na koncu `$onShutdown` tik pred `shutdown()`. - - -Odgovori --------- - -Odgovor, ki ga vrača presenter, je objekt, ki implementira vmesnik [api:Nette\Application\Response]. Na voljo je vrsta pripravljenih odgovorov: - -- [api:Nette\Application\Responses\CallbackResponse] - pošlje povratni klic -- [api:Nette\Application\Responses\FileResponse] - pošlje datoteko -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - pošlje JSON -- [api:Nette\Application\Responses\RedirectResponse] - preusmeritev -- [api:Nette\Application\Responses\TextResponse] - pošlje besedilo -- [api:Nette\Application\Responses\VoidResponse] - prazen odgovor - -Odgovori se pošiljajo z metodo `sendResponse()`: - -```php -use Nette\Application\Responses; - -// Navadno besedilo -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Pošlje datoteko -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// Odgovor bo povratni klic -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - - -Omejitev dostopa s pomočjo `#[Requires]` .{data-version:3.2.2} --------------------------------------------------------------- - -Atribut `#[Requires]` ponuja napredne možnosti za omejevanje dostopa do presenterjev in njihovih metod. Lahko ga uporabite za specifikacijo HTTP metod, zahtevanje AJAX zahtevka, omejitev na isti izvor (same origin), in dostop samo prek posredovanja (forwarding). Atribut lahko uporabite tako za razrede presenterjev kot za posamezne metode `action()`, `render()`, `handle()` in `createComponent()`. - -Lahko določite te omejitve: -- na HTTP metode: `#[Requires(methods: ['GET', 'POST'])]` -- zahtevanje AJAX zahtevka: `#[Requires(ajax: true)]` -- dostop samo iz istega izvora: `#[Requires(sameOrigin: true)]` -- dostop samo prek posredovanja: `#[Requires(forward: true)]` -- omejitev na konkretne akcije: `#[Requires(actions: 'default')]` - -Podrobnosti najdete v navodilu [Kako uporabljati atribut Requires |best-practices:attribute-requires]. - - -Preverjanje HTTP metode ------------------------ - -Presenterji v Nette samodejno preverjajo HTTP metodo vsakega dohodnega zahtevka. Razlog za to preverjanje je predvsem varnost. Standardno so dovoljene metode `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. - -Če želite dovoliti dodatno na primer metodo `OPTIONS`, uporabite za to atribut `#[Requires]` (od Nette Application v3.2): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -V različici 3.1 se preverjanje izvaja v `checkHttpMethod()`, ki ugotavlja, ali je metoda, specificirana v zahtevku, vsebovana v polju `$presenter->allowedMethods`. Dodajanje metode naredite takole: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Pomembno je poudariti, da če dovolite metodo `OPTIONS`, jo morate nato tudi ustrezno obravnavati znotraj svojega presenterja. Metoda se pogosto uporablja kot t.i. preflight request, ki ga brskalnik samodejno pošlje pred dejanskim zahtevkom, ko je treba ugotoviti, ali je zahtevek dovoljen z vidika CORS (Cross-Origin Resource Sharing) politike. Če metodo dovolite, vendar ne implementirate pravilnega odgovora, lahko to vodi do neskladij in potencialnih varnostnih težav. - - -Nadaljnje branje -================ - -- [Metode in atributi inject |best-practices:inject-method-attribute] -- [Sestavljanje presenterjev iz trait |best-practices:presenter-traits] -- [Posredovanje nastavitev v presenterje |best-practices:passing-settings-to-presenters] -- [Kako se vrniti na prejšnjo stran |best-practices:restore-request] diff --git a/application/sl/routing.texy b/application/sl/routing.texy deleted file mode 100644 index 06c2ccee69..0000000000 --- a/application/sl/routing.texy +++ /dev/null @@ -1,721 +0,0 @@ -Usmerjanje -********** - -
    - -Usmerjevalnik (Router) skrbi za vse v zvezi z URL naslovi, da vam nad njimi ne bi bilo treba več razmišljati. Pokazali si bomo: - -- kako nastaviti usmerjevalnik, da bodo URL-ji po želji -- povedali si bomo o SEO in preusmeritvah -- in pokazali si bomo, kako napisati lasten usmerjevalnik - -
    - - -Bolj človeški URL-ji (ali tudi kul ali lepi URL-ji) so bolj uporabni, lažje zapomnljivi in pozitivno prispevajo k SEO. Nette na to misli in razvijalcem popolnoma ustreza. Za svojo aplikacijo si lahko zasnujete točno takšno strukturo URL naslovov, kakršno boste želeli. Lahko jo zasnujete celo šele takrat, ko je aplikacija že končana, saj se to izvede brez posegov v kodo ali predloge. Definira se namreč na eleganten način na enem [samem mestu |#Vključitev v aplikacijo], v usmerjevalniku, in ni tako razpršena v obliki anotacij v vseh presenterjih. - -Usmerjevalnik v Nette je izjemen s tem, da je **dvosmeren.** Zna tako dekodirati URL v HTTP zahtevku kot tudi ustvarjati povezave. Igra torej ključno vlogo v [Nette Application |how-it-works#Nette Application], saj delsno odloča o tem, kateri presenter in akcija bosta izvajala trenutni zahtevek, delsno pa se uporablja za [generiranje URL-jev |creating-links] v predlogi itd. - -Vendar usmerjevalnik ni omejen samo na to uporabo, lahko ga uporabite v aplikacijah, kjer se presenterji sploh ne uporabljajo, za REST API itd. Več v delu [#Samostojna uporaba]. - - -Zbirka poti -=========== - -Najprijetnejši način, kako definirati obliko URL naslovov v aplikaciji, ponuja razred [api:Nette\Application\Routers\RouteList]. Definicija je sestavljena iz seznama t.i. poti (routes), torej mask URL naslovov in k njim pridruženih presenterjev in akcij s pomočjo preprostega API-ja. Poti ni treba poimenovati. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -Primer pravi, da če v brskalniku odpremo `https://domain.com/rss.xml`, se prikaže presenter `Feed` z akcijo `rss`, če `https://domain.com/article/12`, se prikaže presenter `Article` z akcijo `view` itd. V primeru nenajdene primerne poti Nette Application reagira s sprožitvijo izjeme [BadRequestException |api:Nette\Application\BadRequestException], ki se uporabniku prikaže kot stran z napako 404 Not Found. - - -Vrstni red poti ---------------- - -Popolnoma **ključni je vrstni red**, v katerem so posamezne poti navedene, ker se vrednotijo postopoma od zgoraj navzdol. Velja pravilo, da poti deklariramo **od specifičnih k splošnim**: - -```php -// SLABO: 'rss.xml' ujame prva pot in ta niz razume kot -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// DOBRO -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Poti se vrednotijo od zgoraj navzdol tudi pri generiranju povezav: - -```php -// SLABO: povezava na 'Feed:rss' generira kot 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// DOBRO -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Ne bomo vam skrivali, da pravilno sestavljanje poti zahteva določeno spretnost. Preden se vanjo poglobite, vam bo koristen pomočnik [usmerjevalna plošča |#Razhroščevanje usmerjevalnika]. - - -Maska in parametri ------------------- - -Maska opisuje relativno pot od korenskega direktorija spletnega mesta. Najenostavnejša maska je statični URL: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Pogosto maske vsebujejo t.i. **parametre**. Ti so navedeni v ostrih oklepajih (npr. ``) in so posredovani v ciljni presenter, na primer metodi `renderShow(int $year)` ali v persistentni parameter `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Primer pravi, da če v brskalniku odpremo `https://example.com/chronicle/2020`, se prikaže presenter `History` z akcijo `show` in parametrom `year: 2020`. - -Parametrom lahko določimo privzeto vrednost neposredno v maski in s tem postanejo izbirni: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Pot bo zdaj sprejela tudi URL `https://example.com/chronicle/`, ki spet prikaže `History:show` s parametrom `year: 2020`. - -Parameter je lahko seveda tudi ime presenterja in akcije. Na primer tako: - -```php -$router->addRoute('/', 'Home:default'); -``` - -Navedena pot sprejema npr. URL v obliki `/article/edit` ali tudi `/catalog/list` in jih razume kot presenterje in akcije `Article:edit` in `Catalog:list`. - -Hkrati daje parametroma `presenter` in `action` privzeti vrednosti `Home` in `default` in sta torej tudi izbirna. Tako pot sprejema tudi URL v obliki `/article` in ga razume kot `Article:default`. Ali obratno, povezava na `Product:default` generira pot `/product`, povezava na privzeti `Home:default` pot `/`. - -Maska lahko opisuje ne samo relativno pot od korenskega direktorija spletnega mesta, ampak tudi absolutno pot, če se začne s poševnico, ali celo celoten absolutni URL, če se začne z dvema poševnicama: - -```php -// relativno glede na document root -$router->addRoute('/', /* ... */); - -// absolutna pot (relativna glede na domeno) -$router->addRoute('//', /* ... */); - -// absolutni URL vključno z domeno (relativen glede na shemo) -$router->addRoute('//.example.com//', /* ... */); - -// absolutni URL vključno s shemo -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Validacijski izrazi -------------------- - -Za vsak parameter lahko določimo validacijski pogoj s pomočjo [regularnega izraza|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Na primer, parametru `id` določimo, da lahko vsebuje samo števke s pomočjo regularnega izraza `\d+`: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -Privzeti regularni izraz za vse parametre je `[^/]+`, tj. vse razen poševnice. Če mora parameter sprejemati tudi poševnice, navedemo izraz `.+`: - -```php -// sprejema https://example.com/a/b/c, path bo 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Izbirne sekvence ----------------- - -V maski lahko označujemo izbirne dele s pomočjo oglatih oklepajev. Izbirni je lahko katerikoli del maske, lahko se v njem nahajajo tudi parametri: - -```php -$router->addRoute('[/]', /* ... */); - -// Sprejema poti: -// /sl/download => lang => sl, name => download -// /download => lang => null, name => download -``` - -Ko je parameter del izbirne sekvence, postane seveda tudi izbiren. Če nima navedene privzete vrednosti, bo null. - -Izbirni deli so lahko tudi v domeni: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Sekvence je mogoče poljubno gnezditi in kombinirati: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Sprejema poti: -// /sl/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -Pri generiranju URL-jev se stremi k najkrajši varianti, zato se vse, kar je mogoče izpustiti, izpusti. Zato na primer pot `index[.html]` generira pot `/index`. Obrniti obnašanje je mogoče z navedbo klicaja za levim oglatim oklepajem: - -```php -// sprejema /hello in /hello.html, generira /hello -$router->addRoute('[.html]', /* ... */); - -// sprejema /hello in /hello.html, generira /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Izbirni parametri (tj. parametri, ki imajo privzeto vrednost) brez oglatih oklepajev se obnašajo v bistvu tako, kot da bi bili oklepajeni na naslednji način: - -```php -$router->addRoute('//', /* ... */); - -// ustreza temu: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Če bi želeli vplivati na obnašanje končne poševnice, da bi se npr. namesto `/home/` generiralo samo `/home`, lahko to dosežemo takole: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Nadomestni znaki ----------------- - -V maski absolutne poti lahko uporabimo naslednje nadomestne znake in se tako izognemo npr. potrebi po zapisovanju domene v masko, ki se lahko razlikuje v razvojnem in produkcijskem okolju: - -- `%tld%` = top level domain, npr. `com` ali `org` -- `%sld%` = second level domain, npr. `example` -- `%domain%` = domena brez poddomen, npr. `example.com` -- `%host%` = celoten gostitelj, npr. `www.example.com` -- `%basePath%` = pot do korenskega direktorija - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Za podrobnejšo specifikacijo lahko uporabimo še razširjenejšo obliko, kjer poleg privzetih vrednosti lahko nastavimo tudi druge lastnosti parametrov, kot na primer validacijski regularni izraz (glej parameter `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Pomembno je opozoriti, da če parametri, definirani v polju, niso navedeni v maski poti, njihovih vrednosti ni mogoče spremeniti, niti s pomočjo poizvedbenih parametrov, navedenih za vprašajem v URL-ju. - - -Filtri in prevodi ------------------ - -Izvorne kode aplikacije pišemo v angleščini, vendar če naj ima spletno mesto slovenske URL-je, potem preprosto usmerjanje tipa: - -```php -$router->addRoute('/', 'Home:default'); -``` - -bo generiralo angleške URL-je, kot na primer `/product/123` ali `/cart`. Če želimo imeti presenterje in akcije v URL-ju predstavljene s slovenskimi besedami (npr. `/izdelek/123` ali `/kosarica`), lahko uporabimo prevodni slovar. Za njegov zapis že potrebujemo »bolj zgovorno« varianto drugega parametra: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // niz v URL => presenter - 'izdelek' => 'Product', - 'kosarica' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'seznam' => 'list', - ], - ], -]); -``` - -Več ključev prevodnega slovarja lahko vodi na isti presenter. S tem se zanj ustvarijo različni aliasi. Za kanonično varianto (torej tisto, ki bo v generiranem URL-ju) se šteje zadnji ključ. - -Prevodno tabelo lahko na ta način uporabimo za katerikoli parameter. Pri čemer, če prevod ne obstaja, se vzame prvotna vrednost. To obnašanje lahko spremenimo z dopolnitvijo `Route::FilterStrict => true` in pot potem zavrne URL, če vrednost ni v slovarju. - -Poleg prevodnega slovarja v obliki polja lahko uporabimo tudi lastne prevodne funkcije. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -Funkcija `Route::FilterIn` pretvarja med parametrom v URL-ju in nizom, ki se nato posreduje v presenter, funkcija `FilterOut` zagotavlja pretvorbo v nasprotno smer. - -Parametri `presenter`, `action` in `module` že imajo preddefinirane filtre, ki pretvarjajo med slogom PascalCase oz. camelCase in kebab-case, uporabljenim v URL-ju. Privzeta vrednost parametrov se zapisuje že v transformirani obliki, tako da na primer v primeru presenterja pišemo ``, ne pa ``. - - -Splošni filtri --------------- - -Poleg filtrov, namenjenih konkretnim parametrom, lahko definiramo tudi splošne filtre, ki prejmejo asociativno polje vseh parametrov, ki jih lahko kakorkoli modificirajo in nato vrnejo. Splošne filtre definiramo pod ključem `null`. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Splošni filtri dajejo možnost prilagoditi obnašanje poti na popolnoma kakršenkoli način. Lahko jih uporabimo na primer za modifikacijo parametrov na podlagi drugih parametrov. Na primer, prevajanje `` in `` na podlagi trenutne vrednosti parametra ``. - -Če ima parameter definiran lasten filter in hkrati obstaja splošni filter, se izvede lastni `FilterIn` pred splošnim in obratno splošni `FilterOut` pred lastnim. Torej znotraj splošnega filtra so vrednosti parametrov `presenter` oz. `action` zapisane v slogu PascalCase oz. camelCase. - - -Enosmerne poti OneWay ---------------------- - -Enosmerne poti se uporabljajo za ohranjanje funkcionalnosti starih URL-jev, ki jih aplikacija ne generira več, vendar jih še vedno sprejema. Označimo jih z zastavico `OneWay`: - -```php -// stari URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); -// novi URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -Pri dostopu do starega URL-ja presenter samodejno preusmeri na nov URL, tako da vam te strani iskalniki ne indeksirajo dvakrat (glej [#SEO in kanonizacija]). - - -Dinamično usmerjanje s povratnimi klici ---------------------------------------- - -Dinamično usmerjanje s povratnimi klici (callbacks) vam omogoča, da potem dodelite neposredno funkcije (callbacke), ki se izvedejo, ko je dana pot obiskana. Ta fleksibilna funkcionalnost vam omogoča hitro in učinkovito ustvarjanje različnih končnih točk (endpoints) za vašo aplikacijo: - -```php -$router->addRoute('test', function () { - echo 'ste na naslovu /test'; -}); -``` - -Lahko tudi definirate v maski parametre, ki se samodejno posredujejo v vaš callback: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'sl' => 'Dobrodošli na slovenski različici našega spletnega mesta!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - - -Moduli ------- - -Če imamo več poti, ki spadajo v skupni [modul |directory-structure#Presenterji in predloge], uporabimo `withModule()`: - -```php -$router = new RouteList; -$router->withModule('Forum') // naslednje poti so del modula Forum - ->addRoute('rss', 'Feed:rss') // presenter bo Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // naslednje poti so del modula Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Alternativa je uporaba parametra `module`: - -```php -// URL manage/dashboard/default se preslika na presenter Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - - -Poddomene ---------- - -Zbirke poti lahko členimo po poddomenah: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -V imenu domene lahko uporabimo tudi [#Nadomestni znaki]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Predpona poti -------------- - -Zbirke poti lahko členimo po poti v URL-ju: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // ujame URL /eshop/rss - ->addRoute('/'); // ujame URL /eshop// -``` - - -Kombinacije ------------ - -Zgoraj navedeno členjenje lahko medsebojno kombiniramo: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Poizvedbeni parametri ---------------------- - -Maske lahko vsebujejo tudi poizvedbene parametre (parametre za vprašajem v URL-ju). Tem ni mogoče definirati validacijskega izraza, vendar lahko spremenimo ime, pod katerim se posredujejo v presenter: - -```php -// poizvedbeni parameter 'cat' želimo v aplikaciji uporabiti pod imenom 'categoryId' -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Foo parametri -------------- - -Zdaj gremo že globlje. Foo parametri so v bistvu neimenovani parametri, ki omogočajo ujemanje regularnega izraza. Primer je pot, ki sprejema `/index`, `/index.html`, `/index.htm` in `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -Lahko tudi eksplicitno definiramo niz, ki bo uporabljen pri generiranju URL-ja. Niz mora biti umeščen neposredno za vprašajem. Naslednja pot je podobna prejšnji, vendar generira `/index.html` namesto `/index`, ker je niz `.html` nastavljen kot generacijska vrednost: - -```php -$router->addRoute('index', /* ... */); -``` - - -Vključitev v aplikacijo -======================= - -Da bi ustvarjeni usmerjevalnik vključili v aplikacijo, moramo o njem povedati DI vsebnika. Najlažja pot je pripraviti tovarno, ki bo objekt usmerjevalnika izdelala, in sporočiti v konfiguraciji vsebnika, da jo naj uporabi. Recimo, da za ta namen napišemo metodo `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -V [konfiguracijo |dependency-injection:services] nato zapišemo: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Kakršnekoli odvisnosti, na primer od podatkovne baze itd., se posredujejo tovarniški metodi kot njeni parametri s pomočjo [autowiringa |dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Veliko enostavnejši usmerjevalnik kot zbirka poti je [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Uporabimo ga takrat, ko nimamo posebnih zahtev glede oblike URL-ja, če ni na voljo `mod_rewrite` (ali njegove alternative) ali če zaenkrat ne želimo reševati lepih URL-jev. - -Generira naslove približno v tej obliki: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Parameter konstruktorja SimpleRouterja je privzeti presenter & akcija, na katerega naj se usmerja, če odpremo stran brez parametrov, npr. `http://example.com/`. - -```php -// privzeti presenter bo 'Home' in akcija 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Priporočamo, da SimpleRouter neposredno definirate v [konfiguraciji |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO in kanonizacija -=================== - -Ogrodje prispeva k SEO (optimizaciji najdljivosti na internetu) s tem, da preprečuje podvojenost vsebine na različnih URL-jih. Če do določenega cilja vodi več naslovov, npr. `/index` in `/index.html`, ogrodje prvega od njih določi za primarnega (kanoničnega) in ostale nanj preusmeri s pomočjo HTTP kode 301. Zahvaljujoč temu vam iskalniki strani ne indeksirajo dvakrat in ne razpršijo njihovega page ranka. - -Temu procesu rečemo kanonizacija. Kanonični URL je tisti, ki ga generira usmerjevalnik, tj. prva ustrezna pot v zbirki brez zastavice OneWay. Zato v zbirki navajamo **primarne poti kot prve**. - -Kanonizacijo izvaja presenter, več v poglavju [kanonizacija |presenters#Kanonizacija]. - - -HTTPS -===== - -Da bi lahko uporabljali HTTPS protokol, ga je treba omogočiti na gostovanju in pravilno konfigurirati strežnik. - -Preusmeritev celotnega spletnega mesta na HTTPS je treba nastaviti na ravni strežnika, na primer s pomočjo datoteke .htaccess v korenskem direktoriju naše aplikacije, in to s HTTP kodo 301. Nastavitev se lahko razlikuje glede na gostovanje in izgleda približno takole: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Usmerjevalnik generira URL z istim protokolom, s katerim je bila stran naložena, zato ni treba ničesar več nastavljati. - -Če pa izjemoma potrebujemo, da različne poti tečejo pod različnimi protokoli, ga navedemo v maski poti: - -```php -// Generiral bo naslov s HTTP -$router->addRoute('http://%host%//', /* ... */); - -// Generiral bo naslov s HTTPS -$router->addRoute('https://%host%//', /* ... */); -``` - - -Razhroščevanje usmerjevalnika -============================= - -Usmerjevalna plošča, ki se prikazuje v [Tracy Baru |tracy:], je koristen pomočnik, ki prikazuje seznam poti in tudi parametrov, ki jih je usmerjevalnik pridobil iz URL-ja. - -Zelena vrstica s simbolom ✓ predstavlja pot, ki je obdelala trenutni URL, z modro barvo in simbolom ≈ so označene poti, ki bi prav tako obdelale URL, če jih zelena ne bi prehitela. Nato vidimo trenutni presenter & akcijo. - -[* routing-debugger.webp *] - -Hkrati, če pride do nepričakovane preusmeritve zaradi [kanonizacije |#SEO in kanonizacija], je koristno pogledati v ploščo v vrstici *redirect*, kjer ugotovite, kako je usmerjevalnik URL prvotno razumel in zakaj je preusmeril. - -.[note] -Pri razhroščevanju usmerjevalnika priporočamo, da v brskalniku odprete Developer Tools (Ctrl+Shift+I ali Cmd+Option+I) in v plošči Network izklopite predpomnilnik, da se vanj ne shranjujejo preusmeritve. - - -Zmogljivost -=========== - -Število poti vpliva na hitrost usmerjevalnika. Njihovo število zagotovo ne bi smelo preseči nekaj deset. Če ima vaše spletno mesto preveč zapleteno strukturo URL-jev, si lahko napišete po meri [#Lasten usmerjevalnik]. - -Če usmerjevalnik nima nobenih odvisnosti, na primer od podatkovne baze, in njegova tovarna ne sprejema nobenih argumentov, lahko njegovo sestavljeno obliko serializiramo neposredno v DI vsebnik in s tem aplikacijo nekoliko pospešimo. - -```neon -routing: - cache: true -``` - - -Lasten usmerjevalnik -==================== - -Naslednje vrstice so namenjene zelo naprednim uporabnikom. Lahko si ustvarite lasten usmerjevalnik in ga popolnoma naravno vključite v zbirko poti. Usmerjevalnik je implementacija vmesnika [api:Nette\Routing\Router] z dvema metodama: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -Metoda `match` obdela trenutni zahtevek [$httpRequest |http:request], iz katerega lahko pridobimo ne samo URL, ampak tudi glave itd., v polje, ki vsebuje ime presenterja in njegove parametre. Če zahtevka ne zna obdelati, vrne null. Pri obdelavi zahtevka moramo vrniti vsaj presenter in akcijo. Ime presenterja je popolno in vsebuje tudi morebitne module: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -Metoda `constructUrl` nasprotno sestavi iz polja parametrov končni absolutni URL. Pri tem lahko uporabi informacije iz parametra [`$refUrl`|api:Nette\Http\UrlScript], kar je trenutni URL. - -V zbirko poti ga dodate s pomočjo `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Samostojna uporaba -================== - -Samostojna uporaba pomeni uporabo sposobnosti usmerjevalnika v aplikaciji, ki ne uporablja Nette Application in presenterjev. Zanj velja skoraj vse, kar smo si v tem poglavju pokazali, s temi razlikami: - -- za zbirke poti uporabljamo razred [api:Nette\Routing\RouteList] -- kot preprost usmerjevalnik razred [api:Nette\Routing\SimpleRouter] -- ker ne obstaja par `Presenter:action`, uporabljamo [#Razširjeni zapis] - -Torej spet ustvarimo metodo, ki nam bo sestavila usmerjevalnik, npr.: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Če uporabljate DI vsebnik, kar priporočamo, spet metodo dodamo v konfiguracijo in nato usmerjevalnik skupaj s HTTP zahtevkom pridobimo iz vsebnika: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Ali pa objekte neposredno izdelamo: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Zdaj preostane le še, da usmerjevalnik spustimo k delu: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // ni bila najdena ustrezna pot, pošljemo napako 404 - exit; -} - -// obdelamo pridobljene parametre -$controller = $params['controller']; -// ... -``` - -In obratno uporabimo usmerjevalnik za sestavljanje povezave: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/router}} diff --git a/application/sl/templates.texy b/application/sl/templates.texy deleted file mode 100644 index 394f2baae2..0000000000 --- a/application/sl/templates.texy +++ /dev/null @@ -1,323 +0,0 @@ -Predloge -******** - -.[perex] -Nette uporablja sistem predlog [Latte |latte:]. Delsno zato, ker gre za najbolj varen sistem predlog za PHP, in hkrati tudi najbolj intuitiven sistem. Ni se vam treba učiti veliko novega, zadostuje znanje PHP in nekaj značk. - -Običajno je, da se stran sestavi iz predloge postavitve + predloge dane akcije. Takole na primer lahko izgleda predloga postavitve, opazite bloke `{block}` in značko `{include}`: - -```latte - - - - {block title}Moja Aplikacija{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -In tole bo predloga akcije: - -```latte -{block title}Domača stran{/block} - -{block content} -

    Domača stran

    -... -{/block} -``` - -Ta definira blok `content`, ki se vstavi na mesto `{include content}` v postavitvi, in tudi ponovno definira blok `title`, s katerim prepiše `{block title}` v postavitvi. Poskusite si predstavljati rezultat. - - -Iskanje predlog ---------------- - -Ni vam treba v presenterjih navajati, katera predloga naj se izriše, ogrodje pot izpelje samo in vam prihrani pisanje. - -Če uporabljate strukturo map, kjer ima vsak presenter svojo mapo, preprosto namestite predlogo v to mapo pod imenom akcije (oz. view), tj. za akcijo `default` uporabite predlogo `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Če uporabljate strukturo, kjer so skupaj presenterji v eni mapi in predloge v mapi `templates`, jo shranite bodisi v datoteko `..latte` ali `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1. varianta - └── Home/ - └── default.latte ← 2. varianta -\-- - -Mapa `templates` je lahko nameščena tudi eno raven višje, tj. na isti ravni, kot je mapa z razredi presenterjev. - -Če predloga ni najdena, presenter odgovori z [napako 404 - stran ni najdena |presenters#Napaka 404 in podobno]. - -View spremenite s pomočjo `$this->setView('jineView')`. Prav tako lahko neposredno določite datoteko s predlogo s pomočjo `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Datoteke, kjer se iščejo predloge, lahko spremenite s prepisom metode [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], ki vrne polje možnih imen datotek. - - -Iskanje predloge postavitve ---------------------------- - -Nette tudi samodejno išče datoteko s postavitvijo. - -Če uporabljate strukturo map, kjer ima vsak presenter svojo mapo, namestite postavitev bodisi v mapo s presenterjem, če je specifična samo zanj, ali eno raven višje, če je skupna za več presenterjev: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← skupna postavitev - └── Home/ - ├── @layout.latte ← samo za presenter Home - ├── HomePresenter.php - └── default.latte -\-- - -Če uporabljate strukturo, kjer so skupaj presenterji v eni mapi in predloge v mapi `templates`, se bo postavitev pričakovala na teh mestih: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← skupna postavitev - ├── Home.@layout.latte ← samo za Home, 1. varianta - └── Home/ - └── @layout.latte ← samo za Home, 2. varianta -\-- - -Če se presenter nahaja v modulu, se bo iskalo tudi na višjih ravneh map, glede na gnezdenje modula. - -Ime postavitve lahko spremenite s pomočjo `$this->setLayout('layoutAdmin')` in potem se bo pričakovalo v datoteki `@layoutAdmin.latte`. Prav tako lahko neposredno določite datoteko s predlogo postavitve s pomočjo `$this->setLayout('/path/to/template.latte')`. - -S pomočjo `$this->setLayout(false)` ali značke `{layout none}` znotraj predloge se iskanje postavitve izklopi. - -.[note] -Datoteke, kjer se iščejo predloge postavitve, lahko spremenite s prepisom metode [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], ki vrne polje možnih imen datotek. - - -Spremenljivke v predlogi ------------------------- - -Spremenljivke v predlogo posredujemo tako, da jih zapišemo v `$this->template` in potem jih imamo na voljo v predlogi kot lokalne spremenljivke: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Tako enostavno lahko v predloge posredujemo kakršnekoli spremenljivke. Pri razvoju robustnih aplikacij pa je običajno bolj koristno se omejiti. Na primer tako, da eksplicitno definiramo seznam spremenljivk, ki jih predloga pričakuje, in njihovih tipov. Zahvaljujoč temu nam bo lahko PHP preverjal tipe, IDE pravilno predlagal in statična analiza odkrivala napake. - -In kako takšen seznam definiramo? Preprosto v obliki razreda in njegovih lastnosti. Poimenujemo ga podobno kot presenter, le s `Template` na koncu: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // in druge spremenljivke -} -``` - -Objekt `$this->template` v presenterju bo zdaj instanca razreda `ArticleTemplate`. Tako bo PHP pri zapisu preverjal deklarirane tipe. In od različice PHP 8.2 naprej bo opozoril tudi na zapis v neobstoječo spremenljivko, v prejšnjih različicah lahko isto dosežemo z uporabo traite [Nette\SmartObject |utils:smartobject]. - -Anotacija `@property-read` je namenjena za IDE in statično analizo, zahvaljujoč njej bo delovalo predlaganje, glej "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Luksuza predlaganja si lahko privoščite tudi v predlogah, dovolj je v PhpStorm namestiti vtičnik za Latte in navesti na začetek predloge ime razreda, več v članku "Latte: kako do tipskega sistema":https://blog.nette.org/sl/latte-how-to-use-type-system: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Tako delujejo tudi predloge v komponentah, dovolj je le upoštevati imensko konvencijo in za komponento npr. `FifteenControl` ustvariti razred predloge `FifteenTemplate`. - -Če potrebujete ustvariti `$template` kot instanco drugega razreda, uporabite metodo `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Privzete spremenljivke ----------------------- - -Presenterji in komponente samodejno posredujejo v predloge nekaj uporabnih spremenljivk: - -- `$basePath` je absolutna URL pot do korenskega direktorija (npr. `/eshop`) -- `$baseUrl` je absolutni URL do korenskega direktorija (npr. `http://localhost/eshop`) -- `$user` je objekt [ki predstavlja uporabnika |security:authentication] -- `$presenter` je trenutni presenter -- `$control` je trenutna komponenta ali presenter -- `$flashes` polje [sporočil |presenters#Flash sporočila] poslanih s funkcijo `flashMessage()` - -Če uporabljate lasten razred predloge, se te spremenljivke posredujejo, če zanje ustvarite lastnost. - - -Ustvarjanje povezav -------------------- - -V predlogi se ustvarjajo povezave na druge presenterje & akcije na ta način: - -```latte -podrobnosti izdelka -``` - -Atribut `n:href` je zelo priročen za HTML značke ``. Če želimo povezavo izpisati drugje, na primer v besedilu, uporabimo `{link}`: - -```latte -Naslov je: {link Home:default} -``` - -Več informacij najdete v poglavju [Ustvarjanje URL povezav |creating-links]. - - -Lastni filtri, značke ipd. --------------------------- - -Sistem predlog Latte lahko razširimo z lastnimi filtri, funkcijami, značkami ipd. To lahko storimo neposredno v metodi `render` ali `beforeRender()`: - -```php -public function beforeRender(): void -{ - // dodajanje filtra - $this->template->addFilter('foo', /* ... */); - - // ali konfiguriramo neposredno objekt Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -Latte v različici 3 ponuja naprednejši način in to je ustvarjanje si [extension |latte:extending-latte#Latte Extension] za vsak spletni projekt. Primer takšnega razreda: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Registriramo jo s pomočjo [konfiguracije |configuration#Predloge Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Prevajanje ----------- - -Če programirate večjezično aplikacijo, boste najverjetneje potrebovali nekatera besedila v predlogi izpisati v različnih jezikih. Nette Framework za ta namen definira vmesnik za prevajanje [api:Nette\Localization\Translator], ki ima eno samo metodo `translate()`. Ta sprejema sporočilo `$message`, kar je praviloma niz, in poljubne druge parametre. Naloga je vrniti preveden niz. V Nette ni nobene privzete implementacije, lahko si izberete glede na svoje potrebe iz več pripravljenih rešitev, ki jih najdete na [Componette |https://componette.org/search/localization]. V njihovi dokumentaciji boste izvedeli, kako prevajalnik konfigurirati. - -Predlogam lahko nastavimo prevajalnik, ki si ga [pustimo posredovati |dependency-injection:passing-dependencies], z metodo `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Prevajalnik je alternativno mogoče nastaviti s pomočjo [konfiguracije |configuration#Predloge Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Nato lahko prevajalnik uporabljamo na primer kot filter `|translate`, in to vključno z dopolnilnimi parametri, ki se posredujejo metodi `translate()` (glej `foo, bar`): - -```latte -{='Košarica'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Ali kot podčrtajno značko: - -```latte -{_'Košarica'} -{_$item} -{_$item, foo, bar} -``` - -Za prevod odseka predloge obstaja parna značka `{translate}` (od Latte 2.11, prej se je uporabljala značka `{_}`): - -```latte -{translate}Naročilo{/translate} -{translate foo, bar}Naročilo{/translate} -``` - -Prevajalnik se standardno kliče med izvajanjem pri izrisovanju predloge. Latte različice 3 pa zna vsa statična besedila prevajati že med kompilacijo predloge. S tem se prihrani zmogljivost, ker se vsak niz prevede samo enkrat in končni prevod se zapiše v prevedeno obliko. V mapi s predpomnilnikom tako nastane več prevedenih različic predloge, ena za vsak jezik. Za to je dovolj le navesti jezik kot drugi parameter: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Statično besedilo je mišljeno na primer `{_'hello'}` ali `{translate}hello{/translate}`. Nestatična besedila, kot na primer `{_$foo}`, se bodo še naprej prevajala med izvajanjem. diff --git a/application/uk/@home.texy b/application/uk/@home.texy deleted file mode 100644 index 5627e56f5d..0000000000 --- a/application/uk/@home.texy +++ /dev/null @@ -1,85 +0,0 @@ -Nette Application -***************** - -.[perex] -Nette Application є ядром фреймворку Nette, яке надає потужні інструменти для створення сучасних веб-застосунків. Воно пропонує низку виняткових функцій, які значно полегшують розробку та покращують безпеку й підтримуваність коду. - - -Встановлення ------------- - -Бібліотеку можна завантажити та встановити за допомогою інструменту [Composer|best-practices:composer]: - -```shell -composer require nette/application -``` - - -Чому варто обрати Nette Application? ------------------------------------- - -Nette завжди був піонером у галузі веб-технологій. - -**Двосторонній роутер:** Nette має вдосконалену систему маршрутизації, яка є унікальною завдяки своїй двосторонності — вона не тільки перетворює URL-адреси на дії застосунку, але й може генерувати URL-адреси у зворотному напрямку. Це означає, що: -- Ви можете будь-коли змінити структуру URL-адрес усього застосунку без необхідності редагувати шаблони -- URL-адреси автоматично канонізуються, що покращує SEO -- Маршрутизація визначається в одному місці, а не розкидана по анотаціях - -**Компоненти та сигнали:** Вбудована система компонентів, натхненна Delphi та React.js, є абсолютно унікальною серед PHP-фреймворків: -- Дозволяє створювати багаторазові UI-елементи -- Підтримує ієрархічне складання компонентів -- Пропонує елегантну обробку AJAX-запитів за допомогою сигналів -- Багата бібліотека готових компонентів на [Componette](https://componette.org) - -**AJAX та сніпети:** Nette представив революційний спосіб роботи з AJAX ще у 2009 році, задовго до появи подібних рішень, таких як Hotwire для Ruby on Rails або Symfony UX Turbo: -- Сніпети дозволяють оновлювати лише частини сторінки без необхідності писати JavaScript -- Автоматична інтеграція з компонентною системою -- Розумна інвалідація частин сторінок -- Мінімальна кількість переданих даних - -**Інтуїтивні шаблони [Latte|latte:]:** Найбезпечніша система шаблонів для PHP з розширеними функціями: -- Автоматичний захист від XSS за допомогою контекстно-залежного екранування -- Розширюваність за допомогою власних фільтрів, функцій та тегів -- Спадкування шаблонів та сніпети для AJAX -- Відмінна підтримка PHP 8.x з системою типів - -**Dependency Injection:** Nette повністю використовує Dependency Injection: -- Автоматична передача залежностей (autowiring) -- Конфігурація за допомогою зрозумілого формату NEON -- Підтримка фабрик для компонентів - - -Основні переваги ----------------- - -- **Безпека**: Автоматичний захист від [вразливостей|nette:vulnerability-protection], таких як XSS, CSRF тощо. -- **Продуктивність**: Менше коду, більше функцій завдяки розумному дизайну -- **Налагодження**: [Tracy debugger|tracy:] з панеллю маршрутизації -- **Швидкодія**: Розумний кеш, ліниве завантаження компонентів -- **Гнучкість**: Легка зміна URL-адрес навіть після завершення розробки застосунку -- **Компоненти**: Унікальна система багаторазових UI-елементів -- **Сучасність**: Повна підтримка PHP 8.4+ та системи типів - - -Починаємо ---------- - -1. [Як працюють застосунки? |how-it-works] - Розуміння базової архітектури -2. [Presenters |presenters] - Робота з презентерами та діями -3. [Шаблони |templates] - Створення шаблонів у Latte -4. [Маршрутизація |routing] - Конфігурація URL-адрес -5. [Інтерактивні компоненти |components] - Використання компонентної системи - - -Сумісність з PHP ----------------- - -| версія | сумісна з 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 - -Застосовується до останньої версії патчу. diff --git a/application/uk/@left-menu.texy b/application/uk/@left-menu.texy deleted file mode 100644 index 5ad8904c1a..0000000000 --- a/application/uk/@left-menu.texy +++ /dev/null @@ -1,22 +0,0 @@ -Nette Application -***************** -- [Як працюють застосунки? |how-it-works] -- [Bootstrapping] -- [Presenters |presenters] -- [Шаблони |templates] -- [Структура каталогів |directory-structure] -- [Маршрутизація |routing] -- [Створення посилань URL |creating-links] -- [Інтерактивні компоненти |components] -- [AJAX & сніпети |ajax] -- [Multiplier |Multiplier] -- [Конфігурація |configuration] - - -Додаткове читання -***************** -- [Чому варто використовувати Nette? |www:10-reasons-why-nette] -- [Встановлення |nette:installation] -- [Пишемо перший застосунок! |quickstart:] -- [Посібники та практики |best-practices:] -- [Вирішення проблем |nette:troubleshooting] diff --git a/application/uk/@meta.texy b/application/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/application/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/application/uk/ajax.texy b/application/uk/ajax.texy deleted file mode 100644 index 92d0d2837a..0000000000 --- a/application/uk/ajax.texy +++ /dev/null @@ -1,249 +0,0 @@ -AJAX & сніпети -************** - -
    - -В епоху сучасних веб-застосунків, де функціональність часто розподілена між сервером і браузером, AJAX є необхідним сполучним елементом. Які можливості пропонує нам Nette Framework у цій галузі? -- надсилання частин шаблону, так званих сніпетів -- передача змінних між PHP і JavaScript -- інструменти для налагодження AJAX-запитів - -
    - - -AJAX-запит -========== - -AJAX-запит, по суті, не відрізняється від класичного HTTP-запиту. Викликається presenter із певними параметрами. І від presenter'а залежить, як він реагуватиме на запит - він може повернути дані у форматі JSON, надіслати частину HTML-коду, XML-документ тощо. - -На стороні браузера ми ініціюємо AJAX-запит за допомогою функції `fetch()`: - -```js -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -.then(response => response.json()) -.then(payload => { - // обробка відповіді -}); -``` - -На стороні сервера ми розпізнаємо AJAX-запит за допомогою методу `$httpRequest->isAjax()` сервісу [що інкапсулює HTTP-запит |http:request]. Для виявлення він використовує HTTP-заголовок `X-Requested-With`, тому важливо його надсилати. У presenter'і можна використовувати метод `$this->isAjax()`. - -Якщо ви хочете надіслати дані у форматі JSON, використовуйте метод [`sendJson()` |presenters#Надсилання відповіді]. Метод також завершує роботу presenter'а. - -```php -public function actionExport(): void -{ - $this->sendJson($this->model->getData); -} -``` - -Якщо ви плануєте відповісти за допомогою спеціального шаблону, призначеного для AJAX, ви можете зробити це так: - -```php -public function handleClick($param): void -{ - if ($this->isAjax()) { - $this->template->setFile('path/to/ajax.latte'); - } - // ... -} -``` - - -Сніпети -======= - -Найпотужнішим засобом, який пропонує Nette для зв'язку сервера з клієнтом, є сніпети. Завдяки їм ви можете перетворити звичайний застосунок на AJAX-застосунок з мінімальними зусиллями та кількома рядками коду. Як це все працює, демонструє приклад Fifteen, код якого ви знайдете на [GitHub |https://github.com/nette-examples/fifteen]. - -Сніпети, або фрагменти, дозволяють оновлювати лише частини сторінки, замість того, щоб перезавантажувати всю сторінку. Це не тільки швидше та ефективніше, але й забезпечує більш комфортний користувацький досвід. Сніпети можуть нагадувати вам Hotwire для Ruby on Rails або Symfony UX Turbo. Цікаво, що Nette представило сніпети на 14 років раніше. - -Як працюють сніпети? При першому завантаженні сторінки (не AJAX-запит) завантажується вся сторінка, включно з усіма сніпетами. Коли користувач взаємодіє зі сторінкою (наприклад, натискає кнопку, надсилає форму тощо), замість завантаження всієї сторінки викликається AJAX-запит. Код у presenter'і виконує дію і вирішує, які сніпети потрібно оновити. Nette рендерить ці сніпети та надсилає їх у вигляді масиву у форматі JSON. Обробний код у браузері отримує сніпети та вставляє їх назад у сторінку. Таким чином, передається лише код змінених сніпетів, що економить пропускну здатність і прискорює завантаження порівняно з передачею вмісту всієї сторінки. - - -Naja ----- - -Для обробки сніпетів на стороні браузера використовується [бібліотека Naja |https://naja.js.org]. Її [встановіть |https://naja.js.org/#/guide/01-install-setup-naja] як пакет node.js (для використання з застосунками Webpack, Rollup, Vite, Parcel та іншими): - -```shell -npm install naja -``` - -…або безпосередньо вставте в шаблон сторінки: - -```latte - -``` - -Спочатку потрібно бібліотеку [ініціалізувати |https://naja.js.org/#/guide/01-install-setup-naja?id=initialization]: - -```js -naja.initialize(); -``` - -Щоб перетворити звичайне посилання (сигнал) або надсилання форми на AJAX-запит, достатньо позначити відповідне посилання, форму або кнопку класом `ajax`: - -```latte -Перейти - -
    - -
    - -або - -
    - -
    -``` - - -Перемальовування сніпетів -------------------------- - -Кожен об'єкт класу [Control |components] (включно з самим Presenter'ом) відстежує, чи відбулися зміни, що вимагають його перемальовування. Для цього використовується метод `redrawControl()`: - -```php -public function handleLogin(string $user): void -{ - // після входу потрібно перемалювати відповідну частину - $this->redrawControl(); - // ... -} -``` - -Nette дозволяє ще більш точно контролювати, що саме потрібно перемалювати. Згаданий метод може приймати як аргумент назву сніпета. Таким чином, можна інвалідувати (тобто: змусити перемалювати) на рівні частин шаблону. Якщо інвалідується весь компонент, то перемальовується і кожен його сніпет: - -```php -// інвалідує сніпет 'header' -$this->redrawControl('header'); -``` - - -Сніпети в Latte ---------------- - -Використання сніпетів у Latte надзвичайно просте. Щоб визначити частину шаблону як сніпет, просто оберніть її тегами `{snippet}` та `{/snippet}`: - -```latte -{snippet header} -

    Привіт ...

    -{/snippet} -``` - -Сніпет створює в HTML-сторінці елемент `
    ` зі спеціальним згенерованим `id`. При перемальовуванні сніпета оновлюється вміст цього елемента. Тому необхідно, щоб при первинному відображенні сторінки відображалися також усі сніпети, навіть якщо вони спочатку можуть бути порожніми. - -Ви можете створити сніпет з іншим елементом, ніж `
    `, за допомогою n:атрибута: - -```latte -
    -

    Привіт ...

    -
    -``` - - -Області сніпетів ----------------- - -Назви сніпетів також можуть бути виразами: - -```latte -{foreach $items as $id => $item} -
  • {$item}
  • -{/foreach} -``` - -Таким чином, у нас виникне кілька сніпетів `item-0`, `item-1` тощо. Якщо ми безпосередньо інвалідуємо динамічний сніпет (наприклад, `item-1`), нічого не перемалюється. Причина в тому, що сніпети справді працюють як вирізки і відображаються лише безпосередньо вони самі. Але в шаблоні фактично немає жодного сніпета з назвою `item-1`. Він виникає лише при виконанні коду навколо сніпета, тобто циклу foreach. Тому позначимо частину шаблону, яка має виконатися, за допомогою тегу `{snippetArea}`: - -```latte -
      - {foreach $items as $id => $item} -
    • {$item}
    • - {/foreach} -
    -``` - -І змусимо перемалювати як сам сніпет, так і всю батьківську область: - -```php -$this->redrawControl('itemsContainer'); -$this->redrawControl('item-1'); -``` - -Водночас бажано забезпечити, щоб масив `$items` містив лише ті елементи, які потрібно перемалювати. - -Якщо ми вставляємо в шаблон за допомогою тегу `{include}` інший шаблон, який містить сніпети, необхідно вставлення шаблону знову включити в `snippetArea` і інвалідувати його разом зі сніпетом: - -```latte -{snippetArea include} - {include 'included.latte'} -{/snippetArea} -``` - -```latte -{* included.latte *} -{snippet item} - ... -{/snippet} -``` - -```php -$this->redrawControl('include'); -$this->redrawControl('item'); -``` - - -Сніпети в компонентах ---------------------- - -Ви можете створювати сніпети і в [компонентах|components], і Nette буде автоматично їх перемальовувати. Але тут є певне обмеження: для перемальовування сніпетів викликається метод `render()` без параметрів. Тобто передача параметрів у шаблоні не працюватиме: - -```latte -OK -{control productGrid} - -не працюватиме: -{control productGrid $arg, $arg} -{control productGrid:paginator} -``` - - -Надсилання користувацьких даних -------------------------------- - -Разом зі сніпетами ви можете надсилати клієнту будь-які інші дані. Достатньо записати їх в об'єкт `payload`: - -```php -public function actionDelete(int $id): void -{ - // ... - if ($this->isAjax()) { - $this->payload->message = 'Успішно'; - } -} -``` - - -Передача параметрів -=================== - -Якщо ми надсилаємо компоненту параметри за допомогою AJAX-запиту, чи то параметри сигналу, чи персистентні параметри, ми повинні вказати у запиті їхню глобальну назву, яка містить також ім'я компонента. Повну назву параметра повертає метод `getParameterId()`. - -```js -let url = new URL({link //foo!}); -url.searchParams.set({$control->getParameterId('bar')}, bar); - -fetch(url, { - headers: {'X-Requested-With': 'XMLHttpRequest'}, -}) -``` - -І метод handle з відповідними параметрами в компоненті: - -```php -public function handleFoo(int $bar): void -{ -} -``` diff --git a/application/uk/bootstrapping.texy b/application/uk/bootstrapping.texy deleted file mode 100644 index 593350fa04..0000000000 --- a/application/uk/bootstrapping.texy +++ /dev/null @@ -1,297 +0,0 @@ -Завантаження -************ - -
    - -Завантаження — це процес ініціалізації середовища додатка, створення контейнера впровадження залежностей (DI) та запуску додатка. Ми обговоримо: - -- як клас Bootstrap ініціалізує середовище -- як додатки налаштовуються за допомогою NEON файлів -- як розрізняти режим виробництва та розробки -- як створити та налаштувати DI контейнер - -
    - - -Застосунки, чи то веб-застосунки, чи скрипти, що запускаються з командного рядка, починають свою роботу з певної форми ініціалізації середовища. У давні часи за це відповідав файл з назвою, наприклад, `include.inc.php`, який включався первинним файлом. У сучасних застосунках Nette його замінив клас `Bootstrap`, який як частину застосунку ви знайдете у файлі `app/Bootstrap.php`. Він може виглядати, наприклад, так: - -```php -use Nette\Bootstrap\Configurator; - -class Bootstrap -{ - private Configurator $configurator; - private string $rootDir; - - public function __construct() - { - $this->rootDir = dirname(__DIR__); - // Configurator відповідає за налаштування середовища застосунку та сервісів. - $this->configurator = new Configurator; - // Встановлює каталог для тимчасових файлів, що генеруються Nette (наприклад, скомпільовані шаблони) - $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 розумний, і режим розробки вмикається автоматично, - // або ви можете ввімкнути його для конкретної IP-адреси, розкоментувавши наступний рядок: - // $this->configurator->setDebugMode('secret@23.75.345.200'); - - // Активує Tracy: неперевершений "швейцарський ніж" для налагодження. - $this->configurator->enableTracy($this->rootDir . '/log'); - - // RobotLoader: автоматично завантажує всі класи у вибраному каталозі - $this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); - } - - private function setupContainer(): void - { - // Завантажує конфігураційні файли - $this->configurator->addConfig($this->rootDir . '/config/common.neon'); - } -} -``` - - -index.php -========= - -Первинним файлом у випадку веб-застосунків є `index.php`, який знаходиться у [публічному каталозі |directory-structure#Публічний каталог www] `www/`. Він отримує від класу Bootstrap ініціалізацію середовища та створення DI-контейнера. Потім з нього отримує сервіс `Application`, який запускає веб-застосунок: - -```php -$bootstrap = new App\Bootstrap; -// Ініціалізація середовища + створення DI-контейнера -$container = $bootstrap->bootWebApplication(); -// DI-контейнер створює об'єкт Nette\Application\Application -$application = $container->getByType(Nette\Application\Application::class); -// Запуск застосунку Nette та обробка вхідного запиту -$application->run(); -``` - -Як бачимо, з налаштуванням середовища та створенням DI-контейнера (впровадження залежностей) допомагає клас [api:Nette\Bootstrap\Configurator], який ми зараз детальніше розглянемо. - - -Режим розробки проти робочого режиму -==================================== - -Nette поводиться по-різному залежно від того, чи працює він на сервері розробки чи на робочому сервері: - -🛠️ Режим розробки (Development): - - Показує панель налагодження Tracy з корисною інформацією (SQL-запити, час виконання, використана пам'ять) - - У разі помилки показує детальну сторінку помилки з викликами функцій та вмістом змінних - - Автоматично оновлює кеш при зміні шаблонів Latte, редагуванні конфігураційних файлів тощо. - - -🚀 Робочий режим (Production): - - Не показує жодної налагоджувальної інформації, всі помилки записує в лог - - У разі помилки показує ErrorPresenter або загальну сторінку "Server Error" - - Кеш ніколи автоматично не оновлюється! - - Оптимізований для швидкості та безпеки - - -Вибір режиму здійснюється автовизначенням, тому зазвичай не потрібно нічого налаштовувати або вручну перемикати: - -- режим розробки: на localhost (IP-адреса `127.0.0.1` або `::1`), якщо немає проксі (тобто її HTTP-заголовка) -- робочий режим: скрізь в інших місцях - -Якщо ми хочемо ввімкнути режим розробки і в інших випадках, наприклад, для програмістів, що підключаються з конкретної IP-адреси, використовуємо `setDebugMode()`: - -```php -$this->configurator->setDebugMode('23.75.345.200'); // можна вказати і масив IP-адрес -``` - -Однозначно рекомендуємо комбінувати IP-адресу з cookie. У cookie `nette-debug` збережемо секретний токен, наприклад, `secret1234`, і таким чином активуємо режим розробки для програмістів, що підключаються з конкретної IP-адреси та мають у cookie згаданий токен: - -```php -$this->configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Режим розробки можна також повністю вимкнути, навіть для localhost: - -```php -$this->configurator->setDebugMode(false); -``` - -Увага, значення `true` вмикає режим розробки примусово, що ніколи не повинно статися на робочому сервері. - - -Інструмент налагодження Tracy -============================= - -Для легкого налагодження ще ввімкнемо чудовий інструмент [Tracy |tracy:]. У режимі розробки він візуалізує помилки, а в робочому режимі помилки логує до вказаного каталогу: - -```php -$this->configurator->enableTracy($this->rootDir . '/log'); -``` - - -Тимчасові файли -=============== - -Nette використовує кеш для DI-контейнера, RobotLoader, шаблонів тощо. Тому необхідно встановити шлях до каталогу, куди буде зберігатися кеш: - -```php -$this->configurator->setTempDirectory($this->rootDir . '/temp'); -``` - -На Linux або macOS встановіть для каталогів `log/` та `temp/` [права на запис |nette:troubleshooting#Налаштування прав доступу до каталогів]. - - -RobotLoader -=========== - -Зазвичай ми захочемо автоматично завантажувати класи за допомогою [RobotLoader |robot-loader:], тому ми повинні його запустити і дозволити йому завантажувати класи з каталогу, де знаходиться `Bootstrap.php` (тобто `__DIR__`), та всіх підкаталогів: - -```php -$this->configurator->createRobotLoader() - ->addDirectory(__DIR__) - ->register(); -``` - -Альтернативний підхід — дозволити завантажувати класи лише через [Composer |best-practices:composer], дотримуючись PSR-4. - - -Часовий пояс -============ - -За допомогою конфігуратора ви можете встановити стандартний часовий пояс. - -```php -$this->configurator->setTimeZone('Europe/Kyiv'); -``` - - -Конфігурація DI-контейнера -========================== - -Частиною процесу завантаження є створення DI-контейнера, або фабрики об'єктів, що є серцем усього застосунку. Це фактично PHP-клас, який генерує Nette і зберігає в каталозі з кешем. Фабрика виробляє ключові об'єкти застосунку, і за допомогою конфігураційних файлів ми інструктуємо її, як їх створювати та налаштовувати, чим впливаємо на поведінку всього застосунку. - -Конфігураційні файли зазвичай записуються у форматі [NEON |neon:format]. В окремому розділі ви дізнаєтеся, [що можна налаштувати |nette:configuring]. - -.[tip] -У режимі розробки контейнер автоматично оновлюється при кожній зміні коду або конфігураційних файлів. У робочому режимі він генерується лише один раз, і зміни не перевіряються для максимальної продуктивності. - -Конфігураційні файли завантажуємо за допомогою `addConfig()`: - -```php -$this->configurator->addConfig($this->rootDir . '/config/common.neon'); -``` - -Якщо ми хочемо додати більше конфігураційних файлів, ми можемо викликати функцію `addConfig()` кілька разів. - -```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'); -} -``` - -Назва `cli.php` не є помилкою, конфігурація може бути записана також у PHP-файлі, який повертає її як масив. - -Також ми можемо додати інші конфігураційні файли в [секції `includes` |dependency-injection:configuration#Включення файлів]. - -Якщо в конфігураційних файлах з'являються елементи з однаковими ключами, вони будуть перезаписані, або у випадку [масивів об'єднані |dependency-injection:configuration#Об єднання]. Файл, що завантажується пізніше, має вищий пріоритет, ніж попередній. Файл, у якому вказана секція `includes`, має вищий пріоритет, ніж файли, що в ньому включені. - - -Статичні параметри ------------------- - -Параметри, що використовуються в конфігураційних файлах, ми можемо визначити [у секції `parameters` |dependency-injection:configuration#Параметри], а також передавати (чи перезаписувати) їх методом `addStaticParameters()` (має псевдонім `addParameters()`). Важливо, що різні значення параметрів спричинять генерацію додаткових DI-контейнерів, тобто додаткових класів. - -```php -$this->configurator->addStaticParameters([ - 'projectId' => 23, -]); -``` - -На параметр `projectId` можна посилатися в конфігурації звичайним записом `%projectId%`. - - -Динамічні параметри -------------------- - -До контейнера ми можемо додати й динамічні параметри, різні значення яких, на відміну від статичних параметрів, не спричиняють генерації нових DI-контейнерів. - -```php -$this->configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Таким чином, ми можемо легко додати, наприклад, змінні середовища, на які потім можна посилатися в конфігурації записом `%env.variable%`. - -```php -$this->configurator->addDynamicParameters([ - 'env' => getenv(), -]); -``` - - -Стандартні параметри --------------------- - -У конфігураційних файлах ви можете використовувати ці статичні параметри: - -- `%appDir%` — абсолютний шлях до каталогу з файлом `Bootstrap.php` -- `%wwwDir%` — абсолютний шлях до каталогу з вхідним файлом `index.php` -- `%tempDir%` — абсолютний шлях до каталогу для тимчасових файлів -- `%vendorDir%` — абсолютний шлях до каталогу, куди Composer встановлює бібліотеки -- `%rootDir%` — абсолютний шлях до кореневого каталогу проєкту -- `%debugMode%` — вказує, чи перебуває застосунок у режимі налагодження -- `%consoleMode%` — вказує, чи прийшов запит через командний рядок - - -Імпортовані сервіси -------------------- - -Тепер ми заглиблюємося. Хоча сенс DI-контейнера полягає у створенні об'єктів, винятково може виникнути потреба вставити в контейнер існуючий об'єкт. Ми робимо це, визначаючи сервіс з прапорцем `imported: true`. - -```neon -services: - myservice: - type: App\Model\MyCustomService - imported: true -``` - -І в bootstrap ми вставляємо об'єкт у контейнер: - -```php -$this->configurator->addServices([ - 'myservice' => new App\Model\MyCustomService('foobar'), -]); -``` - - -Різне середовище -================ - -Не бійтеся змінювати клас Bootstrap відповідно до ваших потреб. Методу `bootWebApplication()` ви можете додати параметри для розрізнення веб-проектів. Або ми можемо додати інші методи, наприклад `bootTestEnvironment()`, який ініціалізує середовище для юніт-тестів, `bootConsoleApplication()` для скриптів, що викликаються з командного рядка, тощо. - -```php -public function bootTestEnvironment(): Nette\DI\Container -{ - Tester\Environment::setup(); // ініціалізація Nette Tester - $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/uk/components.texy b/application/uk/components.texy deleted file mode 100644 index cda44c9ffb..0000000000 --- a/application/uk/components.texy +++ /dev/null @@ -1,485 +0,0 @@ -Інтерактивні компоненти -*********************** - -
    - -Компоненти — це окремі об'єкти, що використовуються повторно, які ми вставляємо на сторінки. Це можуть бути форми, таблиці даних, опитування, власне все, що має сенс використовувати повторно. Ми покажемо: - -- як використовувати компоненти? -- як їх писати? -- що таке сигнали? - -
    - -Nette має вбудовану систему компонентів. Щось подібне можуть пам'ятати ті, хто працював з Delphi або ASP.NET Web Forms, на чомусь віддалено схожому побудовані React або Vue.js. Однак у світі PHP-фреймворків це унікальна річ. - -При цьому компоненти суттєво впливають на підхід до створення застосунків. Ви можете складати сторінки з готових блоків. Потрібна таблиця даних в адміністративній панелі? Знайдіть її на [Componette |https://componette.org/search/component], репозиторії доповнень з відкритим кодом (тобто не тільки компонентів) для Nette, і просто вставте в presenter. - -До presenter'а можна включити будь-яку кількість компонентів. А в деякі компоненти можна вставляти інші компоненти. Таким чином створюється дерево компонентів, коренем якого є presenter. - - -Фабричні методи -=============== - -Як компоненти вставляються в presenter і потім використовуються? Зазвичай за допомогою фабричних методів. - -Фабрика компонентів — це елегантний спосіб створювати компоненти лише тоді, коли вони дійсно потрібні (lazy / on demand). Вся магія полягає в реалізації методу з назвою `createComponent()`, де `` — це назва створюваного компонента, який створює та повертає компонент. - -```php .{file:DefaultPresenter.php} -class DefaultPresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentPoll(): PollControl - { - $poll = new PollControl; - $poll->items = $this->item; - return $poll; - } -} -``` - -Завдяки тому, що всі компоненти створюються в окремих методах, код стає більш зрозумілим. - -.[note] -Назви компонентів завжди починаються з малої літери, хоча в назві методу вони пишуться з великої. - -Фабрики ніколи не викликаються безпосередньо, вони викликаються самі в момент першого використання компонента. Завдяки цьому компонент створюється в потрібний момент і лише тоді, коли він дійсно потрібен. Якщо ми не використовуємо компонент (наприклад, при AJAX-запиті, коли передається лише частина сторінки, або при кешуванні шаблону), він взагалі не створюється, і ми економимо ресурси сервера. - -```php .{file:DefaultPresenter.php} -// звертаємося до компонента, і якщо це вперше, -// викликається createComponentPoll(), який його створює -$poll = $this->getComponent('poll'); -// альтернативний синтаксис: $poll = $this['poll']; -``` - -У шаблоні можна відобразити компонент за допомогою тегу [{control} |#Відображення]. Тому не потрібно вручну передавати компоненти в шаблон. - -```latte -

    Голосуйте

    - -{control poll} -``` - - -Голлівудський стиль -=================== - -Компоненти зазвичай використовують одну свіжу техніку, яку ми любимо називати Голлівудським стилем. Ви напевно знаєте крилату фразу, яку так часто чують учасники кінопроб: "Не дзвоніть нам, ми вам зателефонуємо". Саме про це йдеться. - -У Nette замість того, щоб постійно щось запитувати ("чи була надіслана форма?", "чи була вона валідною?" або "чи натиснув користувач цю кнопку?"), ви кажете фреймворку "коли це станеться, виклич цей метод" і залишаєте подальшу роботу йому. Якщо ви програмуєте на JavaScript, цей стиль програмування вам добре знайомий. Ви пишете функції, які викликаються, коли настає певна подія. І мова передає їм відповідні параметри. - -Це повністю змінює погляд на написання застосунків. Чим більше завдань ви можете залишити фреймворку, тим менше роботи у вас. І тим менше ви можете щось пропустити. - - -Пишемо компонент -================ - -Під поняттям компонент зазвичай мається на увазі нащадок класу [api:Nette\Application\UI\Control]. (Точніше було б використовувати термін "controls", але "контроли" мають в українській мові зовсім інше значення, і скоріше прижилися "компоненти".) Сам presenter [api:Nette\Application\UI\Presenter] є, до речі, також нащадком класу `Control`. - -```php .{file:PollControl.php} -use Nette\Application\UI\Control; - -class PollControl extends Control -{ -} -``` - - -Відображення -============ - -Ми вже знаємо, що для відображення компонента використовується тег `{control componentName}`. Він фактично викликає метод `render()` компонента, в якому ми дбаємо про відображення. У нас є, так само як і в presenter'і, [Latte шаблон|templates] у змінній `$this->template`, куди ми передаємо параметри. На відміну від presenter'а, ми повинні вказати файл із шаблоном і змусити його відобразитися: - -```php .{file:PollControl.php} -public function render(): void -{ - // вставляємо в шаблон деякі параметри - $this->template->param = $value; - // і відображаємо його - $this->template->render(__DIR__ . '/poll.latte'); -} -``` - -Тег `{control}` дозволяє передати параметри в метод `render()`: - -```latte -{control poll $id, $message} -``` - -```php .{file:PollControl.php} -public function render(int $id, string $message): void -{ - // ... -} -``` - -Іноді компонент може складатися з кількох частин, які ми хочемо відображати окремо. Для кожної з них ми створюємо власний метод відображення, тут у прикладі, наприклад, `renderPaginator()`: - -```php .{file:PollControl.php} -public function renderPaginator(): void -{ - // ... -} -``` - -А в шаблоні ми потім викликаємо його за допомогою: - -```latte -{control poll:paginator} -``` - -Для кращого розуміння добре знати, як цей тег перекладається в PHP. - -```latte -{control poll} -{control poll:paginator 123, 'hello'} -``` - -перекладається як: - -```php -$control->getComponent('poll')->render(); -$control->getComponent('poll')->renderPaginator(123, 'hello'); -``` - -Метод `getComponent()` повертає компонент `poll` і над цим компонентом викликає метод `render()`, відповідно `renderPaginator()`, якщо в тезі після двокрапки вказано інший спосіб рендерингу. - -.[caution] -Увага, якщо десь у параметрах з'явиться **`=>`**, усі параметри будуть упаковані в масив і передані як перший аргумент: - -```latte -{control poll, id: 123, message: 'hello'} -``` - -перекладається як: - -```php -$control->getComponent('poll')->render(['id' => 123, 'message' => 'hello']); -``` - -Відображення підкомпонента: - -```latte -{control cartControl-someForm} -``` - -перекладається як: - -```php -$control->getComponent("cartControl-someForm")->render(); -``` - -Компоненти, так само як і presenter'и, автоматично передають у шаблони кілька корисних змінних: - -- `$basePath` — абсолютний URL-шлях до кореневого каталогу (наприклад, `/eshop`) -- `$baseUrl` — абсолютний URL до кореневого каталогу (наприклад, `http://localhost/eshop`) -- `$user` — об'єкт [що представляє користувача |security:authentication] -- `$presenter` — поточний presenter -- `$control` — поточний компонент -- `$flashes` — масив [повідомлень |#Flash-повідомлення], надісланих функцією `flashMessage()` - - -Сигнал -====== - -Ми вже знаємо, що навігація в застосунку Nette полягає у посиланні або перенаправленні на пари `Presenter:action`. Але що, якщо ми просто хочемо виконати дію на **поточній сторінці**? Наприклад, змінити сортування стовпців у таблиці; видалити елемент; перемкнути світлий/темний режим; надіслати форму; проголосувати в опитуванні тощо. - -Цей тип запитів називається сигналами. І подібно до того, як дії викликають методи `action()` або `render()`, сигнали викликають методи `handle()`. У той час як поняття дії (або view) пов'язане виключно з presenter'ами, сигнали стосуються всіх компонентів. А отже, й presenter'ів, оскільки `UI\Presenter` є нащадком `UI\Control`. - -```php -public function handleClick(int $x, int $y): void -{ - // ... обробка сигналу ... -} -``` - -Посилання, що викликає сигнал, створюється звичайним способом, тобто в шаблоні атрибутом `n:href` або тегом `{link}`, у коді методом `link()`. Більше в розділі [Створення URL-посилань |creating-links#Посилання на сигнал]. - -```latte -натисніть тут -``` - -Сигнал завжди викликається на поточному presenter'і та action, його неможливо викликати на іншому presenter'і або іншому action. - -Сигнал, отже, спричиняє перезавантаження сторінки так само, як і при початковому запиті, лише додатково викликає метод обробки сигналу з відповідними параметрами. Якщо метод не існує, викидається виняток [api:Nette\Application\UI\BadSignalException], який користувачеві відображається як сторінка помилки 403 Forbidden. - - -Сніпети та AJAX -=============== - -Сигнали вам, можливо, трохи нагадують AJAX: обробники, які викликаються на поточній сторінці. І ви маєте рацію, сигнали дійсно часто викликаються за допомогою AJAX, і потім ми передаємо в браузер лише змінені частини сторінки. Тобто так звані сніпети. Більше інформації ви знайдете на [сторінці, присвяченій AJAX |ajax]. - - -Flash-повідомлення -================== - -Компонент має власне сховище flash-повідомлень, незалежне від presenter'а. Це повідомлення, які, наприклад, інформують про результат операції. Важливою особливістю flash-повідомлень є те, що вони доступні в шаблоні навіть після перенаправлення. Навіть після відображення вони залишаються активними ще 30 секунд – наприклад, на випадок, якщо через помилку передачі користувач оновить сторінку - повідомлення йому одразу не зникне. - -Надсилання забезпечує метод [flashMessage |api:Nette\Application\UI\Control::flashMessage()]. Першим параметром є текст повідомлення або об'єкт `stdClass`, що представляє повідомлення. Необов'язковим другим параметром є його тип (error, warning, info тощо). Метод `flashMessage()` повертає екземпляр flash-повідомлення як об'єкт `stdClass`, до якого можна додавати додаткову інформацію. - -```php -$this->flashMessage('Елемент було видалено.'); -$this->redirect(/* ... */); // і перенаправляємо -``` - -У шаблоні ці повідомлення доступні у змінній `$flashes` як об'єкти `stdClass`, які містять властивості `message` (текст повідомлення), `type` (тип повідомлення) і можуть містити вже згадану користувацьку інформацію. Відобразимо їх, наприклад, так: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Перенаправлення після сигналу -============================= - -Після обробки сигналу компонента часто відбувається перенаправлення. Це схожа ситуація, як з формами - після їх надсилання ми також перенаправляємо, щоб при оновленні сторінки в браузері не відбулося повторного надсилання даних. - -```php -$this->redirect('this') // перенаправляє на поточний presenter та action -``` - -Оскільки компонент є елементом, що використовується повторно, і зазвичай не повинен мати прямого зв'язку з конкретними presenter'ами, методи `redirect()` та `link()` автоматично інтерпретують параметр як сигнал компонента: - -```php -$this->redirect('click') // перенаправляє на сигнал 'click' того ж компонента -``` - -Якщо вам потрібно перенаправити на інший presenter чи дію, ви можете зробити це через presenter: - -```php -$this->getPresenter()->redirect('Product:show'); // перенаправляє на інший presenter/action -``` - - -Персистентні параметри -====================== - -Персистентні параметри служать для підтримки стану в компонентах між різними запитами. Їхнє значення залишається незмінним навіть після натискання на посилання. На відміну від даних у сесії, вони передаються в URL. І це відбувається повністю автоматично, включно з посиланнями, створеними в інших компонентах на тій самій сторінці. - -Наприклад, у вас є компонент для пагінації вмісту. Таких компонентів на сторінці може бути кілька. І ми хочемо, щоб після натискання на посилання всі компоненти залишалися на своїй поточній сторінці. Тому ми зробимо номер сторінки (`page`) персистентним параметром. - -Створення персистентного параметра в Nette надзвичайно просте. Достатньо створити публічну властивість і позначити її атрибутом: (раніше використовувалося `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // цей рядок важливий - -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; // має бути public -} -``` - -Для властивості рекомендуємо вказувати тип даних (наприклад, `int`) і ви можете вказати значення за замовчуванням. Значення параметрів можна [валідувати |#Валідація персистентних параметрів]. - -При створенні посилання можна змінити значення персистентного параметра: - -```latte -наступна -``` - -Або його можна *скинути*, тобто видалити з URL. Тоді він набуде свого значення за замовчуванням: - -```latte -скинути -``` - - -Персистентні компоненти -======================= - -Не тільки параметри, але й компоненти можуть бути персистентними. У такого компонента його персистентні параметри передаються і між різними діями presenter'а, або між кількома presenter'ами. Персистентні компоненти позначаємо анотацією біля класу presenter'а. Наприклад, так позначимо компоненти `calendar` та `poll`: - -```php -/** - * @persistent(calendar, poll) - */ -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Підкомпоненти всередині цих компонентів не потрібно позначати, вони також стануть персистентними. - -У PHP 8 ви можете для позначення персистентних компонентів використовувати також атрибути: - -```php -use Nette\Application\Attributes\Persistent; - -#[Persistent('calendar', 'poll')] -class DefaultPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Компоненти із залежностями -========================== - -Як створювати компоненти із залежностями, не "забруднюючи" presenter'ів, які їх використовуватимуть? Завдяки розумним властивостям DI-контейнера в Nette можна, так само як при використанні класичних сервісів, залишити більшу частину роботи фреймворку. - -Візьмемо як приклад компонент, який має залежність від сервісу `PollFacade`: - -```php -class PollControl extends Control -{ - public function __construct( - private int $id, // Id опитування, для якого ми створюємо компонент - private PollFacade $facade, - ) { - } - - public function handleVote(int $voteId): void - { - $this->facade->vote($this->id, $voteId); - // ... - } -} -``` - -Якби ми писали класичний сервіс, не було б чого вирішувати. Про передачу всіх залежностей невидимо подбав би DI-контейнер. Але з компонентами ми зазвичай поводимося так, що їхній новий екземпляр створюємо безпосередньо в presenter'і в [фабричних методах |#Фабричні методи] `createComponent…()`. Але передавати всі залежності всіх компонентів у presenter, щоб потім передати їх компонентам, незручно. І стільки написаного коду… - -Логічним питанням є, чому б просто не зареєструвати компонент як класичний сервіс, не передати його в presenter і потім у методі `createComponent…()` не повертати? Такий підхід, однак, недоречний, оскільки ми хочемо мати можливість створювати компонент навіть кілька разів. - -Правильним рішенням є написати для компонента фабрику, тобто клас, який нам створить компонент: - -```php -class PollControlFactory -{ - public function __construct( - private PollFacade $facade, - ) { - } - - public function create(int $id): PollControl - { - return new PollControl($id, $this->facade); - } -} -``` - -Таким чином, фабрику зареєструємо в нашому контейнері в конфігурації: - -```neon -services: - - PollControlFactory -``` - -і нарешті використаємо її в нашому presenter'і: - -```php -class PollPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private PollControlFactory $pollControlFactory, - ) { - } - - protected function createComponentPollControl(): PollControl - { - $pollId = 1; // можемо передати наш параметр - return $this->pollControlFactory->create($pollId); - } -} -``` - -Чудово те, що Nette DI такі прості фабрики вміє [генерувати |dependency-injection:factory], тому замість її повного коду достатньо написати лише її інтерфейс: - -```php -interface PollControlFactory -{ - public function create(int $id): PollControl; -} -``` - -І це все. Nette внутрішньо реалізує цей інтерфейс і передасть його в presenter, де ми вже можемо його використовувати. Магічно він додасть до нашого компонента і параметр `$id`, і екземпляр класу `PollFacade`. - - -Компоненти до глибини -===================== - -Компоненти в Nette Application представляють собою повторно використовувані частини веб-застосунку, які ми вставляємо на сторінки і яким, власне, присвячена вся ця глава. Які саме можливості має такий компонент? - -1) його можна відобразити в шаблоні -2) він знає, [яку свою частину |ajax#Сніпети] має відобразити при AJAX-запиті (сніпети) -3) він має можливість зберігати свій стан в URL (персистентні параметри) -4) він має можливість реагувати на дії користувача (сигнали) -5) він створює ієрархічну структуру (де коренем є presenter) - -Кожну з цих функцій забезпечує певний клас спадкової лінії. За відображення (1 + 2) відповідає [api:Nette\Application\UI\Control], за включення в [життєвий цикл |presenters#Життєвий цикл презентера] (3, 4) — клас [api:Nette\Application\UI\Component], а за створення ієрархічної структури (5) — класи [Container та Component |component-model:]. - -``` -Nette\ComponentModel\Component { IComponent } -| -+- Nette\ComponentModel\Container { IContainer } - | - +- Nette\Application\UI\Component { SignalReceiver, StatePersistent } - | - +- Nette\Application\UI\Control { Renderable } - | - +- Nette\Application\UI\Presenter { IPresenter } -``` - - -Життєвий цикл компонента ------------------------- - -[* lifecycle-component.svg *] *** *Життєвий цикл компонента* .<> - - -Валідація персистентних параметрів ----------------------------------- - -Значення [персистентних параметрів |#Персистентні параметри], отримані з URL, записує у властивості метод `loadState()`. Він також перевіряє, чи відповідає тип даних, вказаний у властивості, інакше відповідає помилкою 404 і сторінка не відображається. - -Ніколи сліпо не довіряйте персистентним параметрам, оскільки їх може легко перезаписати користувач в URL. Таким чином, наприклад, перевіримо, чи номер сторінки `$this->page` більший за 0. Підходящим способом є перезапис згаданого методу `loadState()`: - -```php -class PaginatingControl extends Control -{ - #[Persistent] - public int $page = 1; - - public function loadState(array $params): void - { - parent::loadState($params); // тут встановлюється $this->page - // далі йде власна перевірка значення: - if ($this->page < 1) { - $this->error(); - } - } -} -``` - -Зворотний процес, тобто збір значень з персистентних властивостей, відповідає метод `saveState()`. - - -Сигнали до глибини ------------------- - -Сигнал спричиняє перезавантаження сторінки так само, як і при початковому запиті (крім випадку, коли він викликаний AJAX) і викликає метод `signalReceived($signal)`, стандартна реалізація якого в класі `Nette\Application\UI\Component` намагається викликати метод, складений зі слів `handle{signal}`. Подальша обробка залежить від конкретного об'єкта. Об'єкти, що успадковують від `Component` (тобто `Control` і `Presenter`), реагують так, що намагаються викликати метод `handle{signal}` з відповідними параметрами. - -Іншими словами: береться визначення функції `handle{signal}` та всі параметри, що прийшли із запитом, і до аргументів за іменем підставляються параметри з URL, і намагається викликати даний метод. Наприклад, як параметр `$id` передається значення з параметра `id` в URL, як `$something` передається `something` з URL тощо. І якщо метод не існує, метод `signalReceived` викидає [виняток |api:Nette\Application\UI\BadSignalException]. - -Сигнал може приймати будь-який компонент, presenter або об'єкт, який реалізує інтерфейс `SignalReceiver` і підключений до дерева компонентів. - -Основними одержувачами сигналів будуть `Presenter`'и та візуальні компоненти, що успадковують від `Control`. Сигнал має служити знаком для об'єкта, що він має щось зробити – опитування має зарахувати голос від користувача, блок з новинами має розгорнутися і показати вдвічі більше новин, форма була надіслана і має обробити дані тощо. - -URL для сигналу створюємо за допомогою методу [Component::link() |api:Nette\Application\UI\Component::link()]. Як параметр `$destination` передаємо рядок `{signal}!` і як `$args` масив аргументів, які ми хочемо передати сигналу. Сигнал завжди викликається на поточному presenter'і та action з поточними параметрами, параметри сигналу лише додаються. Крім того, на самому початку додається **параметр `?do`, який визначає сигнал**. - -Його формат — або `{signal}`, або `{signalReceiver}-{signal}`. `{signalReceiver}` — це назва компонента в presenter'і. Тому в назві компонента не може бути дефіса — він використовується для розділення назви компонента і сигналу, однак таким чином можна вкладати кілька компонентів. - -Метод [isSignalReceiver()|api:Nette\Application\UI\Presenter::isSignalReceiver()] перевіряє, чи є компонент (перший аргумент) одержувачем сигналу (другий аргумент). Другий аргумент можна опустити — тоді він з'ясовує, чи є компонент одержувачем будь-якого сигналу. Як другий параметр можна вказати `true`, і цим перевірити, чи є одержувачем не тільки вказаний компонент, але й будь-який його нащадок. - -На будь-якому етапі, що передує `handle{signal}`, ми можемо виконати сигнал вручну, викликавши метод [processSignal()|api:Nette\Application\UI\Presenter::processSignal()], який бере на себе обробку сигналу — бере компонент, який визначено як одержувача сигналу (якщо одержувач сигналу не вказаний, це сам presenter) і надсилає йому сигнал. - -Приклад: - -```php -if ($this->isSignalReceiver($this, 'paging') || $this->isSignalReceiver($this, 'sorting')) { - $this->processSignal(); -} -``` - -Таким чином, сигнал виконано передчасно і більше не буде викликатися. diff --git a/application/uk/configuration.texy b/application/uk/configuration.texy deleted file mode 100644 index 6cdade511b..0000000000 --- a/application/uk/configuration.texy +++ /dev/null @@ -1,191 +0,0 @@ -Конфігурація застосунків -************************ - -.[perex] -Огляд конфігураційних опцій для застосунків Nette. - - -Application -=========== - -```neon -application: - # показувати панель "Nette Application" у Tracy BlueScreen? - debugger: ... # (bool) за замовчуванням true - - # чи буде при помилці викликатися error-presenter? - # має ефект лише в режимі розробки - catchExceptions: ... # (bool) за замовчуванням true - - # назва error-presenter - errorPresenter: Error # (string|array) за замовчуванням 'Nette:Error' - - # визначає аліаси для презентерів та дій - aliases: ... - - # визначає правила для перекладу назви presenter на клас - mapping: ... - - # неправильні посилання не генерують попередження? - # має ефект лише в режимі розробки - silentLinks: ... # (bool) за замовчуванням false -``` - -Починаючи з версії `nette/application` 3.2, можна визначити пару error-presenter'ів: - -```neon -application: - errorPresenter: - 4xx: Error4xx # для винятку Nette\Application\BadRequestException - 5xx: Error5xx # для інших винятків -``` - -Опція `silentLinks` визначає, як Nette поводитиметься в режимі розробки, коли генерація посилання зазнає невдачі (наприклад, тому що не існує presenter тощо). Стандартне значення `false` означає, що Nette викине помилку `E_USER_WARNING`. Встановлення на `true` призведе до придушення цього повідомлення про помилку. У робочому середовищі `E_USER_WARNING` викликається завжди. Цю поведінку можна також контролювати, встановивши змінну presenter [$invalidLinkMode |creating-links#Недійсні посилання]. - -[Аліаси спрощують посилання |creating-links#Аліаси] на часто використовувані презентери. - -[Мапінг визначає правила |directory-structure#Мапінг presenter ів], за якими з назви presenter виводиться назва класу. - - -Автоматична реєстрація презентерів ----------------------------------- - -Nette автоматично додає презентери як сервіси до DI-контейнера, що суттєво прискорює їхнє створення. Як Nette знаходить презентери, можна налаштувати: - -```neon -application: - # шукати презентери в Composer class map? - scanComposer: ... # (bool) за замовчуванням true - - # маска, якій має відповідати назва класу та файлу - scanFilter: ... # (string) за замовчуванням '*Presenter' - - # у яких каталогах шукати презентери? - scanDirs: # (string[]|false) за замовчуванням '%appDir%' - - %vendorDir%/mymodule -``` - -Каталоги, зазначені в `scanDirs`, не перезаписують стандартне значення `%appDir%`, а доповнюють його, отже `scanDirs` міститиме обидва шляхи `%appDir%` та `%vendorDir%/mymodule`. Якщо ми хочемо виключити стандартний каталог, використаємо [знак оклику |dependency-injection:configuration#Об єднання], який перезапише значення: - -```neon -application: - scanDirs!: - - %vendorDir%/mymodule -``` - -Сканування каталогів можна вимкнути, вказавши значення false. Не рекомендуємо повністю придушувати автоматичне додавання презентерів, оскільки інакше це призведе до зниження швидкодії застосунку. - - -Шаблони Latte -============= - -За допомогою цього налаштування можна глобально вплинути на поведінку Latte в компонентах та презентерах. - -```neon -latte: - # показувати панель Latte в Tracy Bar для головного шаблону (true) або всіх компонентів (all)? - debugger: ... # (true|false|'all') за замовчуванням true - - # генерує шаблони із заголовком declare(strict_types=1) - strictTypes: ... # (bool) за замовчуванням false - - # вмикає режим [суворого парсера |latte:develop#striktní režim] - strictParsing: ... # (bool) за замовчуванням false - - # активує [перевірку згенерованого коду |latte:develop#Kontrola vygenerovaného kódu] - phpLinter: ... # (string) за замовчуванням null - - # встановлює локаль - locale: cs_CZ # (string) за замовчуванням null - - # клас об'єкта $this->template - templateClass: App\MyTemplateClass # за замовчуванням Nette\Bridges\ApplicationLatte\DefaultTemplate -``` - -Якщо ви використовуєте Latte версії 3, ви можете додавати нові [розширення |latte:extending-latte#Latte Extension] за допомогою: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Якщо ви використовуєте Latte версії 2, ви можете реєструвати нові теги, вказавши ім'я класу або посилання на сервіс. За замовчуванням викликається метод `install()`, але це можна змінити, вказавши ім'я іншого методу: - -```neon -latte: - # реєстрація користувацьких тегів Latte - macros: - - App\MyLatteMacros::register # статичний метод, назва класу або callable - - @App\MyLatteMacrosFactory # сервіс з методом install() - - @App\MyLatteMacrosFactory::register # сервіс з методом register() - -services: - - App\MyLatteMacrosFactory -``` - - -Маршрутизація -============= - -Основні налаштування: - -```neon -routing: - # показувати панель маршрутизації в Tracy Bar? - debugger: ... # (bool) за замовчуванням true - - # серіалізує маршрутизатор до DI-контейнера - cache: ... # (bool) за замовчуванням false -``` - -Маршрутизацію зазвичай визначаємо в класі [RouterFactory |routing#Колекція маршрутів]. Альтернативно, маршрути можна визначити також у конфігурації за допомогою пар `маска: дія`, але цей спосіб не пропонує такої широкої варіативності в налаштуваннях: - -```neon -routing: - routes: - 'detail/': Admin:Home:default - '/': Front:Home:default -``` - - -Константи -========= - -Створення PHP-констант. - -```neon -constants: - Foobar: 'baz' -``` - -Після запуску застосунку буде створена константа `Foobar`. - -.[note] -Константи не повинні слугувати як якісь глобально доступні змінні. Для передачі значень в об'єкти використовуйте [впровадження залежностей |dependency-injection:passing-dependencies]. - - -PHP -=== - -Налаштування директив PHP. Огляд усіх директив ви знайдете на [php.net |https://www.php.net/manual/en/ini.list.php]. - -```neon -php: - date.timezone: Europe/Prague -``` - - -Сервіси DI -========== - -Ці сервіси додаються до DI-контейнера: - -| Назва | Тип | Опис -|---------------------------------------------------------- -| `application.application` | [api:Nette\Application\Application] | [запускач усього застосунку |how-it-works#Nette Application] -| `application.linkGenerator` | [api:Nette\Application\LinkGenerator] | [LinkGenerator |creating-links#LinkGenerator] -| `application.presenterFactory` | [api:Nette\Application\PresenterFactory] | фабрика презентерів -| `application.###` | [api:Nette\Application\UI\Presenter] | окремі презентери -| `latte.latteFactory` | [api:Nette\Bridges\ApplicationLatte\LatteFactory] | фабрика об'єкта `Latte\Engine` -| `latte.templateFactory` | [api:Nette\Application\UI\TemplateFactory] | фабрика для [`$this->template` |templates] diff --git a/application/uk/creating-links.texy b/application/uk/creating-links.texy deleted file mode 100644 index 361bc0c8ab..0000000000 --- a/application/uk/creating-links.texy +++ /dev/null @@ -1,286 +0,0 @@ -Створення URL-посилань -********************** - -
    - -Створювати посилання в Nette просто, як показувати пальцем. Достатньо лише вказати напрямок, і фреймворк зробить усю роботу за вас. Ми покажемо: - -- як створювати посилання в шаблонах та інших місцях -- як відрізнити посилання на поточну сторінку -- що робити з недійсними посиланнями - -
    - - -Завдяки [двосторонньому роутингу |routing] вам ніколи не доведеться вписувати URL-адреси вашого застосунку вручну в шаблони чи код, оскільки вони можуть згодом змінитися, або складно їх складати. У посиланні достатньо вказати presenter та дію, передати можливі параметри, і фреймворк сам згенерує URL. Власне, це дуже схоже на виклик функції. Вам це сподобається. - - -У шаблоні presenter'а -===================== - -Найчастіше ми створюємо посилання в шаблонах, і чудовим помічником є атрибут `n:href`: - -```latte -деталі -``` - -Зверніть увагу, що замість HTML-атрибута `href` ми використали [n:атрибут |latte:syntax#n:атрибути] `n:href`. Його значенням є не URL, як це було б у випадку атрибута `href`, а назва presenter'а та дії. - -Натискання на посилання, спрощено кажучи, схоже на виклик методу `ProductPresenter::renderShow()`. І якщо він має параметри у своїй сигнатурі, ми можемо викликати його з аргументами: - -```latte -деталі продукту -``` - -Можна передавати й іменовані параметри. Наступне посилання передає параметр `lang` зі значенням `cs`: - -```latte -деталі продукту -``` - -Якщо метод `ProductPresenter::renderShow()` не має `$lang` у своїй сигнатурі, він може отримати значення параметра за допомогою `$lang = $this->getParameter('lang')` або з [властивості |presenters#Параметри запиту]. - -Якщо параметри зберігаються в масиві, їх можна розгорнути оператором `...` (в Latte 2.x оператором `(expand)`): - -```latte -{var $args = [$product->id, lang => cs]} -деталі продукту -``` - -У посиланнях також автоматично передаються так звані [персистентні параметри |presenters#Персистентні параметри]. - -Атрибут `n:href` дуже зручний для HTML-тегів ``. Якщо ми хочемо вивести посилання в іншому місці, наприклад, у тексті, використовуємо `{link}`: - -```latte -Адреса: {link Home:default} -``` - - -У коді -====== - -Для створення посилання в presenter'і служить метод `link()`: - -```php -$url = $this->link('Product:show', $product->id); -``` - -Параметри можна передати також за допомогою масиву, де можна вказати й іменовані параметри: - -```php -$url = $this->link('Product:show', [$product->id, 'lang' => 'cs']); -``` - -Посилання можна створювати і без presenter'а, для цього існує [#LinkGenerator] та його метод `link()`. - - -Посилання на presenter -====================== - -Якщо ціллю посилання є presenter та дія, воно має такий синтаксис: - -``` -[//] [[[[:]module:]presenter:]action | this] [#fragment] -``` - -Формат підтримують усі теги Latte та всі методи presenter'а, які працюють з посиланнями, тобто `n:href`, `{link}`, `{plink}`, `link()`, `lazyLink()`, `isLinkCurrent()`, `redirect()`, `redirectPermanent()`, `forward()`, `canonicalize()`, а також [#LinkGenerator]. Тому, хоча в прикладах використано `n:href`, там могла б бути будь-яка з функцій. - -Основною формою є `Presenter:action`: - -```latte -головна сторінка -``` - -Якщо ми посилаємося на дію поточного presenter'а, ми можемо опустити його назву: - -```latte -головна сторінка -``` - -Якщо ціллю є дія `default`, ми можемо її опустити, але двокрапка має залишитися: - -```latte -головна сторінка -``` - -Посилання також можуть вказувати на інші [модулі |directory-structure#Presenter и та шаблони]. Тут посилання розрізняються на відносні до вкладеного підмодуля або абсолютні. Принцип аналогічний до шляхів на диску, тільки замість слешів використовуються двокрапки. Припустимо, що поточний presenter є частиною модуля `Front`, тоді запишемо: - -```latte -посилання на Front:Shop:Product:show -посилання на Admin:Product:show -``` - -Особливим випадком є посилання [на себе |#Посилання на поточну сторінку], коли як ціль вказуємо `this`. - -```latte -оновити -``` - -Ми можемо посилатися на певну частину сторінки через так званий фрагмент за знаком решітки `#`: - -```latte -посилання на Home:default та фрагмент #main -``` - - -Абсолютні шляхи -=============== - -Посилання, згенеровані за допомогою `link()` або `n:href`, завжди є абсолютними шляхами (тобто починаються зі знака `/`), але не абсолютними URL з протоколом та доменом, як `https://domain`. - -Для генерації абсолютного URL додайте на початок два слеші (наприклад, `n:href="//Home:"`). Або можна перемкнути presenter, щоб він генерував лише абсолютні посилання, встановивши `$this->absoluteUrls = true`. - - -Посилання на поточну сторінку -============================= - -Ціль `this` створить посилання на поточну сторінку: - -```latte -оновити -``` - -Водночас передаються всі параметри, зазначені в сигнатурі методу `action()` або `render()`, якщо `action()` не визначено. Отже, якщо ми на сторінці `Product:show` і `id: 123`, посилання на `this` передасть і цей параметр. - -Звичайно, можна вказати параметри безпосередньо: - -```latte -оновити -``` - -Функція `isLinkCurrent()` перевіряє, чи ціль посилання збігається з поточною сторінкою. Це можна використати, наприклад, у шаблоні для розрізнення посилань тощо. - -Параметри такі ж, як у методі `link()`, але додатково можна замість конкретної дії вказати заступний знак `*`, який означає будь-яку дію даного presenter'а. - -```latte -{if !isLinkCurrent('Admin:login')} - Увійдіть -{/if} - -
  • - ... -
  • -``` - -У комбінації з `n:href` в одному елементі можна використовувати скорочену форму: - -```latte -... -``` - -Заступний знак `*` можна використовувати лише замість дії, а не presenter'а. - -Для перевірки, чи ми знаходимося в певному модулі або його підмодулі, використовуємо метод `isModuleCurrent(moduleName)`. - -```latte -
  • - ... -
  • -``` - - -Посилання на сигнал -=================== - -Ціллю посилання може бути не тільки presenter та дія, але й [сигнал |components#Сигнал] (викликають метод `handle()`). Тоді синтаксис такий: - -``` -[//] [sub-component:]signal! [#fragment] -``` - -Сигнал, отже, відрізняється знаком оклику: - -```latte -сигнал -``` - -Можна створити й посилання на сигнал підкомпонента (або під-підкомпонента): - -```latte -сигнал -``` - - -Посилання в компоненті -====================== - -Оскільки [компоненти|components] є окремими повторно використовуваними одиницями, які не повинні мати жодних зв'язків з навколишніми presenter'ами, посилання тут працюють трохи інакше. Атрибут Latte `n:href` та тег `{link}`, а також методи компонента, такі як `link()` та інші, розглядають ціль посилання **завжди як назву сигналу**. Тому навіть не потрібно вказувати знак оклику: - -```latte -сигнал, а не дія -``` - -Якщо ми хочемо в шаблоні компонента посилатися на presenter'ів, використовуємо для цього тег `{plink}`: - -```latte -вступ -``` - -або в коді - -```php -$this->getPresenter()->link('Home:default') -``` - - -Аліаси .{data-version:v3.2.2} -============================= - -Іноді може бути корисно призначити парі Presenter:дія легко запам'ятовуваний псевдонім. Наприклад, головну сторінку `Front:Home:default` назвати просто `home` або `Admin:Dashboard:default` як `admin`. - -Аліаси визначаються в [конфігурації|configuration] під ключем `application › aliases`: - -```neon -application: - aliases: - home: Front:Home:default - admin: Admin:Dashboard:default - sign: Front:Sign:in -``` - -У посиланнях вони потім записуються за допомогою символу @, наприклад: - -```latte -адміністрація -``` - -Вони також підтримуються у всіх методах, що працюють з посиланнями, таких як `redirect()` тощо. - - -Недійсні посилання -================== - -Може статися, що ми створимо недійсне посилання - або тому, що воно веде на неіснуючий presenter, або тому, що передає більше параметрів, ніж цільовий метод приймає у своїй сигнатурі, або коли для цільової дії неможливо згенерувати URL. Як поводитися з недійсними посиланнями, визначає статична змінна `Presenter::$invalidLinkMode`. Вона може набувати комбінації таких значень (констант): - -- `Presenter::InvalidLinkSilent` - тихий режим, як URL повертається знак # -- `Presenter::InvalidLinkWarning` - викидається попередження E_USER_WARNING, яке в робочому режимі буде залоговано, але не спричинить переривання виконання скрипта -- `Presenter::InvalidLinkTextual` - візуальне попередження, виводить помилку безпосередньо в посиланні -- `Presenter::InvalidLinkException` - викидається виняток InvalidLinkException - -Стандартне налаштування — `InvalidLinkWarning` у робочому режимі та `InvalidLinkWarning | InvalidLinkTextual` у режимі розробки. `InvalidLinkWarning` у робочому середовищі не спричиняє переривання скрипта, але попередження буде залоговано. У середовищі розробки його перехопить [Tracy |tracy:] і відобразить блюскрін. `InvalidLinkTextual` працює так, що як URL повертає повідомлення про помилку, яке починається символами `#error:`. Щоб такі посилання були помітні з першого погляду, доповнимо CSS: - -```css -a[href^="#error:"] { - background: red; - color: white; -} -``` - -Якщо ми не хочемо, щоб у середовищі розробки генерувалися попередження, можемо встановити тихий режим безпосередньо в [конфігурації|configuration]. - -```neon -application: - silentLinks: true -``` - - -LinkGenerator -============= - -Як створювати посилання з такою ж зручністю, як метод `link()`, але без наявності presenter'а? Для цього існує [api:Nette\Application\LinkGenerator]. - -LinkGenerator — це сервіс, який ви можете отримати через конструктор, а потім створювати посилання його методом `link()`. - -Порівняно з presenter'ами є відмінність. LinkGenerator створює всі посилання одразу як абсолютні URL. Також не існує "поточного presenter'а", тому не можна як ціль вказати лише назву дії `link('default')` або вказувати відносні шляхи до модулів. - -Недійсні посилання завжди викидають `Nette\Application\UI\InvalidLinkException`. diff --git a/application/uk/directory-structure.texy b/application/uk/directory-structure.texy deleted file mode 100644 index 2643f4f031..0000000000 --- a/application/uk/directory-structure.texy +++ /dev/null @@ -1,526 +0,0 @@ -Структура каталогів застосунку -****************************** - -
    - -Як спроектувати зрозумілу та масштабовану структуру каталогів для проектів на Nette Framework? Ми покажемо перевірені практики, які допоможуть вам організувати код. Ви дізнаєтеся: - -- як **логічно розділити** застосунок на каталоги -- як спроектувати структуру так, щоб вона **добре масштабувалася** зі зростанням проекту -- які є **можливі альтернативи** та їхні переваги чи недоліки - -
    - - -Важливо зазначити, що сам Nette Framework не наполягає на жодній конкретній структурі. Він розроблений так, щоб його можна було легко адаптувати до будь-яких потреб та уподобань. - - -Базова структура проекту -======================== - -Хоча Nette Framework не диктує жодної жорсткої структури каталогів, існує перевірене стандартне розташування у вигляді [Web Project|https://github.com/nette/web-project]: - -/--pre -web-project/ -├── app/ ← каталог із застосунком -├── assets/ ← файли SCSS, JS, зображення..., альтернативно resources/ -├── bin/ ← скрипти для командного рядка -├── config/ ← конфігурація -├── log/ ← залоговані помилки -├── temp/ ← тимчасові файли, кеш -├── tests/ ← тести -├── vendor/ ← бібліотеки, встановлені Composer -└── www/ ← публічний каталог (document-root) -\-- - -Цю структуру ви можете вільно змінювати відповідно до своїх потреб - папки перейменовувати чи переміщувати. Потім достатньо лише змінити відносні шляхи до каталогів у файлі `Bootstrap.php` та, можливо, `composer.json`. Більше нічого не потрібно, жодної складної реконфігурації, жодних змін констант. Nette має розумне автовизначення і автоматично розпізнає розташування застосунку, включно з його базовим URL. - - -Принципи організації коду -========================= - -Коли ви вперше досліджуєте новий проект, ви повинні швидко в ньому зорієнтуватися. Уявіть, що ви розкриваєте каталог `app/Model/` і бачите таку структуру: - -/--pre -app/Model/ -├── Services/ -├── Repositories/ -└── Entities/ -\-- - -З неї ви дізнаєтеся лише те, що проект використовує якісь сервіси, репозиторії та сутності. Про справжнє призначення застосунку ви не дізнаєтеся абсолютно нічого. - -Розглянемо інший підхід - **організацію за доменами**: - -/--pre -app/Model/ -├── Cart/ -├── Payment/ -├── Order/ -└── Product/ -\-- - -Тут все інакше - з першого погляду зрозуміло, що це інтернет-магазин. Вже самі назви каталогів розкривають, що вміє застосунок - працює з платежами, замовленнями та продуктами. - -Перший підхід (організація за типом класів) на практиці спричиняє низку проблем: код, який логічно пов'язаний, розкиданий по різних папках, і вам доводиться між ними перескакувати. Тому ми будемо організовувати за доменами. - - -Простори імен -------------- - -Зазвичай структура каталогів відповідає просторам імен у застосунку. Це означає, що фізичне розташування файлів відповідає їхньому namespace. Наприклад, клас, розташований у `app/Model/Product/ProductRepository.php`, повинен мати namespace `App\Model\Product`. Цей принцип допомагає орієнтуватися в коді та спрощує автозавантаження. - - -Однина проти множини в назвах ------------------------------ - -Зверніть увагу, що для основних каталогів застосунку ми використовуємо однину: `app`, `config`, `log`, `temp`, `www`. Так само і всередині застосунку: `Model`, `Core`, `Presentation`. Це тому, що кожен з них представляє одну цілісну концепцію. - -Подібно, наприклад, `app/Model/Product` представляє все, що стосується продуктів. Ми не назвемо це `Products`, оскільки це не папка, повна продуктів (там були б файли `nokia.php`, `samsung.php`). Це namespace, що містить класи для роботи з продуктами - `ProductRepository.php`, `ProductService.php`. - -Папка `app/Tasks` у множині, оскільки містить набір окремих виконуваних скриптів - `CleanupTask.php`, `ImportTask.php`. Кожен з них є окремою одиницею. - -Для послідовності рекомендуємо використовувати: -- Однину для namespace, що представляє функціональну одиницю (хоча й працює з кількома сутностями) -- Множину для колекцій окремих одиниць -- У разі невизначеності або якщо ви не хочете над цим замислюватися, вибирайте однину - - -Публічний каталог `www/` -======================== - -Цей каталог є єдиним доступним з вебу (так званий document-root). Часто можна зустріти назву `public/` замість `www/` - це лише питання конвенції і на функціональність це не впливає. Каталог містить: -- [Точка входу |bootstrapping#index.php] застосунку `index.php` -- Файл `.htaccess` з правилами для mod_rewrite (для Apache) -- Статичні файли (CSS, JavaScript, зображення) -- Завантажені файли - -Для належного захисту застосунку важливо мати правильно [налаштований document-root |nette:troubleshooting#Як змінити або видалити каталог www з URL]. - -.[note] -Ніколи не розміщуйте в цьому каталозі папку `node_modules/` - вона містить тисячі файлів, які можуть бути виконуваними і не повинні бути публічно доступними. - - -Каталог застосунку `app/` -========================= - -Це головний каталог з кодом застосунку. Базова структура: - -/--pre -app/ -├── Core/ ← інфраструктурні питання -├── Model/ ← бізнес-логіка -├── Presentation/ ← presenter'и та шаблони -├── Tasks/ ← скрипти командного рядка -└── Bootstrap.php ← завантажувальний клас застосунку -\-- - -`Bootstrap.php` — це [стартовий клас застосунку|bootstrapping], який ініціалізує середовище, завантажує конфігурацію та створює DI-контейнер. - -Тепер розглянемо окремі підкаталоги детальніше. - - -Presenter'и та шаблони -====================== - -Презентаційна частина застосунку знаходиться в каталозі `app/Presentation`. Альтернативою є коротке `app/UI`. Це місце для всіх presenter'ів, їхніх шаблонів та можливих допоміжних класів. - -Цей шар ми організовуємо за доменами. У складному проекті, який поєднує інтернет-магазин, блог та API, структура виглядала б так: - -/--pre -app/Presentation/ -├── Shop/ ← фронтенд інтернет-магазину -│ ├── Product/ -│ ├── Cart/ -│ └── Order/ -├── Blog/ ← блог -│ ├── Home/ -│ └── Post/ -├── Admin/ ← адміністрація -│ ├── Dashboard/ -│ └── Products/ -└── Api/ ← кінцеві точки API - └── V1/ -\-- - -Навпаки, для простого блогу ми б використали такий поділ: - -/--pre -app/Presentation/ -├── Front/ ← фронтенд сайту -│ ├── Home/ -│ └── Post/ -├── Admin/ ← адміністрація -│ ├── Dashboard/ -│ └── Posts/ -├── Error/ -└── Export/ ← RSS, sitemaps тощо. -\-- - -Папки, такі як `Home/` або `Dashboard/`, містять presenter'и та шаблони. Папки, такі як `Front/`, `Admin/` або `Api/`, називаємо **модулями**. Технічно це звичайні каталоги, які служать для логічного поділу застосунку. - -Кожна папка з presenter'ом містить однойменний presenter та його шаблони. Наприклад, папка `Dashboard/` містить: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -└── default.latte ← шаблон -\-- - -Ця структура каталогів відображається в просторах імен класів. Наприклад, `DashboardPresenter` знаходиться в просторі імен `App\Presentation\Admin\Dashboard` (див. [#Мапінг presenter ів]): - -```php -namespace App\Presentation\Admin\Dashboard; - -class DashboardPresenter extends Nette\Application\UI\Presenter -{ - // ... -} -``` - -На presenter `Dashboard` всередині модуля `Admin` ми посилаємося в застосунку за допомогою двокрапкової нотації як на `Admin:Dashboard`. На його дію `default` — як на `Admin:Dashboard:default`. У випадку вкладених модулів використовуємо більше двокрапок, наприклад `Shop:Order:Detail:default`. - - -Гнучкий розвиток структури --------------------------- - -Однією з великих переваг цієї структури є те, як елегантно вона адаптується до зростаючих потреб проекту. Як приклад візьмемо частину, що генерує XML-фіди. На початку маємо просту форму: - -/--pre -Export/ -├── ExportPresenter.php ← один presenter для всіх експортів -├── sitemap.latte ← шаблон для sitemap -└── feed.latte ← шаблон для RSS-фіду -\-- - -З часом з'являться інші типи фідів, і нам знадобиться для них більше логіки... Жодних проблем! Папка `Export/` просто стає модулем: - -/--pre -Export/ -├── Sitemap/ -│ ├── SitemapPresenter.php -│ └── sitemap.latte -└── Feed/ - ├── FeedPresenter.php - ├── zbozi.latte ← фід для Zboží.cz - └── heureka.latte ← фід для Heureka.cz -\-- - -Ця трансформація абсолютно плавна - достатньо створити нові підпапки, розділити в них код і оновити посилання (наприклад, з `Export:feed` на `Export:Feed:zbozi`). Завдяки цьому ми можемо структуру поступово розширювати за потребою, рівень вкладеності ніяк не обмежений. - -Якщо, наприклад, в адміністрації у вас багато presenter'ів, що стосуються управління замовленнями, таких як `OrderDetail`, `OrderEdit`, `OrderDispatch` тощо, ви можете для кращої організації в цьому місці створити модуль (папку) `Order`, в якому будуть (папки для) presenter'ів `Detail`, `Edit`, `Dispatch` та інші. - - -Розташування шаблонів ---------------------- - -У попередніх прикладах ми бачили, що шаблони розташовані безпосередньо в папці з presenter'ом: - -/--pre -Dashboard/ -├── DashboardPresenter.php ← presenter -├── DashboardTemplate.php ← необов'язковий клас для шаблону -└── default.latte ← шаблон -\-- - -Це розташування на практиці виявляється найзручнішим - усі пов'язані файли у вас одразу під рукою. - -Альтернативно, ви можете розмістити шаблони в підпапці `templates/`. Nette підтримує обидва варіанти. Ви навіть можете розмістити шаблони повністю поза папкою `Presentation/`. Все про можливості розташування шаблонів ви знайдете в розділі [Пошук шаблонів |templates#Пошук шаблонів]. - - -Допоміжні класи та компоненти ------------------------------ - -До presenter'ів та шаблонів часто належать й інші допоміжні файли. Розмістимо їх логічно відповідно до їхньої сфери дії: - -1. **Безпосередньо біля presenter'а** у випадку специфічних компонентів для даного presenter'а: - -/--pre -Product/ -├── ProductPresenter.php -├── ProductGrid.php ← компонент для виведення продуктів -└── FilterForm.php ← форма для фільтрації -\-- - -2. **Для модуля** - рекомендуємо використовувати папку `Accessory`, яка розміщується зручно на початку алфавіту: - -/--pre -Front/ -├── Accessory/ -│ ├── NavbarControl.php ← компоненти для фронтенду -│ └── TemplateFilters.php -├── Product/ -└── Cart/ -\-- - -3. **Для всього застосунку** - в `Presentation/Accessory/`: -/--pre -app/Presentation/ -├── Accessory/ -│ ├── LatteExtension.php -│ └── TemplateFilters.php -├── Front/ -└── Admin/ -\-- - -Або ви можете розмістити допоміжні класи, такі як `LatteExtension.php` або `TemplateFilters.php`, в інфраструктурній папці `app/Core/Latte/`. А компоненти — в `app/Components`. Вибір залежить від звичок команди. - - -Модель - серце застосунку -========================= - -Модель містить усю бізнес-логіку застосунку. Для її організації знову діє правило - структуруємо за доменами: - -/--pre -app/Model/ -├── Payment/ ← все, що стосується платежів -│ ├── PaymentFacade.php ← головна точка входу -│ ├── PaymentRepository.php -│ ├── Payment.php ← сутність -├── Order/ ← все, що стосується замовлень -│ ├── OrderFacade.php -│ ├── OrderRepository.php -│ ├── Order.php -└── Shipping/ ← все, що стосується доставки -\-- - -У моделі зазвичай зустрічаються такі типи класів: - -**Фасади**: представляють головну точку входу до конкретної домени в застосунку. Діють як оркестратор, який координує співпрацю між різними сервісами з метою реалізації повних use-cases (як "створити замовлення" або "обробити платіж"). Під своїм оркестраційним шаром фасад приховує деталі реалізації від решти застосунку, чим надає чистий інтерфейс для роботи з даною доменою. - -```php -class OrderFacade -{ - public function createOrder(Cart $cart): Order - { - // валідація - // створення замовлення - // надсилання електронного листа - // запис у статистику - } -} -``` - -**Сервіси**: зосереджуються на специфічній бізнес-операції в межах домени. На відміну від фасаду, який оркеструє цілі use-cases, сервіс реалізує конкретну бізнес-логіку (як розрахунки цін або обробка платежів). Сервіси зазвичай без стану і можуть бути використані або фасадами як будівельні блоки для складніших операцій, або безпосередньо іншими частинами застосунку для простіших завдань. - -```php -class PricingService -{ - public function calculateTotal(Order $order): Money - { - // розрахунок ціни - } -} -``` - -**Репозиторії**: забезпечують усю комунікацію з сховищем даних, зазвичай базою даних. Його завданням є завантаження та збереження сутностей та реалізація методів для їх пошуку. Репозиторій відокремлює решту застосунку від деталей реалізації бази даних і надає об'єктно-орієнтований інтерфейс для роботи з даними. - -```php -class OrderRepository -{ - public function find(int $id): ?Order - { - } - - public function findByCustomer(int $customerId): array - { - } -} -``` - -**Сутності**: об'єкти, що представляють основні бізнес-концепції в застосунку, які мають свою ідентичність і змінюються з часом. Зазвичай це класи, що мапуються на таблиці бази даних за допомогою ORM (як Nette Database Explorer або Doctrine). Сутності можуть містити бізнес-правила, що стосуються їхніх даних, та логіку валідації. - -```php -// Сутність, мапована на таблицю бази даних 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, - ]); - } -} -``` - -**Об'єкти значень**: незмінні об'єкти, що представляють значення без власної ідентичності - наприклад, грошова сума або адреса електронної пошти. Два екземпляри об'єкта значення з однаковими значеннями вважаються ідентичними. - - -Інфраструктурний код -==================== - -Папка `Core/` (або також `Infrastructure/`) є домом для технічної основи застосунку. Інфраструктурний код зазвичай включає: - -/--pre -app/Core/ -├── Router/ ← маршрутизація та управління URL -│ └── RouterFactory.php -├── Security/ ← автентифікація та авторизація -│ ├── Authenticator.php -│ └── Authorizator.php -├── Logging/ ← логування та моніторинг -│ ├── SentryLogger.php -│ └── FileLogger.php -├── Cache/ ← шар кешування -│ └── FullPageCache.php -└── Integration/ ← інтеграція з зовнішніми сервісами - ├── Slack/ - └── Stripe/ -\-- - -Для менших проектів, звісно, достатньо плоского поділу: - -/--pre -Core/ -├── RouterFactory.php -├── Authenticator.php -└── QueueMailer.php -\-- - -Це код, який: - -- Вирішує технічну інфраструктуру (маршрутизація, логування, кешування) -- Інтегрує зовнішні сервіси (Sentry, Elasticsearch, Redis) -- Надає базові сервіси для всього застосунку (пошта, база даних) -- Здебільшого незалежний від конкретної домени - кеш або логер працює однаково для інтернет-магазину чи блогу. - -Вагаєтеся, чи певний клас належить сюди, чи до моделі? Ключова відмінність полягає в тому, що код у `Core/`: - -- Нічого не знає про домену (продукти, замовлення, статті) -- Здебільшого можна перенести в інший проект -- Вирішує "як це працює" (як надіслати лист), а не "що це робить" (який лист надіслати) - -Приклад для кращого розуміння: - -- `App\Core\MailerFactory` - створює екземпляри класу для надсилання електронних листів, вирішує налаштування SMTP -- `App\Model\OrderMailer` - використовує `MailerFactory` для надсилання електронних листів про замовлення, знає їхні шаблони та коли їх потрібно надіслати - - -Скрипти командного рядка -======================== - -Застосунки часто потребують виконання дій поза звичайними HTTP-запитами - чи то обробка даних у фоновому режимі, обслуговування, чи періодичні завдання. Для запуску служать прості скрипти в каталозі `bin/`, саму логіку реалізації ми розміщуємо в `app/Tasks/` (або `app/Commands/`). - -Приклад: - -/--pre -app/Tasks/ -├── Maintenance/ ← скрипти обслуговування -│ ├── CleanupCommand.php ← видалення старих даних -│ └── DbOptimizeCommand.php ← оптимізація бази даних -├── Integration/ ← інтеграція з зовнішніми системами -│ ├── ImportProducts.php ← імпорт із системи постачальника -│ └── SyncOrders.php ← синхронізація замовлень -└── Scheduled/ ← регулярні завдання - ├── NewsletterCommand.php ← розсилка новин - └── ReminderCommand.php ← сповіщення клієнтам -\-- - -Що належить до моделі, а що до скриптів командного рядка? Наприклад, логіка для надсилання одного електронного листа є частиною моделі, масова розсилка тисяч електронних листів вже належить до `Tasks/`. - -Завдання зазвичай [запускаємо з командного рядка |https://blog.nette.org/en/cli-scripts-in-nette-application] або через cron. Їх можна запускати і через HTTP-запит, але потрібно пам'ятати про безпеку. Presenter, який запускає завдання, потрібно захистити, наприклад, лише для зареєстрованих користувачів або сильним токеном та доступом з дозволених IP-адрес. Для тривалих завдань потрібно збільшити часовий ліміт скрипта та використовувати `session_write_close()`, щоб не блокувалася сесія. - - -Інші можливі каталоги -===================== - -Крім згаданих базових каталогів, ви можете за потребою проекту додати інші спеціалізовані папки. Розглянемо найпоширеніші з них та їхнє використання: - -/--pre -app/ -├── Api/ ← логіка для API, незалежна від презентаційного шару -├── Database/ ← міграційні скрипти та сідери для тестових даних -├── Components/ ← спільні візуальні компоненти для всього застосунку -├── Event/ ← корисно, якщо використовуєте подієво-орієнтовану архітектуру -├── Mail/ ← шаблони електронних листів та пов'язана логіка -└── Utils/ ← допоміжні класи -\-- - -Для спільних візуальних компонентів, що використовуються в presenter'ах по всьому застосунку, можна використовувати папку `app/Components` або `app/Controls`: - -/--pre -app/Components/ -├── Form/ ← спільні компоненти форм -│ ├── SignInForm.php -│ └── UserForm.php -├── Grid/ ← компоненти для виведення даних -│ └── DataGrid.php -└── Navigation/ ← елементи навігації - ├── Breadcrumbs.php - └── Menu.php -\-- - -Сюди належать компоненти, які мають складнішу логіку. Якщо ви хочете ділитися компонентами між кількома проектами, доцільно виділити їх в окремий composer пакет. - -До каталогу `app/Mail` ви можете розмістити управління електронною поштою: - -/--pre -app/Mail/ -├── templates/ ← шаблони електронних листів -│ ├── order-confirmation.latte -│ └── welcome.latte -└── OrderMailer.php -\-- - - -Мапінг presenter'ів -=================== - -Мапінг визначає правила для виведення назви класу з назви presenter'а. Ми вказуємо їх у [конфігурації|configuration] під ключем `application › mapping`. - -На цій сторінці ми показали, що presenter'и розміщуємо в папці `app/Presentation` (або `app/UI`). Цю конвенцію ми повинні повідомити Nette в конфігураційному файлі. Достатньо одного рядка: - -```neon -application: - mapping: App\Presentation\*\**Presenter -``` - -Як працює мапінг? Для кращого розуміння спочатку уявимо застосунок без модулів. Ми хочемо, щоб класи presenter'ів належали до простору імен `App\Presentation`, щоб presenter `Home` мапувався на клас `App\Presentation\HomePresenter`. Цього досягнемо такою конфігурацією: - -```neon -application: - mapping: App\Presentation\*Presenter -``` - -Мапінг працює так, що назва presenter'а `Home` замінює зірочку в масці `App\Presentation\*Presenter`, чим отримуємо кінцеву назву класу `App\Presentation\HomePresenter`. Просто! - -Але, як ви бачите в прикладах у цьому та інших розділах, класи presenter'ів ми розміщуємо в однойменних підкаталогах, наприклад, presenter `Home` мапується на клас `App\Presentation\Home\HomePresenter`. Цього досягнемо подвоєнням двокрапки (вимагає Nette Application 3.2): - -```neon -application: - mapping: App\Presentation\**Presenter -``` - -Тепер перейдемо до мапінгу presenter'ів у модулі. Для кожного модуля ми можемо визначити специфічний мапінг: - -```neon -application: - mapping: - Front: App\Presentation\Front\**Presenter - Admin: App\Presentation\Admin\**Presenter - Api: App\Api\*Presenter -``` - -Згідно з цією конфігурацією, presenter `Front:Home` мапується на клас `App\Presentation\Front\Home\HomePresenter`, тоді як presenter `Api:OAuth` на клас `App\Api\OAuthPresenter`. - -Оскільки модулі `Front` та `Admin` мають схожий спосіб мапінгу, і таких модулів, ймовірно, буде більше, можна створити загальне правило, яке їх замінить. До маски класу так додасться нова зірочка для модуля: - -```neon -application: - mapping: - *: App\Presentation\*\**Presenter - Api: App\Api\*Presenter -``` - -Це працює і для глибше вкладених структур каталогів, як, наприклад, presenter `Admin:User:Edit`, сегмент із зірочкою повторюється для кожного рівня, і результатом є клас `App\Presentation\Admin\User\Edit\EditPresenter`. - -Альтернативним записом є використання замість рядка масиву, що складається з трьох сегментів. Цей запис еквівалентний попередньому: - -```neon -application: - mapping: - *: [App\Presentation, *, **Presenter] - Api: [App\Api, '', *Presenter] -``` diff --git a/application/uk/how-it-works.texy b/application/uk/how-it-works.texy deleted file mode 100644 index ab074c7c17..0000000000 --- a/application/uk/how-it-works.texy +++ /dev/null @@ -1,200 +0,0 @@ -Як працюють застосунки? -*********************** - -
    - -Ви читаєте основний документ документації Nette. Ви дізнаєтеся весь принцип роботи веб-застосунків. Гарно від А до Я, від моменту народження до останнього подиху PHP-скрипта. Після прочитання ви будете знати: - -- як це все працює -- що таке Bootstrap, Presenter та DI-контейнер -- як виглядає структура каталогів - -
    - - -Структура каталогів -=================== - -Відкрийте приклад скелета веб-застосунку під назвою [WebProject|https://github.com/nette/web-project] і під час читання можете дивитися на файли, про які йдеться. - -Структура каталогів виглядає приблизно так: - -/--pre -web-project/ -├── app/ ← каталог із застосунком -│ ├── Core/ ← базові класи, необхідні для роботи -│ │ └── RouterFactory.php ← конфігурація URL-адрес -│ ├── Presentation/ ← презентери, шаблони та ін. -│ │ ├── @layout.latte ← шаблон layout -│ │ └── Home/ ← каталог презентера Home -│ │ ├── HomePresenter.php ← клас презентера Home -│ │ └── default.latte ← шаблон дії default -│ └── Bootstrap.php ← завантажувальний клас Bootstrap -├── assets/ ← ресурси (SCSS, TypeScript, вихідні зображення) -├── bin/ ← скрипти, що запускаються з командного рядка -├── config/ ← конфігураційні файли -│ ├── common.neon -│ └── services.neon -├── log/ ← залоговані помилки -├── temp/ ← тимчасові файли, кеш, … -├── vendor/ ← бібліотеки, встановлені Composer -│ ├── ... -│ └── autoload.php ← автозавантаження всіх встановлених пакетів -├── www/ ← публічний каталог або document-root проекту -│ ├── assets/ ← скомпільовані статичні файли (CSS, JS, зображення, ...) -│ ├── .htaccess ← правила mod_rewrite -│ └── index.php ← первинний файл, яким запускається застосунок -└── .htaccess ← забороняє доступ до всіх каталогів, крім www -\-- - -Структуру каталогів можна будь-як змінювати, папки перейменовувати чи переміщувати, вона абсолютно гнучка. Nette, крім того, має розумне автовизначення і автоматично розпізнає розташування застосунку, включно з його базовим URL. - -Для трохи більших застосунків ми можемо папки з презентерами та шаблонами [розділити на підкаталоги |directory-structure#Presenter и та шаблони] та класи на простори імен, які називаємо модулями. - -Каталог `www/` представляє так званий публічний каталог або document-root проекту. Ви можете його перейменувати без необхідності щось додатково налаштовувати на стороні застосунку. Лише потрібно [налаштувати хостинг |nette:troubleshooting#Як змінити або видалити каталог www з URL] так, щоб document-root вказував на цей каталог. - -WebProject ви можете також одразу завантажити разом з Nette за допомогою [Composer |best-practices:composer]: - -```shell -composer create-project nette/web-project -``` - -На Linux або macOS встановіть для каталогів `log/` та `temp/` [права на запис |nette:troubleshooting#Налаштування прав доступу до каталогів]. - -Застосунок WebProject готовий до запуску, не потрібно взагалі нічого налаштовувати, і ви можете одразу відобразити його в браузері, звернувшись до папки `www/`. - - -HTTP-запит -========== - -Все починається в той момент, коли користувач у браузері відкриває сторінку. Тобто коли браузер стукає на сервер з HTTP-запитом. Запит спрямований на єдиний PHP-файл, який знаходиться в публічному каталозі `www/`, і це `index.php`. Припустимо, що йдеться про запит на адресу `https://example.com/product/123`. Завдяки відповідному [налаштуванню сервера |nette:troubleshooting#Як налаштувати сервер для гарних URL] навіть цей URL мапується на файл `index.php`, і він виконується. - -Його завдання: - -1) ініціалізувати середовище -2) отримати фабрику -3) запустити застосунок Nette, який обробить запит - -Яку ж фабрику? Ми ж не виробляємо трактори, а веб-сторінки! Зачекайте, зараз все поясниться. - -Словами "ініціалізація середовища" ми маємо на увазі, наприклад, те, що активується [Tracy|tracy:], що є чудовим інструментом для логування або візуалізації помилок. На робочому сервері він логує помилки, на сервері розробки одразу їх відображає. Отже, до ініціалізації належить і рішення, чи працює веб-сайт у робочому чи розробницькому режимі. Для цього Nette використовує [розумне автовизначення |bootstrapping#Режим розробки проти робочого режиму]: якщо ви запускаєте веб-сайт на localhost, він працює в режимі розробки. Вам не потрібно нічого налаштовувати, і застосунок одразу готовий як для розробки, так і для реального розгортання. Ці кроки виконуються і детально описані в розділі про [клас Bootstrap|bootstrapping]. - -Третім пунктом (так, другий ми пропустили, але повернемося до нього) є запуск застосунку. Обробкою HTTP-запитів у Nette займається клас `Nette\Application\Application` (далі `Application`), тому, коли ми говоримо запустити застосунок, ми маємо на увазі конкретно виклик методу з характерною назвою `run()` на об'єкті цього класу. - -Nette — це наставник, який веде вас до написання чистих застосунків за перевіреними методиками. І одна з тих абсолютно найперевіреніших називається **dependency injection**, скорочено DI. На даний момент ми не хочемо обтяжувати вас поясненням DI, для цього є [окремий розділ|dependency-injection:introduction], важливим є наслідок, що ключові об'єкти нам зазвичай створюватиме фабрика об'єктів, яка називається **DI-контейнер** (скорочено DIC). Так, це та фабрика, про яку йшлося нещодавно. І вона створить нам і об'єкт `Application`, тому нам спочатку потрібен контейнер. Отримаємо його за допомогою класу `Configurator` і змусимо його створити об'єкт `Application`, викличемо на ньому метод `run()`, і тим самим запуститься застосунок Nette. Саме це відбувається у файлі [index.php |bootstrapping#index.php]. - - -Nette Application -================= - -Клас Application має єдине завдання: відповісти на HTTP-запит. - -Застосунки, написані на Nette, поділяються на безліч так званих презентерів (в інших фреймворках ви можете зустріти термін контролер, це те саме), що є класами, кожен з яких представляє якусь конкретну сторінку веб-сайту: наприклад, головну сторінку; продукт в інтернет-магазині; форму входу; sitemap feed тощо. Застосунок може мати від одного до тисяч презентерів. - -Application починає з того, що запитує так званий маршрутизатор, щоб вирішити, якому з презентерів передати поточний запит для обробки. Маршрутизатор вирішує, чия це відповідальність. Він дивиться на вхідний URL `https://example.com/product/123` і на основі того, як він налаштований, вирішує, що це робота, наприклад, для **презентера** `Product`, від якого він захоче як **дію** відображення (`show`) продукту з `id: 123`. Пару презентер + дія прийнято записувати, розділяючи двокрапкою, як `Product:show`. - -Отже, маршрутизатор перетворив URL на пару `Presenter:action` + параметри, у нашому випадку `Product:show` + `id: 123`. Як виглядає такий маршрутизатор, ви можете побачити у файлі `app/Core/RouterFactory.php`, і ми детально його описуємо в розділі [Маршрутизація |Routing]. - -Йдемо далі. Application вже знає ім'я презентера і може продовжувати. Тим, що створить об'єкт класу `ProductPresenter`, що є кодом презентера `Product`. Точніше кажучи, він попросить DI-контейнер створити презентер, оскільки для створення існує він. - -Презентер може виглядати приблизно так: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ProductRepository $repository, - ) { - } - - public function renderShow(int $id): void - { - // отримуємо дані з моделі та передаємо в шаблон - $this->template->product = $this->repository->getProduct($id); - } -} -``` - -Обробку запиту перебирає презентер. І завдання звучить чітко: виконай дію `show` з `id: 123`. Що мовою презентерів означає, що викликається метод `renderShow()`, і в параметрі `$id` він отримує `123`. - -Презентер може обслуговувати кілька дій, тобто мати кілька методів `render()`. Але ми рекомендуємо проектувати презентери з однією або якомога меншою кількістю дій. - -Отже, викликався метод `renderShow(123)`, код якого є вигаданим прикладом, але ви можете на ньому побачити, як передаються дані в шаблон, тобто записом у `$this->template`. - -Потім презентер повертає відповідь. Це може бути HTML-сторінка, зображення, XML-документ, надсилання файлу з диска, JSON або, наприклад, перенаправлення на іншу сторінку. Важливо, що якщо ми явно не скажемо, як він має відповісти (що є випадком `ProductPresenter`), відповіддю буде відображення шаблону з HTML-сторінкою. Чому? Тому що в 99% випадків ми хочемо відобразити шаблон, тому презентер таку поведінку вважає стандартною і хоче полегшити нам роботу. У цьому сенс Nette. - -Нам навіть не потрібно вказувати, який шаблон відобразити, шлях до нього він виведе сам. У випадку дії `show` він просто спробує завантажити шаблон `show.latte` в каталозі з класом `ProductPresenter`. Також він спробує знайти layout у файлі `@layout.latte` (детальніше про [пошук шаблонів |templates#Пошук шаблонів]). - -І потім шаблони відобразить. Тим самим завдання презентера та всього застосунку виконано, і робота завершена. Якби шаблон не існував, повернулася б сторінка з помилкою 404. Більше про презентери ви дізнаєтеся на сторінці [Презентери|presenters]. - -[* request-flow.svg *] - -Для певності, спробуймо підсумувати весь процес з трохи іншим URL: - -1) URL буде `https://example.com` -2) завантажуємо застосунок, створюється контейнер і запускається `Application::run()` -3) маршрутизатор декодує URL як пару `Home:default` -4) створюється об'єкт класу `HomePresenter` -5) викликається метод `renderDefault()` (якщо існує) -6) відображається шаблон, наприклад, `default.latte` з layout, наприклад, `@layout.latte` - - -Можливо, ви зараз зіткнулися з великою кількістю нових понять, але ми віримо, що вони мають сенс. Створення застосунків у Nette — це величезне задоволення. - - -Шаблони -======= - -Коли вже зайшла мова про шаблони, у Nette використовується система шаблонів [Latte |latte:]. Тому й такі розширення `.latte` у шаблонів. Latte використовується, по-перше, тому що це найбільш захищена система шаблонів для PHP, а по-друге, також система найбільш інтуїтивно зрозуміла. Вам не потрібно вчити багато нового, достатньо знання PHP та кількох тегів. Все ви дізнаєтеся [у документації |templates]. - -У шаблоні [створюються посилання |creating-links] на інші презентери та дії так: - -```latte -деталі продукту -``` - -Просто замість реального URL ви пишете відому пару `Presenter:action` і вказуєте можливі параметри. Трюк полягає в `n:href`, яке говорить, що цей атрибут обробить Nette. І згенерує: - -```latte -деталі продукту -``` - -Генерацією URL займається вже згаданий маршрутизатор. Справа в тому, що маршрутизатори в Nette виняткові тим, що вміють виконувати не тільки перетворення з URL на пару presenter:action, але й навпаки, тобто з назви презентера + дії + параметрів генерувати URL. Завдяки цьому в Nette ви можете повністю змінити форми URL у всьому готовому застосунку, не змінюючи жодного символу в шаблоні чи презентері. Лише тим, що зміните маршрутизатор. Також завдяки цьому працює так звана канонізація, що є ще однією унікальною властивістю Nette, яка сприяє кращому SEO (оптимізації знаходження в Інтернеті), автоматично запобігаючи існуванню дубльованого контенту на різних URL. Багато програмістів вважають це вражаючим. - - -Інтерактивні компоненти -======================= - -Про презентери ми повинні розповісти вам ще одну річ: вони мають вбудовану систему компонентів. Щось подібне можуть пам'ятати ті, хто працював з Delphi або ASP.NET Web Forms, на чомусь віддалено схожому побудовані React або Vue.js. У світі PHP-фреймворків це абсолютно унікальна річ. - -Компоненти — це окремі повторно використовувані одиниці, які ми вставляємо на сторінки (тобто презентери). Це можуть бути [форми |forms:in-presenter], [datagrid |https://componette.org/contributte/datagrid/], меню, опитування, власне все, що має сенс використовувати повторно. Ми можемо створювати власні компоненти або використовувати деякі з [величезної пропозиції |https://componette.org] компонентів з відкритим кодом. - -Компоненти суттєво впливають на підхід до створення застосунків. Вони відкриють вам нові можливості складання сторінок з готових одиниць. І до того ж мають щось спільне з [Голлівудом |components#Голлівудський стиль]. - - -DI-контейнер та конфігурація -============================ - -DI-контейнер, або фабрика об'єктів, є серцем усього застосунку. - -Не хвилюйтеся, це не якийсь магічний чорний ящик, як могло б здатися з попередніх рядків. Власне, це один досить нудний PHP-клас, який генерує Nette і зберігає в каталозі з кешем. Він має багато методів, названих як `createServiceAbcd()`, і кожен з них вміє створити та повернути якийсь об'єкт. Так, там є і метод `createServiceApplication()`, який створить `Nette\Application\Application`, який нам був потрібен у файлі `index.php` для запуску застосунку. І є методи, що створюють окремі презентери. І так далі. - -Об'єктам, які створює DI-контейнер, з якоїсь причини називають сервісами. - -Що в цьому класі справді особливого, так це те, що його програмуєте не ви, а фреймворк. Він дійсно генерує PHP-код і зберігає його на диску. Ви лише даєте інструкції, які об'єкти має вміти створювати контейнер і як саме. І ці інструкції записані в [конфігураційних файлах |bootstrapping#Конфігурація DI-контейнера], для яких використовується формат [NEON|neon:format], і тому вони мають розширення `.neon`. - -Конфігураційні файли служать виключно для інструктування DI-контейнера. Отже, коли, наприклад, я вказую в секції [session |http:configuration#Сесія] опцію `expiration: 14 days`, то DI-контейнер при створенні об'єкта `Nette\Http\Session`, що представляє сесію, викличе його метод `setExpiration('14 days')`, і тим самим конфігурація стане реальністю. - -Для вас підготовлено цілий розділ, що описує, що все можна [налаштувати |nette:configuring] та як [визначити власні сервіси |dependency-injection:services]. - -Як тільки ви трохи заглибитеся у створення сервісів, ви натрапите на слово [autowiring |dependency-injection:autowiring]. Це фішка, яка неймовірним чином спростить вам життя. Вона вміє автоматично передавати об'єкти туди, де вони вам потрібні (наприклад, у конструкторах ваших класів), не вимагаючи від вас нічого робити. Ви дізнаєтеся, що DI-контейнер у Nette — це маленьке диво. - - -Куди далі? -========== - -Ми пройшлися по основних принципах застосунків у Nette. Поки що дуже поверхнево, але скоро ви заглибитеся глибше і з часом створите чудові веб-застосунки. Куди йти далі? Ви вже спробували підручник [Пишемо перший застосунок|quickstart:]? - -Крім вищеописаного, Nette має цілий арсенал [корисних класів|utils:], [шар бази даних|database:], тощо. Спробуйте просто проклацати документацію. Або [блог|https://blog.nette.org]. Ви відкриєте багато цікавого. - -Нехай фреймворк приносить вам багато радості 💙 diff --git a/application/uk/multiplier.texy b/application/uk/multiplier.texy deleted file mode 100644 index 4b882fd4d4..0000000000 --- a/application/uk/multiplier.texy +++ /dev/null @@ -1,63 +0,0 @@ -Multiplier: динамічні компоненти -******************************** - -.[perex] -Інструмент для динамічного створення інтерактивних компонентів - -Почнемо з типового прикладу: маємо список товарів в інтернет-магазині, причому біля кожного ми хочемо вивести форму для додавання товару в кошик. Одним з можливих варіантів є обгортання всього списку в одну форму. Набагато зручніший спосіб нам пропонує [api:Nette\Application\UI\Multiplier]. - -Multiplier дозволяє зручно визначити фабрику для кількох компонентів. Він працює за принципом вкладених компонентів - кожен компонент, що успадковує від [api:Nette\ComponentModel\Container], може містити інші компоненти. - -.[tip] -Див. розділ про [модель компонентів |components#Компоненти до глибини] у документації або [лекцію від Honza Tvrdík|https://www.youtube.com/watch?v=8y3LLexWu-I]. - -Суть Multiplier полягає в тому, що він виступає в ролі батька, який може динамічно створювати своїх нащадків за допомогою callback-функції, переданої в конструкторі. Див. приклад: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function () { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Кількість товару:') - ->setRequired(); - $form->addSubmit('send', 'Додати в кошик'); - return $form; - }); -} -``` - -Тепер ми можемо в шаблоні просто біля кожного товару відобразити форму - і кожна буде дійсно унікальним компонентом. - -```latte -{foreach $items as $item} -

    {$item->title}

    - {$item->description} - - {control "shopForm-$item->id"} -{/foreach} -``` - -Аргумент, переданий у тезі `{control}`, має формат, який говорить: - -1. отримай компонент `shopForm` -2. і з нього отримай нащадка `$item->id` - -При першому виклику пункту **1.** `shopForm` ще не існує, тому викликається його фабрика `createComponentShopForm`. На отриманому компоненті (екземплярі Multiplier) потім викликається фабрика конкретної форми - це анонімна функція, яку ми передали Multiplier у конструкторі. - -У наступній ітерації foreach метод `createComponentShopForm` вже не буде викликаний (компонент існує), але оскільки ми шукаємо іншого його нащадка (`$item->id` буде різним у кожній ітерації), знову буде викликана анонімна функція і поверне нам нову форму. - -Єдине, що залишається, - це забезпечити, щоб форма додала в кошик дійсно той товар, який потрібно - наразі форма біля кожного товару абсолютно однакова. Допоможе нам властивість Multiplier (і загалом кожної фабрики компонентів у Nette Framework), а саме те, що кожна фабрика як свій перший аргумент отримує назву створюваного компонента. У нашому випадку це буде `$item->id`, що є саме тим даними, які нам потрібні. Достатньо лише трохи змінити створення форми: - -```php -protected function createComponentShopForm(): Multiplier -{ - return new Multiplier(function ($itemId) { - $form = new Nette\Application\UI\Form; - $form->addInteger('count', 'Кількість товару:') - ->setRequired(); - $form->addHidden('itemId', $itemId); - $form->addSubmit('send', 'Додати в кошик'); - return $form; - }); -} -``` diff --git a/application/uk/presenters.texy b/application/uk/presenters.texy deleted file mode 100644 index 8284bc3a31..0000000000 --- a/application/uk/presenters.texy +++ /dev/null @@ -1,500 +0,0 @@ -Презентери -********** - -
    - -Ми ознайомимося з тим, як у Nette пишуться презентери та шаблони. Після прочитання ви будете знати: - -- як працює презентер -- що таке персистентні параметри -- як відображаються шаблони - -
    - -[Ми вже знаємо |how-it-works#Nette Application], що презентер — це клас, який представляє певну конкретну сторінку веб-застосунку, наприклад, головну сторінку; продукт в інтернет-магазині; форму входу; стрічку sitemap тощо. Застосунок може мати від одного до тисяч презентерів. В інших фреймворках їх також називають контролерами. - -Зазвичай під поняттям презентер мається на увазі нащадок класу [api:Nette\Application\UI\Presenter], який підходить для генерації веб-інтерфейсів і якому ми присвятимо решту цього розділу. У загальному сенсі презентер — це будь-який об'єкт, що реалізує інтерфейс [api:Nette\Application\IPresenter]. - - -Життєвий цикл презентера -======================== - -Завданням презентера є обробити запит і повернути відповідь (це може бути HTML-сторінка, зображення, перенаправлення тощо). - -Отже, на початку йому передається запит. Це не безпосередньо HTTP-запит, а об'єкт [api:Nette\Application\Request], в який був перетворений HTTP-запит за допомогою маршрутизатора. З цим об'єктом ми зазвичай не стикаємося, оскільки презентер розумно делегує обробку запиту іншим методам, які ми зараз розглянемо. - -[* lifecycle.svg *] *** *Життєвий цикл презентера* .<> - -Зображення представляє список методів, які послідовно викликаються зверху вниз, якщо вони існують. Жоден з них не обов'язковий, ми можемо мати абсолютно порожній презентер без жодного методу і побудувати на ньому простий статичний веб-сайт. - - -`__construct()` ---------------- - -Конструктор не зовсім належить до життєвого циклу презентера, оскільки викликається в момент створення об'єкта. Але ми згадуємо його через важливість. Конструктор (разом з [методом inject|best-practices:inject-method-attribute]) служить для передачі залежностей. - -Презентер не повинен займатися бізнес-логікою застосунку, записувати та читати з бази даних, виконувати обчислення тощо. Для цього існують класи з шару, який ми називаємо моделлю. Наприклад, клас `ArticleRepository` може відповідати за завантаження та збереження статей. Щоб презентер міг з ним працювати, він отримує його [передачею за допомогою dependency injection |dependency-injection:passing-dependencies]: - - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articles, - ) { - } -} -``` - - -`startup()` ------------ - -Одразу після отримання запиту викликається метод `startup()`. Ви можете використовувати його для ініціалізації властивостей, перевірки прав користувача тощо. Вимагається, щоб метод завжди викликав батьківський `parent::startup()`. - - -`action(args...)` .{toc: action()} --------------------------------------------------- - -Аналог методу `render()`. У той час як `render()` призначений для підготовки даних для конкретного шаблону, який потім відображається, то в `action()` обробляється запит без зв'язку з відображенням шаблону. Наприклад, обробляються дані, користувач входить або виходить з системи, тощо, а потім [перенаправляється в інше місце |#Перенаправлення]. - -Важливо, що `action()` викликається раніше, ніж `render()`, тому в ньому ми можемо, за потреби, змінити подальший хід подій, тобто змінити шаблон, який буде відображатися, а також метод `render()`, який буде викликатися. Це робиться за допомогою `setView('іншийView')`. - -Методу передаються параметри із запиту. Можна і рекомендується вказувати типи параметрів, наприклад, `actionShow(int $id, ?string $slug = null)` - якщо параметр `id` буде відсутній або якщо він не буде цілим числом, презентер поверне [помилку 404 |#Помилка 404 тощо] і завершить роботу. - - -`handle(args...)` .{toc: handle()} --------------------------------------------------- - -Метод обробляє так звані сигнали, з якими ми познайомимося в розділі, присвяченому [компонентам |components#Сигнал]. Він призначений переважно для компонентів та обробки AJAX-запитів. - -Методу передаються параметри із запиту, як у випадку `action()`, включно з перевіркою типів. - - -`beforeRender()` ----------------- - -Метод `beforeRender`, як випливає з назви, викликається перед кожним методом `render()`. Використовується для спільної конфігурації шаблону, передачі змінних для layout тощо. - - -`render(args...)` .{toc: render()} ----------------------------------------------- - -Місце, де ми готуємо шаблон до подальшого відображення, передаємо йому дані тощо. - -Методу передаються параметри із запиту, як у випадку `action()`, включно з перевіркою типів. - -```php -public function renderShow(int $id): void -{ - // отримуємо дані з моделі та передаємо в шаблон - $this->template->article = $this->articles->getById($id); -} -``` - - -`afterRender()` ---------------- - -Метод `afterRender`, як знову ж таки випливає з назви, викликається після кожного методу `render()`. Використовується досить рідко. - - -`shutdown()` ------------- - -Викликається в кінці життєвого циклу презентера. - - -**Добра порада, перш ніж йти далі**. Презентер, як бачимо, може обслуговувати кілька дій/view, тобто мати кілька методів `render()`. Але ми рекомендуємо проектувати презентери з однією або якомога меншою кількістю дій. - - -Надсилання відповіді -==================== - -Відповіддю презентера зазвичай є [відображення шаблону з HTML-сторінкою|templates], але це може бути також надсилання файлу, JSON або, наприклад, перенаправлення на іншу сторінку. - -У будь-який момент життєвого циклу ми можемо одним з наступних методів надіслати відповідь і одночасно завершити роботу презентера: - -- `redirect()`, `redirectPermanent()`, `redirectUrl()` та `forward()` [перенаправляє |#Перенаправлення] -- `error()` завершує презентер [через помилку |#Помилка 404 тощо] -- `sendJson($data)` завершує презентер і [надсилає дані |#Надсилання JSON] у форматі JSON -- `sendTemplate()` завершує презентер і негайно [відображає шаблон |templates] -- `sendResponse($response)` завершує презентер і надсилає [власну відповідь |#Відповіді] -- `terminate()` завершує презентер без відповіді - -Якщо ви не викличете жоден з цих методів, презентер автоматично перейде до відображення шаблону. Чому? Тому що в 99% випадків ми хочемо відобразити шаблон, тому презентер таку поведінку вважає стандартною і хоче полегшити нам роботу. - - -Створення посилань -================== - -Презентер має метод `link()`, за допомогою якого можна створювати URL-посилання на інші презентери. Першим параметром є цільовий презентер та дія, далі йдуть передані аргументи, які можуть бути вказані як масив: - -```php -$url = $this->link('Product:show', $id); - -$url = $this->link('Product:show', [$id, 'lang' => 'cs']); -``` - -У шаблоні створюються посилання на інші презентери та дії таким чином: - -```latte -деталі продукту -``` - -Просто замість реального URL ви пишете відому пару `Presenter:action` і вказуєте можливі параметри. Трюк полягає в `n:href`, яке говорить, що цей атрибут обробить Latte і згенерує реальний URL. У Nette вам взагалі не потрібно думати про URL, лише про презентери та дії. - -Більше інформації ви знайдете в розділі [Створення URL-посилань|creating-links]. - - -Перенаправлення -=============== - -Для переходу на інший презентер служать методи `redirect()` та `forward()`, які мають дуже схожий синтаксис, як метод [link() |#Створення посилань]. - -Метод `forward()` переходить на новий презентер негайно без HTTP-перенаправлення: - -```php -$this->forward('Product:show'); -``` - -Приклад так званого тимчасового перенаправлення з HTTP-кодом 302 (або 303, якщо метод поточного запиту POST): - -```php -$this->redirect('Product:show', $id); -``` - -Постійне перенаправлення з HTTP-кодом 301 досягається так: - -```php -$this->redirectPermanent('Product:show', $id); -``` - -На інший URL поза застосунком можна перенаправити методом `redirectUrl()`. Як другий параметр можна вказати HTTP-код, стандартний — 302 (або 303, якщо метод поточного запиту POST): - -```php -$this->redirectUrl('https://nette.org'); -``` - -Перенаправлення негайно завершує роботу презентера, викидаючи так званий тихий завершальний виняток `Nette\Application\AbortException`. - -Перед перенаправленням можна надіслати [#flash-повідомлення], тобто повідомлення, які будуть відображені в шаблоні після перенаправлення. - - -Flash-повідомлення -================== - -Це повідомлення, які зазвичай інформують про результат якоїсь операції. Важливою особливістю flash-повідомлень є те, що вони доступні в шаблоні навіть після перенаправлення. Навіть після відображення вони залишаються активними ще 30 секунд – наприклад, на випадок, якщо через помилку передачі користувач оновить сторінку - повідомлення йому одразу не зникне. - -Достатньо викликати метод [flashMessage() |api:Nette\Application\UI\Control::flashMessage()], і про передачу в шаблон подбає презентер. Першим параметром є текст повідомлення, а необов'язковим другим параметром — його тип (error, warning, info тощо). Метод `flashMessage()` повертає екземпляр flash-повідомлення, до якого можна додавати додаткову інформацію. - -```php -$this->flashMessage('Елемент було видалено.'); -$this->redirect(/* ... */); // і перенаправляємо -``` - -У шаблоні ці повідомлення доступні у змінній `$flashes` як об'єкти `stdClass`, які містять властивості `message` (текст повідомлення), `type` (тип повідомлення) і можуть містити вже згадану користувацьку інформацію. Відобразимо їх, наприклад, так: - -```latte -{foreach $flashes as $flash} -
    {$flash->message}
    -{/foreach} -``` - - -Помилка 404 тощо. -================= - -Якщо неможливо виконати запит, наприклад, через те, що стаття, яку ми хочемо відобразити, не існує в базі даних, ми викидаємо помилку 404 методом `error(?string $message = null, int $httpCode = 404)`. - -```php -public function renderShow(int $id): void -{ - $article = $this->articles->getById($id); - if (!$article) { - $this->error(); - } - // ... -} -``` - -HTTP-код помилки можна передати як другий параметр, стандартний — 404. Метод працює так, що викидає виняток `Nette\Application\BadRequestException`, після чого `Application` передає управління error-презентеру. Це презентер, завданням якого є відобразити сторінку, що інформує про помилку. Налаштування error-презентера здійснюється в [конфігурації application|configuration]. - - -Надсилання JSON -=============== - -Приклад action-методу, який надсилає дані у форматі JSON і завершує презентер: - -```php -public function actionData(): void -{ - $data = ['hello' => 'nette']; - $this->sendJson($data); -} -``` - - -Параметри запиту .{data-version:3.1.14} -======================================= - -Презентер, а також кожен компонент, отримує з HTTP-запиту свої параметри. Їхнє значення ви можете дізнатися методом `getParameter($name)` або `getParameters()`. Значення є рядками або масивами рядків, це, по суті, сирі дані, отримані безпосередньо з URL. - -Для більшої зручності рекомендуємо зробити параметри доступними через властивості. Достатньо позначити їх атрибутом `#[Parameter]`: - -```php -use Nette\Application\Attributes\Parameter; // цей рядок важливий - -class HomePresenter extends Nette\Application\UI\Presenter -{ - #[Parameter] - public string $theme; // має бути public -} -``` - -Для властивості рекомендуємо вказувати тип даних (наприклад, `string`), і Nette автоматично перетворить значення відповідно до нього. Значення параметрів також можна [валідувати |#Валідація параметрів]. - -При створенні посилання можна безпосередньо встановити значення параметрів: - -```latte -натисніть -``` - - -Персистентні параметри -====================== - -Персистентні параметри служать для підтримки стану між різними запитами. Їхнє значення залишається незмінним навіть після натискання на посилання. На відміну від даних у сесії, вони передаються в URL. І це відбувається повністю автоматично, тому не потрібно їх явно вказувати в `link()` або `n:href`. - -Приклад використання? У вас багатомовний застосунок. Поточна мова — це параметр, який повинен постійно бути частиною URL. Але було б надзвичайно втомливо вказувати його в кожному посиланні. Тож ви робите його персистентним параметром `lang`, і він буде передаватися сам. Чудово! - -Створення персистентного параметра в Nette надзвичайно просте. Достатньо створити публічну властивість і позначити її атрибутом: (раніше використовувалося `/** @persistent */`) - -```php -use Nette\Application\Attributes\Persistent; // цей рядок важливий - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; // має бути public -} -``` - -Якщо `$this->lang` матиме значення, наприклад, `'en'`, то й посилання, створені за допомогою `link()` або `n:href`, міститимуть параметр `lang=en`. І після натискання на посилання знову буде `$this->lang = 'en'`. - -Для властивості рекомендуємо вказувати тип даних (наприклад, `string`) і ви можете вказати значення за замовчуванням. Значення параметрів можна [валідувати |#Валідація параметрів]. - -Персистентні параметри стандартно передаються між усіма діями даного презентера. Щоб вони передавалися і між кількома презентерами, їх потрібно визначити або: - -- у спільному предку, від якого успадковують презентери -- у трейті, який використовують презентери: - -```php -trait LanguageAware -{ - #[Persistent] - public string $lang; -} - -class ProductPresenter extends Nette\Application\UI\Presenter -{ - use LanguageAware; -} -``` - -При створенні посилання можна змінити значення персистентного параметра: - -```latte -деталі українською -``` - -Або його можна *скинути*, тобто видалити з URL. Тоді він набуде свого значення за замовчуванням: - -```latte -натисніть -``` - - -Інтерактивні компоненти -======================= - -Презентери мають вбудовану систему компонентів. Компоненти — це окремі повторно використовувані одиниці, які ми вставляємо в презентери. Це можуть бути [форми |forms:in-presenter], datagrid, меню, власне все, що має сенс використовувати повторно. - -Як компоненти вставляються в презентер і потім використовуються? Це ви дізнаєтеся в розділі [Компоненти |components]. Ви навіть дізнаєтеся, що вони мають спільного з Голлівудом. - -А де я можу отримати компоненти? На сторінці [Componette |https://componette.org/search/component] ви знайдете компоненти з відкритим кодом, а також багато інших доповнень для Nette, які сюди розмістили добровольці зі спільноти навколо фреймворку. - - -Заглиблюємося -============= - -.[tip] -З тим, що ми досі показали в цьому розділі, ви, ймовірно, цілком впораєтеся. Наступні рядки призначені для тих, хто цікавиться презентерами до глибини і хоче знати абсолютно все. - - -Валідація параметрів --------------------- - -Значення [параметрів запиту |#Параметри запиту] та [персистентних параметрів |#Персистентні параметри], отримані з URL, записує у властивості метод `loadState()`. Він також перевіряє, чи відповідає тип даних, вказаний у властивості, інакше відповідає помилкою 404 і сторінка не відображається. - -Ніколи сліпо не довіряйте параметрам, оскільки їх може легко перезаписати користувач в URL. Таким чином, наприклад, перевіримо, чи мова `$this->lang` є серед підтримуваних. Підходящим способом є перезапис згаданого методу `loadState()`: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $lang; - - public function loadState(array $params): void - { - parent::loadState($params); // тут встановлюється $this->lang - // далі йде власна перевірка значення: - if (!in_array($this->lang, ['en', 'cs'])) { - $this->error(); - } - } -} -``` - - -Збереження та відновлення запиту --------------------------------- - -Запит, який обробляє презентер, є об'єктом [api:Nette\Application\Request] і повертає його метод презентера `getRequest()`. - -Поточний запит можна зберегти в сесію або, навпаки, відновити з неї і змусити презентер знову його виконати. Це корисно, наприклад, у ситуації, коли користувач заповнює форму, і його сесія закінчується. Щоб не втратити дані, перед перенаправленням на сторінку входу поточний запит зберігаємо в сесію за допомогою `$reqId = $this->storeRequest()`, яке повертає його ідентифікатор у вигляді короткого рядка, і передаємо його як параметр презентеру входу. - -Після входу викликаємо метод `$this->restoreRequest($reqId)`, який витягує запит із сесії та перенаправляє на нього. Метод при цьому перевіряє, що запит створив той самий користувач, який зараз увійшов. Якщо увійшов інший користувач або ключ недійсний, він нічого не робить, і програма продовжує роботу. - -Подивіться на інструкцію [Як повернутися на попередню сторінку |best-practices:restore-request]. - - -Канонізація ------------ - -Презентери мають одну справді чудову властивість, яка сприяє кращому SEO (оптимізації знаходження в Інтернеті). Вони автоматично запобігають існуванню дубльованого контенту на різних URL. Якщо до певної цілі веде кілька URL-адрес, наприклад, `/index` та `/index?page=1`, фреймворк визначає одну з них як первинну (канонічну) і решту на неї перенаправляє за допомогою HTTP-коду 301. Завдяки цьому пошукові системи не індексують ваші сторінки двічі і не розмивають їхній page rank. - -Цей процес називається канонізацією. Канонічним URL є той, який генерує [маршрутизатор|routing], зазвичай це перший відповідний маршрут у колекції. - -Канонізація стандартно ввімкнена і її можна вимкнути через `$this->autoCanonicalize = false`. - -Перенаправлення не відбувається при AJAX- або POST-запиті, оскільки це призвело б до втрати даних або не мало б доданої вартості з точки зору SEO. - -Канонізацію можна викликати й вручну за допомогою методу `canonicalize()`, якому, подібно до методу `link()`, передається презентер, дія та параметри. Він створює посилання і порівнює його з поточною URL-адресою. Якщо вони відрізняються, то перенаправляє на згенероване посилання. - -```php -public function actionShow(int $id, ?string $slug = null): void -{ - $realSlug = $this->facade->getSlugForId($id); - // перенаправляє, якщо $slug відрізняється від $realSlug - $this->canonicalize('Product:show', [$id, $realSlug]); -} -``` - - -Події ------ - -Крім методів `startup()`, `beforeRender()` та `shutdown()`, які викликаються як частина життєвого циклу презентера, можна визначити ще інші функції, які мають автоматично викликатися. Презентер визначає так звану [подію |nette:glossary#Події události], обробники якої ви додаєте до масивів `$onStartup`, `$onRender` та `$onShutdown`. - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - public function __construct() - { - $this->onStartup[] = function () { - // ... - }; - } -} -``` - -Обробники в масиві `$onStartup` викликаються безпосередньо перед методом `startup()`, далі `$onRender` між `beforeRender()` та `render()`, і нарешті `$onShutdown` безпосередньо перед `shutdown()`. - - -Відповіді ---------- - -Відповідь, яку повертає презентер, є об'єктом, що реалізує інтерфейс [api:Nette\Application\Response]. Доступно багато готових відповідей: - -- [api:Nette\Application\Responses\CallbackResponse] - надсилає callback -- [api:Nette\Application\Responses\FileResponse] - надсилає файл -- [api:Nette\Application\Responses\ForwardResponse] - forward() -- [api:Nette\Application\Responses\JsonResponse] - надсилає JSON -- [api:Nette\Application\Responses\RedirectResponse] - перенаправлення -- [api:Nette\Application\Responses\TextResponse] - надсилає текст -- [api:Nette\Application\Responses\VoidResponse] - порожня відповідь - -Відповіді надсилаються методом `sendResponse()`: - -```php -use Nette\Application\Responses; - -// Простий текст -$this->sendResponse(new Responses\TextResponse('Hello Nette!')); - -// Надсилає файл -$this->sendResponse(new Responses\FileResponse(__DIR__ . '/invoice.pdf', 'Invoice13.pdf')); - -// Відповіддю буде callback -$callback = function (Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse) { - if ($httpResponse->getHeader('Content-Type') === 'text/html') { - echo '

    Hello

    '; - } -}; -$this->sendResponse(new Responses\CallbackResponse($callback)); -``` - - -Обмеження доступу за допомогою `#[Requires]` .{data-version:3.2.2} ------------------------------------------------------------------- - -Атрибут `#[Requires]` надає розширені можливості для обмеження доступу до презентерів та їхніх методів. Його можна використовувати для специфікації HTTP-методів, вимоги AJAX-запиту, обмеження на той самий походження (same origin) та доступу лише через переадресацію. Атрибут можна застосовувати як до класів презентерів, так і до окремих методів `action()`, `render()`, `handle()` та `createComponent()`. - -Ви можете визначити такі обмеження: -- на HTTP-методи: `#[Requires(methods: ['GET', 'POST'])]` -- вимога AJAX-запиту: `#[Requires(ajax: true)]` -- доступ лише з того самого походження: `#[Requires(sameOrigin: true)]` -- доступ лише через forward: `#[Requires(forward: true)]` -- обмеження на конкретні дії: `#[Requires(actions: 'default')]` - -Деталі ви знайдете в інструкції [Як використовувати атрибут Requires |best-practices:attribute-requires]. - - -Перевірка HTTP-методу ---------------------- - -Презентери в Nette автоматично перевіряють HTTP-метод кожного вхідного запиту. Причиною цієї перевірки є насамперед безпека. Стандартно дозволені методи `GET`, `POST`, `HEAD`, `PUT`, `DELETE`, `PATCH`. - -Якщо ви хочете додатково дозволити, наприклад, метод `OPTIONS`, використовуйте для цього атрибут `#[Requires]` (з Nette Application v3.2): - -```php -#[Requires(methods: ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])] -class MyPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -У версії 3.1 перевірка проводиться в `checkHttpMethod()`, яка з'ясовує, чи міститься метод, вказаний у запиті, в масиві `$presenter->allowedMethods`. Додавання методу зробіть так: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - protected function checkHttpMethod(): void - { - $this->allowedMethods[] = 'OPTIONS'; - parent::checkHttpMethod(); - } -} -``` - -Важливо підкреслити, що якщо ви дозволите метод `OPTIONS`, ви повинні потім також належним чином обробити його в рамках свого презентера. Метод часто використовується як так званий preflight request, який браузер автоматично надсилає перед фактичним запитом, коли потрібно з'ясувати, чи дозволений запит з точки зору політики CORS (Cross-Origin Resource Sharing). Якщо ви дозволите метод, але не реалізуєте правильну відповідь, це може призвести до невідповідностей та потенційних проблем безпеки. - - -Подальше читання -================ - -- [Методи та атрибути inject |best-practices:inject-method-attribute] -- [Компонування презентерів з трейтів |best-practices:presenter-traits] -- [Передача налаштувань у презентери |best-practices:passing-settings-to-presenters] -- [Як повернутися на попередню сторінку |best-practices:restore-request] diff --git a/application/uk/routing.texy b/application/uk/routing.texy deleted file mode 100644 index ab3b132ccd..0000000000 --- a/application/uk/routing.texy +++ /dev/null @@ -1,721 +0,0 @@ -Маршрутизація -************* - -
    - -Маршрутизатор відповідає за все, що стосується URL-адрес, щоб вам більше не доводилося над ними замислюватися. Ми покажемо: - -- як налаштувати маршрутизатор, щоб URL були такими, як ви хочете -- поговоримо про SEO та перенаправлення -- і покажемо, як написати власний маршрутизатор - -
    - - -Більш людські URL (або також cool чи pretty URL) є більш зручними для використання, легше запам'ятовуються та позитивно впливають на SEO. Nette про це думає і повністю йде назустріч розробникам. Ви можете для свого застосунку розробити саме таку структуру URL-адрес, яку захочете. Ви можете її розробити навіть тоді, коли застосунок вже готовий, оскільки це обійдеться без втручань у код чи шаблони. Визначається це елегантним способом в одному [єдиному місці |#Включення в застосунок], у маршрутизаторі, і таким чином не розкидано у вигляді анотацій у всіх презентерах. - -Маршрутизатор у Nette є винятковим тим, що він **двосторонній.** Він вміє як декодувати URL в HTTP-запиті, так і створювати посилання. Отже, він відіграє ключову роль у [Nette Application |how-it-works#Nette Application], оскільки не тільки вирішує, який презентер та дія виконуватимуть поточний запит, але також використовується для [генерування URL |creating-links] у шаблоні тощо. - -Однак маршрутизатор не обмежений лише цим використанням, ви можете його використовувати в застосунках, де взагалі не використовуються презентери, для REST API тощо. Більше в частині [#самостійне використання]. - - -Колекція маршрутів -================== - -Найприємніший спосіб визначити вигляд URL-адрес у застосунку пропонує клас [api:Nette\Application\Routers\RouteList]. Визначення складається зі списку так званих маршрутів, тобто масок URL-адрес та пов'язаних з ними презентерів та дій за допомогою простого API. Маршрути не потрібно якось називати. - -```php -$router = new Nette\Application\Routers\RouteList; -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('article/', 'Article:view'); -// ... -``` - -Приклад говорить, що якщо в браузері відкрити `https://domain.com/rss.xml`, відобразиться презентер `Feed` з дією `rss`, якщо `https://domain.com/article/12`, відобразиться презентер `Article` з дією `view` тощо. У разі не знаходження відповідного маршруту Nette Application реагує викиданням винятку [BadRequestException |api:Nette\Application\BadRequestException], який користувачеві відображається як сторінка помилки 404 Not Found. - - -Порядок маршрутів ------------------ - -Абсолютно **ключовим є порядок**, у якому вказані окремі маршрути, оскільки вони оцінюються послідовно зверху вниз. Діє правило, що маршрути декларуємо **від специфічних до загальних**: - -```php -// ПОГАНО: 'rss.xml' перехопить перший маршрут і розуміє цей рядок як -$router->addRoute('', 'Article:view'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// ДОБРЕ -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('', 'Article:view'); -``` - -Маршрути оцінюються зверху вниз також при генерації посилань: - -```php -// ПОГАНО: посилання на 'Feed:rss' згенерує як 'admin/feed/rss' -$router->addRoute('admin//', 'Admin:default'); -$router->addRoute('rss.xml', 'Feed:rss'); - -// ДОБРЕ -$router->addRoute('rss.xml', 'Feed:rss'); -$router->addRoute('admin//', 'Admin:default'); -``` - -Ми не будемо приховувати від вас, що правильне складання маршрутів вимагає певної вправності. Перш ніж ви в неї проникнете, вам буде корисним помічником [панель маршрутизації |#Налагодження маршрутизатора]. - - -Маска та параметри ------------------- - -Маска описує відносний шлях від кореневого каталогу веб-сайту. Найпростішою маскою є статичний URL: - -```php -$router->addRoute('products', 'Products:default'); -``` - -Часто маски містять так звані **параметри**. Вони вказані в кутових дужках (наприклад, ``) і передаються до цільового презентера, наприклад, методу `renderShow(int $year)` або до персистентного параметра `$year`: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Приклад говорить, що якщо в браузері відкрити `https://example.com/chronicle/2020`, відобразиться презентер `History` з дією `show` та параметром `year: 2020`. - -Параметрам можна визначити значення за замовчуванням безпосередньо в масці, і тим самим вони стануть необов'язковими: - -```php -$router->addRoute('chronicle/', 'History:show'); -``` - -Маршрут тепер прийматиме й URL `https://example.com/chronicle/`, який знову відобразить `History:show` з параметром `year: 2020`. - -Параметром може бути, звичайно, й ім'я презентера та дії. Наприклад, так: - -```php -$router->addRoute('/', 'Home:default'); -``` - -Зазначений маршрут приймає, наприклад, URL у вигляді `/article/edit` або також `/catalog/list` і розуміє їх як презентери та дії `Article:edit` та `Catalog:list`. - -Водночас він надає параметрам `presenter` та `action` значення за замовчуванням `Home` та `default`, і вони, отже, також є необов'язковими. Тому маршрут приймає й URL у вигляді `/article` і розуміє його як `Article:default`. Або навпаки, посилання на `Product:default` згенерує шлях `/product`, посилання на стандартний `Home:default` шлях `/`. - -Маска може описувати не тільки відносний шлях від кореневого каталогу веб-сайту, але й абсолютний шлях, якщо починається зі слеша, або навіть цілий абсолютний URL, якщо починається з двох слешів: - -```php -// відносно до document root -$router->addRoute('/', /* ... */); - -// абсолютний шлях (відносно до домену) -$router->addRoute('//', /* ... */); - -// абсолютний URL включно з доменом (відносно до схеми) -$router->addRoute('//.example.com//', /* ... */); - -// абсолютний URL включно зі схемою -$router->addRoute('https://.example.com//', /* ... */); -``` - - -Валідаційні вирази ------------------- - -Для кожного параметра можна встановити умову валідації за допомогою [регулярного виразу|https://www.php.net/manual/en/reference.pcre.pattern.syntax.php]. Наприклад, параметру `id` визначимо, що він може набувати лише цифр за допомогою регулярного виразу `\d+`: - -```php -$router->addRoute('/[/]', /* ... */); -``` - -Стандартним регулярним виразом для всіх параметрів є `[^/]+`, тобто все, крім слеша. Якщо параметр має приймати й слеші, вкажемо вираз `.+`: - -```php -// приймає https://example.com/a/b/c, path буде 'a/b/c' -$router->addRoute('', /* ... */); -``` - - -Необов'язкові послідовності ---------------------------- - -У масці можна позначати необов'язкові частини за допомогою квадратних дужок. Необов'язковою може бути будь-яка частина маски, в ній можуть знаходитися й параметри: - -```php -$router->addRoute('[/]', /* ... */); - -// Приймає шляхи: -// /cs/download => lang => cs, name => download -// /download => lang => null, name => download -``` - -Коли параметр є частиною необов'язкової послідовності, він, зрозуміло, також стає необов'язковим. Якщо він не має вказаного значення за замовчуванням, то буде null. - -Необов'язкові частини можуть бути й у домені: - -```php -$router->addRoute('//[.]example.com//', /* ... */); -``` - -Послідовності можна довільно вкладати та комбінувати: - -```php -$router->addRoute( - '[[-]/][/page-]', - 'Home:default', -); - -// Приймає шляхи: -// /cs/hello -// /en-us/hello -// /hello -// /hello/page-12 -``` - -При генерації URL прагнуть до найкоротшого варіанту, тому все, що можна пропустити, пропускається. Тому, наприклад, маршрут `index[.html]` генерує шлях `/index`. Змінити поведінку можна, вказавши знак оклику за лівою квадратною дужкою: - -```php -// приймає /hello та /hello.html, генерує /hello -$router->addRoute('[.html]', /* ... */); - -// приймає /hello та /hello.html, генерує /hello.html -$router->addRoute('[!.html]', /* ... */); -``` - -Необов'язкові параметри (тобто параметри, що мають значення за замовчуванням) без квадратних дужок поводяться, по суті, так, ніби вони були взяті в дужки таким чином: - -```php -$router->addRoute('//', /* ... */); - -// відповідає цьому: -$router->addRoute('[/[/[]]]', /* ... */); -``` - -Якщо ми хочемо вплинути на поведінку кінцевого слеша, щоб, наприклад, замість `/home/` генерувалося лише `/home`, цього можна досягти так: - -```php -$router->addRoute('[[/[/]]]', /* ... */); -``` - - -Заступні знаки --------------- - -У масці абсолютного шляху ми можемо використовувати наступні заступні знаки і уникнути так, наприклад, необхідності записувати в маску домен, який може відрізнятися в середовищі розробки та робочому середовищі: - -- `%tld%` = домен верхнього рівня, наприклад, `com` або `org` -- `%sld%` = домен другого рівня, наприклад, `example` -- `%domain%` = домен без субдоменів, наприклад, `example.com` -- `%host%` = весь хост, наприклад, `www.example.com` -- `%basePath%` = шлях до кореневого каталогу - -```php -$router->addRoute('//www.%domain%/%basePath%//', /* ... */); -$router->addRoute('//www.%sld%.%tld%/%basePath%//addRoute('/[/]', [ - 'presenter' => 'Home', - 'action' => 'default', -]); -``` - -Для більш детальної специфікації можна використовувати ще більш розширену форму, де крім значень за замовчуванням ми можемо встановити й інші властивості параметрів, як-от валідаційний регулярний вираз (див. параметр `id`): - -```php -use Nette\Routing\Route; - -$router->addRoute('/[/]', [ - 'presenter' => [ - Route::Value => 'Home', - ], - 'action' => [ - Route::Value => 'default', - ], - 'id' => [ - Route::Pattern => '\d+', - ], -]); -``` - -Важливо зазначити, що якщо параметри, визначені в масиві, не вказані в масці шляху, їхні значення не можна змінити, навіть за допомогою query-параметрів, зазначених після знака питання в URL. - - -Фільтри та переклади --------------------- - -Вихідні коди застосунку ми пишемо англійською, але якщо веб-сайт повинен мати українські URL, то просте маршрутування типу: - -```php -$router->addRoute('/', 'Home:default'); -``` - -буде генерувати англійські URL, як-от `/product/123` або `/cart`. Якщо ми хочемо, щоб презентери та дії в URL були представлені українськими словами (наприклад, `/produkt/123` або `/koshyk`), ми можемо використати перекладацький словник. Для його запису вже потрібен "багатослівніший" варіант другого параметра: - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterTable => [ - // рядок в URL => презентер - 'produkt' => 'Product', - 'koshyk' => 'Cart', - 'katalog' => 'Catalog', - ], - ], - 'action' => [ - Route::Value => 'default', - Route::FilterTable => [ - 'spysok' => 'list', - ], - ], -]); -``` - -Кілька ключів перекладацького словника можуть вести на той самий презентер. Тим самим для нього створюються різні псевдоніми. За канонічний варіант (тобто той, який буде у згенерованому URL) вважається останній ключ. - -Перекладацьку таблицю можна таким чином використовувати для будь-якого параметра. При цьому, якщо переклад не існує, береться початкове значення. Цю поведінку можна змінити, доповнивши `Route::FilterStrict => true`, і маршрут тоді відхилить URL, якщо значення немає в словнику. - -Крім перекладацького словника у вигляді масиву, можна застосувати й власні функції перекладу. - -```php -use Nette\Routing\Route; - -$router->addRoute('//', [ - 'presenter' => [ - Route::Value => 'Home', - Route::FilterIn => function (string $s): string { /* ... */ }, - Route::FilterOut => function (string $s): string { /* ... */ }, - ], - 'action' => 'default', - 'id' => null, -]); -``` - -Функція `Route::FilterIn` перетворює параметр в URL на рядок, який потім передається до презентера, функція `FilterOut` забезпечує перетворення у зворотному напрямку. - -Параметри `presenter`, `action` та `module` вже мають передвизначені фільтри, які перетворюють між стилем PascalCase або camelCase та kebab-case, що використовується в URL. Значення параметрів за замовчуванням записується вже в трансформованому вигляді, тому, наприклад, у випадку презентера пишемо ``, а не ``. - - -Загальні фільтри ----------------- - -Крім фільтрів, призначених для конкретних параметрів, ми можемо визначити також загальні фільтри, які отримають асоціативний масив усіх параметрів, які можуть будь-яким чином модифікувати, а потім їх повернути. Загальні фільтри визначаємо під ключем `null`. - -```php -use Nette\Routing\Route; - -$router->addRoute('/', [ - 'presenter' => 'Home', - 'action' => 'default', - '' => [ - Route::FilterIn => function (array $params): array { /* ... */ }, - Route::FilterOut => function (array $params): array { /* ... */ }, - ], -]); -``` - -Загальні фільтри дають можливість змінити поведінку маршруту абсолютно будь-яким способом. Ми можемо їх використовувати, наприклад, для модифікації параметрів на основі інших параметрів. Наприклад, переклад `` та `` на основі поточного значення параметра ``. - -Якщо параметр має визначений власний фільтр і одночасно існує загальний фільтр, виконується власний `FilterIn` перед загальним і, навпаки, загальний `FilterOut` перед власним. Тобто всередині загального фільтра значення параметрів `presenter` або `action` записані в стилі PascalCase або camelCase. - - -Односторонні OneWay -------------------- - -Односторонні маршрути використовуються для збереження функціональності старих URL, які застосунок вже не генерує, але все ще приймає. Позначимо їх прапорцем `OneWay`: - -```php -// старий URL /product-info?id=123 -$router->addRoute('product-info', 'Product:detail', $router::ONE_WAY); -// новий URL /product/123 -$router->addRoute('product/', 'Product:detail'); -``` - -При доступі до старого URL презентер автоматично перенаправляє на новий URL, тому ці сторінки пошукові системи не проіндексують двічі (див. [#SEO та канонізація]). - - -Динамічна маршрутизація з callback-функціями --------------------------------------------- - -Динамічна маршрутизація з callback-функціями дозволяє вам призначати маршрутам безпосередньо функції (callback-функції), які виконуються, коли відвідується відповідний шлях. Ця гнучка функціональність дозволяє швидко та ефективно створювати різні кінцеві точки (endpoints) для вашого застосунку: - -```php -$router->addRoute('test', function () { - echo 'ви на адресі /test'; -}); -``` - -Ви також можете визначити в масці параметри, які автоматично передадуться до вашого callback: - -```php -$router->addRoute('', function (string $lang) { - echo match ($lang) { - 'cs' => 'Ласкаво просимо на українську версію нашого сайту!', - 'en' => 'Welcome to the English version of our website!', - }; -}); -``` - - -Модулі ------- - -Якщо у нас є кілька маршрутів, які належать до спільного [модуля |directory-structure#Presenter и та шаблони], використаємо `withModule()`: - -```php -$router = new RouteList; -$router->withModule('Forum') // наступні маршрути є частиною модуля Forum - ->addRoute('rss', 'Feed:rss') // презентер буде Forum:Feed - ->addRoute('/') - - ->withModule('Admin') // наступні маршрути є частиною модуля Forum:Admin - ->addRoute('sign:in', 'Sign:in'); -``` - -Альтернативою є використання параметра `module`: - -```php -// URL manage/dashboard/default мапується на презентер Admin:Dashboard -$router->addRoute('manage//', [ - 'module' => 'Admin', -]); -``` - - -Субдомени ---------- - -Колекції маршрутів ми можемо розділяти за субдоменами: - -```php -$router = new RouteList; -$router->withDomain('example.com') - ->addRoute('rss', 'Feed:rss') - ->addRoute('/'); -``` - -У назві домену можна використовувати й [#заступні знаки]: - -```php -$router = new RouteList; -$router->withDomain('example.%tld%') - // ... -``` - - -Префікс шляху -------------- - -Колекції маршрутів ми можемо розділяти за шляхом в URL: - -```php -$router = new RouteList; -$router->withPath('eshop') - ->addRoute('rss', 'Feed:rss') // ловить URL /eshop/rss - ->addRoute('/'); // ловить URL /eshop// -``` - - -Комбінації ----------- - -Вищезгаданий поділ можна взаємно комбінувати: - -```php -$router = (new RouteList) - ->withDomain('admin.example.com') - ->withModule('Admin') - ->addRoute(/* ... */) - ->addRoute(/* ... */) - ->end() - ->withModule('Images') - ->addRoute(/* ... */) - ->end() - ->end() - ->withDomain('example.com') - ->withPath('export') - ->addRoute(/* ... */) - // ... -``` - - -Query-параметри ---------------- - -Маски можуть також містити query-параметри (параметри після знака питання в URL). Для них не можна визначити валідаційний вираз, але можна змінити назву, під якою вони передаються до презентера: - -```php -// query-параметр 'cat' ми хочемо в застосунку використовувати під назвою 'categoryId' -$router->addRoute('product ? id= & cat=', /* ... */); -``` - - -Foo-параметри -------------- - -Тепер ми заглиблюємося. Foo-параметри — це, по суті, неіменовані параметри, які дозволяють зіставляти регулярний вираз. Прикладом є маршрут, що приймає `/index`, `/index.html`, `/index.htm` та `/index.php`: - -```php -$router->addRoute('index', /* ... */); -``` - -Можна також явно визначити рядок, який буде використаний при генерації URL. Рядок повинен бути розміщений безпосередньо за знаком питання. Наступний маршрут схожий на попередній, але генерує `/index.html` замість `/index`, оскільки рядок `.html` встановлено як значення для генерації: - -```php -$router->addRoute('index', /* ... */); -``` - - -Включення в застосунок -====================== - -Щоб створений маршрутизатор підключити до застосунку, ми повинні про нього повідомити DI-контейнер. Найпростіший шлях — підготувати фабрику, яка створить об'єкт маршрутизатора, і повідомити в конфігурації контейнера, що її потрібно використовувати. Припустимо, що для цієї мети ми напишемо метод `App\Core\RouterFactory::createRouter()`: - -```php -namespace App\Core; - -use Nette\Application\Routers\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute(/* ... */); - return $router; - } -} -``` - -До [конфігурації |dependency-injection:services] потім запишемо: - -```neon -services: - - App\Core\RouterFactory::createRouter -``` - -Будь-які залежності, наприклад, від бази даних тощо, передаються фабричному методу як його параметри за допомогою [autowiring|dependency-injection:autowiring]: - -```php -public static function createRouter(Nette\Database\Connection $db): RouteList -{ - // ... -} -``` - - -SimpleRouter -============ - -Набагато простішим маршрутизатором, ніж колекція маршрутів, є [SimpleRouter |api:Nette\Application\Routers\SimpleRouter]. Ми використовуємо його тоді, коли не маємо особливих вимог до форми URL, якщо недоступний `mod_rewrite` (або його альтернативи) або якщо поки що не хочемо займатися гарними URL. - -Генерує адреси приблизно в такому вигляді: - -``` -http://example.com/?presenter=Product&action=detail&id=123 -``` - -Параметром конструктора SimpleRouter є стандартний презентер та дія, на який слід спрямовувати, якщо відкрити сторінку без параметрів, наприклад, `http://example.com/`. - -```php -// стандартним презентером буде 'Home' та дія 'default' -$router = new Nette\Application\Routers\SimpleRouter('Home:default'); -``` - -Рекомендуємо SimpleRouter безпосередньо визначати в [конфігурації |dependency-injection:services]: - -```neon -services: - - Nette\Application\Routers\SimpleRouter('Home:default') -``` - - -SEO та канонізація -================== - -Фреймворк сприяє SEO (оптимізації знаходження в Інтернеті), запобігаючи дублюванню контенту на різних URL. Якщо до певної цілі веде кілька адрес, наприклад, `/index` та `/index.html`, фреймворк першу з них визначає як первинну (канонічну) і решту на неї перенаправляє за допомогою HTTP-коду 301. Завдяки цьому пошукові системи не індексують ваші сторінки двічі і не розмивають їхній page rank. - -Цей процес називається канонізацією. Канонічним URL є той, який генерує маршрутизатор, тобто перший відповідний маршрут у колекції без прапорця OneWay. Тому в колекції вказуємо **первинні маршрути першими**. - -Канонізацію виконує презентер, більше в розділі [канонізація |presenters#Канонізація]. - - -HTTPS -===== - -Щоб мати можливість використовувати протокол HTTPS, необхідно його увімкнути на хостингу та правильно налаштувати сервер. - -Перенаправлення всього сайту на HTTPS необхідно налаштувати на рівні сервера, наприклад, за допомогою файлу .htaccess у кореневому каталозі нашого застосунку, і це з HTTP-кодом 301. Налаштування може відрізнятися залежно від хостингу і виглядає приблизно так: - -``` - - RewriteEngine On - ... - RewriteCond %{HTTPS} off - RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] - ... - -``` - -Маршрутизатор генерує URL з тим самим протоколом, з яким була завантажена сторінка, тому нічого більше налаштовувати не потрібно. - -Але якщо винятково потрібно, щоб різні маршрути працювали під різними протоколами, вкажемо його в масці маршруту: - -```php -// Буде генерувати адресу з HTTP -$router->addRoute('http://%host%//', /* ... */); - -// Буде генерувати адресу з HTTPS -$router->addRoute('https://%host%//', /* ... */); -``` - - -Налагодження маршрутизатора -=========================== - -Панель маршрутизації, що відображається в [Tracy Bar |tracy:], є корисним помічником, який показує список маршрутів, а також параметрів, які маршрутизатор отримав з URL. - -Зелена смуга із символом ✓ представляє маршрут, який обробив поточний URL, синім кольором та символом ≈ позначені маршрути, які також обробили б URL, якби їх не випередив зелений. Далі бачимо поточний презентер та дію. - -[* routing-debugger.webp *] - -Водночас, якщо відбувається неочікуване перенаправлення через [канонізацію |#SEO та канонізація], корисно подивитися на панель у рядку *redirect*, де ви дізнаєтеся, як маршрутизатор спочатку зрозумів URL і чому перенаправив. - -.[note] -При налагодженні маршрутизатора рекомендуємо відкрити в браузері Developer Tools (Ctrl+Shift+I або Cmd+Option+I) і в панелі Network вимкнути кеш, щоб у ньому не зберігалися перенаправлення. - - -Продуктивність -============== - -Кількість маршрутів впливає на швидкість маршрутизатора. Їхня кількість точно не повинна перевищувати кілька десятків. Якщо ваш сайт має занадто складну структуру URL, ви можете написати на замовлення [#власний маршрутизатор]. - -Якщо маршрутизатор не має жодних залежностей, наприклад, від бази даних, і його фабрика не приймає жодних аргументів, ми можемо його зібрану форму серіалізувати безпосередньо в DI-контейнер і тим самим трохи прискорити застосунок. - -```neon -routing: - cache: true -``` - - -Власний маршрутизатор -===================== - -Наступні рядки призначені для дуже досвідчених користувачів. Ви можете створити власний маршрутизатор і цілком природно включити його до колекції маршрутів. Маршрутизатор є реалізацією інтерфейсу [api:Nette\Routing\Router] з двома методами: - -```php -use Nette\Http\IRequest as HttpRequest; -use Nette\Http\UrlScript; - -class MyRouter implements Nette\Routing\Router -{ - public function match(HttpRequest $httpRequest): ?array - { - // ... - } - - public function constructUrl(array $params, UrlScript $refUrl): ?string - { - // ... - } -} -``` - -Метод `match` обробляє поточний запит [$httpRequest |http:request], з якого можна отримати не тільки URL, але й заголовки тощо, до масиву, що містить назву презентера та його параметри. Якщо запит обробити не може, повертає null. При обробці запиту ми повинні повернути щонайменше презентер та дію. Назва презентера є повною і містить також можливі модулі: - -```php -[ - 'presenter' => 'Front:Home', - 'action' => 'default', -] -``` - -Метод `constructUrl` навпаки складає з масиву параметрів кінцевий абсолютний URL. Для цього він може використовувати інформацію з параметра [`$refUrl`|api:Nette\Http\UrlScript], що є поточним URL. - -До колекції маршрутів його додасте за допомогою `add()`: - -```php -$router = new Nette\Application\Routers\RouteList; -$router->add($myRouter); -$router->addRoute(/* ... */); -// ... -``` - - -Самостійне використання -======================= - -Самостійним використанням ми маємо на увазі використання можливостей маршрутизатора в застосунку, який не використовує Nette Application та презентери. Для нього діє майже все, що ми показали в цьому розділі, з такими відмінностями: - -- для колекцій маршрутів використовуємо клас [api:Nette\Routing\RouteList] -- як простий маршрутизатор клас [api:Nette\Routing\SimpleRouter] -- оскільки не існує пари `Presenter:action`, використовуємо [#розширений запис] - -Отже, знову створимо метод, який нам складе маршрутизатор, наприклад: - -```php -namespace App\Core; - -use Nette\Routing\RouteList; - -class RouterFactory -{ - public static function createRouter(): RouteList - { - $router = new RouteList; - $router->addRoute('rss.xml', [ - 'controller' => 'RssFeedController', - ]); - $router->addRoute('article/', [ - 'controller' => 'ArticleController', - ]); - // ... - return $router; - } -} -``` - -Якщо ви використовуєте DI-контейнер, що ми рекомендуємо, знову додамо метод до конфігурації, а потім маршрутизатор разом з HTTP-запитом отримаємо з контейнера: - -```php -$router = $container->getByType(Nette\Routing\Router::class); -$httpRequest = $container->getByType(Nette\Http\IRequest::class); -``` - -Або об'єкти безпосередньо створимо: - -```php -$router = App\Core\RouterFactory::createRouter(); -$httpRequest = (new Nette\Http\RequestFactory)->fromGlobals(); -``` - -Тепер залишається лише запустити маршрутизатор до роботи: - -```php -$params = $router->match($httpRequest); -if ($params === null) { - // не знайдено відповідного маршруту, надсилаємо помилку 404 - exit; -} - -// обробляємо отримані параметри -$controller = $params['controller']; -// ... -``` - -І навпаки, використаємо маршрутизатор для складання посилання: - -```php -$params = ['controller' => 'ArticleController', 'id' => 123]; -$url = $router->constructUrl($params, $httpRequest->getUrl()); -``` - - -{{composer: nette/router}} diff --git a/application/uk/templates.texy b/application/uk/templates.texy deleted file mode 100644 index ea1f4480c3..0000000000 --- a/application/uk/templates.texy +++ /dev/null @@ -1,323 +0,0 @@ -Шаблони -******* - -.[perex] -Nette використовує систему шаблонів [Latte |latte:]. По-перше, тому що це найбезпечніша система шаблонів для PHP, а по-друге, вона також є найінтуїтивнішою. Вам не потрібно вивчати багато нового, достатньо знань PHP та кількох тегів. - -Зазвичай сторінка складається з шаблону layout + шаблону для конкретної дії. Ось як може виглядати шаблон layout, зверніть увагу на блоки `{block}` та тег `{include}`: - -```latte - - - - {block title}My App{/block} - - -
    ...
    - {include content} -
    ...
    - - -``` - -А це буде шаблон дії: - -```latte -{block title}Homepage{/block} - -{block content} -

    Homepage

    -... -{/block} -``` - -Він визначає блок `content`, який буде вставлено замість `{include content}` у layout, а також перевизначає блок `title`, який перезапише `{block title}` у layout. Спробуйте уявити результат. - - -Пошук шаблонів --------------- - -Вам не потрібно вказувати в presenter'ах, який шаблон потрібно відобразити, фреймворк сам визначить шлях і заощадить вам час на написання коду. - -Якщо ви використовуєте структуру каталогів, де кожен presenter має власний каталог, просто розмістіть шаблон у цьому каталозі під назвою дії (або view), тобто для дії `default` використовуйте шаблон `default.latte`: - -/--pre -app/ -└── Presentation/ - └── Home/ - ├── HomePresenter.php - └── default.latte -\-- - -Якщо ви використовуєте структуру, де presenter'и знаходяться разом в одному каталозі, а шаблони — у папці `templates`, збережіть його або у файлі `..latte`, або `/.latte`: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── Home.default.latte ← 1-й варіант - └── Home/ - └── default.latte ← 2-й варіант -\-- - -Каталог `templates` також може знаходитись на рівень вище, тобто на тому ж рівні, що й каталог із класами presenter'ів. - -Якщо шаблон не знайдено, presenter відповість [помилкою 404 - сторінку не знайдено |presenters#Помилка 404 тощо]. - -View можна змінити за допомогою `$this->setView('іншийView')`. Також можна безпосередньо вказати файл шаблону за допомогою `$this->template->setFile('/path/to/template.latte')`. - -.[note] -Файли, де шукаються шаблони, можна змінити, перевизначивши метод [formatTemplateFiles() |api:Nette\Application\UI\Presenter::formatTemplateFiles()], який повертає масив можливих імен файлів. - - -Пошук шаблону layout --------------------- - -Nette також автоматично шукає файл layout. - -Якщо ви використовуєте структуру каталогів, де кожен presenter має власний каталог, розмістіть layout або в папці з presenter'ом, якщо він специфічний лише для нього, або на рівень вище, якщо він спільний для кількох presenter'ів: - -/--pre -app/ -└── Presentation/ - ├── @layout.latte ← спільний layout - └── Home/ - ├── @layout.latte ← тільки для presenter'а Home - ├── HomePresenter.php - └── default.latte -\-- - -Якщо ви використовуєте структуру, де presenter'и знаходяться разом в одному каталозі, а шаблони — у папці `templates`, layout очікуватиметься в таких місцях: - -/--pre -app/ -└── Presenters/ - ├── HomePresenter.php - └── templates/ - ├── @layout.latte ← спільний layout - ├── Home.@layout.latte ← тільки для Home, 1-й варіант - └── Home/ - └── @layout.latte ← тільки для Home, 2-й варіант -\-- - -Якщо presenter знаходиться в модулі, пошук буде здійснюватися також на вищих рівнях каталогів, відповідно до вкладеності модуля. - -Назву layout можна змінити за допомогою `$this->setLayout('layoutAdmin')`, і тоді він очікуватиметься у файлі `@layoutAdmin.latte`. Також можна безпосередньо вказати файл шаблону layout за допомогою `$this->setLayout('/path/to/template.latte')`. - -За допомогою `$this->setLayout(false)` або тегу `{layout none}` всередині шаблону пошук layout вимикається. - -.[note] -Файли, де шукаються шаблони layout, можна змінити, перевизначивши метод [formatLayoutTemplateFiles() |api:Nette\Application\UI\Presenter::formatLayoutTemplateFiles()], який повертає масив можливих імен файлів. - - -Змінні в шаблоні ----------------- - -Змінні передаються в шаблон шляхом запису їх у `$this->template`, після чого вони стають доступними в шаблоні як локальні змінні: - -```php -$this->template->article = $this->articles->getById($id); -``` - -Таким чином, ми можемо легко передавати будь-які змінні в шаблони. Однак при розробці надійних додатків корисніше обмежити себе. Наприклад, явно визначивши перелік змінних, які очікує шаблон, та їхні типи. Завдяки цьому PHP зможе перевіряти типи, IDE правильно підказуватиме, а статичний аналіз виявлятиме помилки. - -А як визначити такий перелік? Просто у вигляді класу та його властивостей. Назвемо його подібно до presenter'а, але з `Template` на кінці: - -```php -/** - * @property-read ArticleTemplate $template - */ -class ArticlePresenter extends Nette\Application\UI\Presenter -{ -} - -class ArticleTemplate extends Nette\Bridges\ApplicationLatte\Template -{ - public Model\Article $article; - public Nette\Security\User $user; - - // та інші змінні -} -``` - -Об'єкт `$this->template` у presenter'і тепер буде екземпляром класу `ArticleTemplate`. Таким чином, PHP перевірятиме оголошені типи під час запису. А починаючи з версії PHP 8.2, він також попереджатиме про запис у неіснуючу змінну; у попередніх версіях цього можна досягти за допомогою трейту [Nette\SmartObject |utils:smartobject]. - -Анотація `@property-read` призначена для IDE та статичного аналізу, завдяки їй працюватиме автодоповнення, див. "PhpStorm and code completion for $this⁠-⁠>⁠template":https://blog.nette.org/en/phpstorm-and-code-completion-for-this-template. - -[* phpstorm-completion.webp *] - -Розкішшю автодоповнення можна насолоджуватися і в шаблонах, достатньо встановити плагін для Latte в PhpStorm та вказати на початку шаблону назву класу, більше в статті "Latte: як працювати з системою типів":https://blog.nette.org/uk/latte-how-to-use-type-system: - -```latte -{templateType App\Presentation\Article\ArticleTemplate} -... -``` - -Так само працюють і шаблони в компонентах, достатньо дотримуватися конвенції іменування і для компонента, наприклад, `FifteenControl` створити клас шаблону `FifteenTemplate`. - -Якщо вам потрібно створити `$template` як екземпляр іншого класу, використовуйте метод `createTemplate()`: - -```php -public function renderDefault(): void -{ - $template = $this->createTemplate(SpecialTemplate::class); - $template->foo = 123; - // ... - $this->sendTemplate($template); -} -``` - - -Змінні за замовчуванням ------------------------ - -Presenter'и та компоненти автоматично передають у шаблони кілька корисних змінних: - -- `$basePath` — це абсолютний URL-шлях до кореневого каталогу (наприклад, `/eshop`) -- `$baseUrl` — це абсолютний URL до кореневого каталогу (наприклад, `http://localhost/eshop`) -- `$user` — це об'єкт, [що представляє користувача |security:authentication] -- `$presenter` — це поточний presenter -- `$control` — це поточний компонент або presenter -- `$flashes` — масив [повідомлень |presenters#Flash-повідомлення], надісланих функцією `flashMessage()` - -Якщо ви використовуєте власний клас шаблону, ці змінні будуть передані, якщо ви створите для них властивості. - - -Створення посилань ------------------- - -У шаблоні посилання на інші presenter'и та дії створюються таким чином: - -```latte -деталі продукту -``` - -Атрибут `n:href` дуже зручний для HTML-тегів ``. Якщо ми хочемо вивести посилання в іншому місці, наприклад, у тексті, використовуємо `{link}`: - -```latte -Адреса: {link Home:default} -``` - -Більше інформації ви знайдете в розділі [Створення URL-посилань|creating-links]. - - -Власні фільтри, теги тощо. --------------------------- - -Систему шаблонів Latte можна розширити власними фільтрами, функціями, тегами тощо. Це можна зробити безпосередньо в методі `render` або `beforeRender()`: - -```php -public function beforeRender(): void -{ - // додавання фільтра - $this->template->addFilter('foo', /* ... */); - - // або конфігуруємо безпосередньо об'єкт Latte\Engine - $latte = $this->template->getLatte(); - $latte->addFilterLoader(/* ... */); -} -``` - -Latte версії 3 пропонує більш просунутий спосіб, а саме створення [extension |latte:extending-latte#Latte Extension] для кожного веб-проекту. Приклад такого класу: - -```php -namespace App\Presentation\Accessory; - -final class LatteExtension extends Latte\Extension -{ - public function __construct( - private App\Model\Facade $facade, - private Nette\Security\User $user, - // ... - ) { - } - - public function getFilters(): array - { - return [ - 'timeAgoInWords' => $this->filterTimeAgoInWords(...), - 'money' => $this->filterMoney(...), - // ... - ]; - } - - public function getFunctions(): array - { - return [ - 'canEditArticle' => - fn($article) => $this->facade->canEditArticle($article, $this->user->getId()), - // ... - ]; - } - - // ... -} -``` - -Зареєструємо його за допомогою [конфігурації |configuration#Шаблони Latte]: - -```neon -latte: - extensions: - - App\Presentation\Accessory\LatteExtension -``` - - -Переклад --------- - -Якщо ви програмуєте багатомовний додаток, вам, швидше за все, знадобиться виводити деякі тексти в шаблоні різними мовами. Для цього Nette Framework визначає інтерфейс для перекладу [api:Nette\Localization\Translator], який має єдиний метод `translate()`. Він приймає повідомлення `$message`, яке зазвичай є рядком, та будь-які інші параметри. Завдання полягає в тому, щоб повернути перекладений рядок. У Nette немає реалізації за замовчуванням, ви можете вибрати відповідно до своїх потреб з кількох готових рішень, які можна знайти на [Componette |https://componette.org/search/localization]. У їхній документації ви дізнаєтеся, як налаштувати перекладач. - -Шаблонам можна встановити перекладач, який ми [передамо |dependency-injection:passing-dependencies], за допомогою методу `setTranslator()`: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator); -} -``` - -Перекладач також можна налаштувати за допомогою [конфігурації |configuration#Шаблони Latte]: - -```neon -latte: - extensions: - - Latte\Essential\TranslatorExtension(@Nette\Localization\Translator) -``` - -Після цього перекладач можна використовувати, наприклад, як фільтр `|translate`, включаючи додаткові параметри, які передаються методу `translate()` (див. `foo, bar`): - -```latte -{='Кошик'|translate} -{$item|translate} -{$item|translate, foo, bar} -``` - -Або як тег з підкресленням: - -```latte -{_'Кошик'} -{_$item} -{_$item, foo, bar} -``` - -Для перекладу частини шаблону існує парний тег `{translate}` (з Latte 2.11, раніше використовувався тег `{_}`): - -```latte -{translate}Замовлення{/translate} -{translate foo, bar}Замовлення{/translate} -``` - -Перекладач зазвичай викликається під час виконання при рендерингу шаблону. Однак Latte версії 3 може перекладати всі статичні тексти вже під час компіляції шаблону. Це економить продуктивність, оскільки кожен рядок перекладається лише один раз, а результат перекладу записується в скомпільовану форму. У каталозі кешу таким чином створюється кілька скомпільованих версій шаблону, по одній для кожної мови. Для цього достатньо лише вказати мову як другий параметр: - -```php -protected function beforeRender(): void -{ - // ... - $this->template->setTranslator($translator, $lang); -} -``` - -Статичним текстом мається на увазі, наприклад, `{_'hello'}` або `{translate}hello{/translate}`. Нестатичні тексти, такі як `{_$foo}`, продовжуватимуть перекладатися під час виконання. diff --git a/assets/bg/@home.texy b/assets/bg/@home.texy deleted file mode 100644 index 287b4b6636..0000000000 --- a/assets/bg/@home.texy +++ /dev/null @@ -1,432 +0,0 @@ -Nette Assets -************ - -
    - -Омръзна ли ви ръчното управление на статични файлове във вашите уеб приложения? Забравете за хардкодиране на пътища, справяне с инвалидиране на кеша или притеснения относно версиирането на файлове. Nette Assets трансформира начина, по който работите с изображения, стилови таблици, скриптове и други статични ресурси. - -- **Интелигентно версииране** гарантира, че браузърите винаги зареждат най-новите файлове -- **Автоматично откриване** на типове файлове и размери -- **Безпроблемна Latte интеграция** с интуитивни тагове -- **Гъвкава архитектура**, поддържаща файлови системи, CDN и Vite -- **Лениво зареждане** за оптимална производителност - -
    - - -Защо Nette Assets? -================== - -Работата със статични файлове често означава повтарящ се, податлив на грешки код. Ръчно конструирате URL адреси, добавяте параметри за версии за кеш изчистване и обработвате различни типове файлове по различен начин. Това води до код като: - -```latte -Logo - -``` - -С Nette Assets цялата тази сложност изчезва: - -```latte -{* Всичко автоматизирано - URL, версииране, размери *} - - - -{* Или просто *} -{asset 'css/style.css'} -``` - -Това е! Библиотеката автоматично: -- Добавя параметри за версии въз основа на времето на последна модификация на файла -- Открива размерите на изображението и ги включва в HTML -- Генерира правилния HTML елемент за всеки тип файл -- Обработва както развойна, така и продукционна среда - - -Инсталация -========== - -Инсталирайте Nette Assets с помощта на [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Изисква PHP 8.1 или по-нова и работи перфектно с Nette Framework, но може да се използва и самостоятелно. - - -Първи стъпки -============ - -Nette Assets работи веднага без никаква конфигурация. Поставете статичните си файлове в директорията `www/assets/` и започнете да ги използвате: - -```latte -{* Показва изображение с автоматични размери *} -{asset 'logo.png'} - -{* Включва стилова таблица с версииране *} -{asset 'style.css'} - -{* Зарежда JavaScript модул *} -{asset 'app.js'} -``` - -За повече контрол върху генерирания HTML, използвайте атрибута `n:asset` или функцията `asset()`. - - -Как работи -========== - -Nette Assets е изграден около три основни концепции, които го правят мощен, но лесен за използване: - - -Активи - Вашите файлове стават интелигентни -------------------------------------------- - -**Актив** представлява всеки статичен файл във вашето приложение. Всеки файл става обект с полезни свойства само за четене: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Различните типове файлове предоставят различни свойства: -- **Изображения**: ширина, височина, алтернативен текст, лениво зареждане -- **Скриптове**: тип модул, хешове за цялост, crossorigin -- **Стилови таблици**: медийни заявки, цялост -- **Аудио/Видео**: продължителност, размери -- **Шрифтове**: правилно предварително зареждане с CORS - -Библиотеката автоматично открива типовете файлове и създава подходящия клас актив. - - -Мапъри - Откъде идват файловете -------------------------------- - -**Мапърът** знае как да намира файлове и да създава URL адреси за тях. Можете да имате множество мапъри за различни цели - локални файлове, CDN, облачно хранилище или инструменти за изграждане (всеки от тях има име). Вграденият `FilesystemMapper` обработва локални файлове, докато `ViteMapper` се интегрира с модерни инструменти за изграждане. - -Мапърите се дефинират в [Конфигурация |Configuration]. - - -Регистър - Вашият основен интерфейс ------------------------------------ - -**Регистърът** управлява всички мапъри и предоставя основния API: - -```php -// Инжектирайте регистъра във вашата услуга -public function __construct( - private Nette\Assets\Registry $assets -) {} - -// Вземете активи от различни мапъри -$logo = $this->assets->getAsset('images:logo.png'); // мапър 'image' -$app = $this->assets->getAsset('app:main.js'); // мапър 'app' -$style = $this->assets->getAsset('style.css'); // използва мапъра по подразбиране -``` - -Регистърът автоматично избира правилния мапър и кешира резултатите за производителност. - - -Работа с активи в PHP -===================== - -Регистърът предоставя два метода за извличане на активи: - -```php -// Хвърля Nette\Assets\AssetNotFoundException, ако файлът не съществува -$logo = $assets->getAsset('logo.png'); - -// Връща null, ако файлът не съществува -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Указване на мапъри ------------------- - -Можете изрично да изберете кой мапър да използвате: - -```php -// Използвайте мапъра по подразбиране -$file = $assets->getAsset('document.pdf'); - -// Използвайте конкретен мапър с префикс -$image = $assets->getAsset('images:photo.jpg'); - -// Използвайте конкретен мапър със синтаксис на масив -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Свойства и типове активи ------------------------- - -Всеки тип актив предоставя съответните свойства само за четене: - -```php -// Свойства на изображение -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Свойства на скрипт -$script = $assets->getAsset('app.js'); -echo $script->type; // 'module' или null - -// Свойства на аудио -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // продължителност в секунди - -// Всички активи могат да бъдат преобразувани в низ (връща URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Свойства като размери или продължителност се зареждат лениво само при достъп, поддържайки библиотеката бърза. - - -Използване на активи в Latte шаблони -==================================== - -Nette Assets предоставя интуитивна [Latte|latte:] интеграция с тагове и функции. - - -`{asset}` ---------- - -Тагът `{asset}` рендира пълни HTML елементи: - -```latte -{* Рендира: *} -{asset 'hero.jpg'} - -{* Рендира: *} -{asset 'app.js'} - -{* Рендира: *} -{asset 'style.css'} -``` - -Тагът автоматично: -- Открива типа актив и генерира подходящ HTML -- Включва версииране за кеш изчистване -- Добавя размери за изображения -- Задава правилни атрибути (тип, медия и т.н.) - -Когато се използва вътре в HTML атрибути, той извежда само URL адреса: - -```latte -
    - -``` - - -`n:asset` ---------- - -За пълен контрол върху HTML атрибутите: - -```latte -{* Атрибутът n:asset попълва src, размери и т.н. *} -Product - -{* Работи с всеки подходящ елемент *} - - - -``` - -Използвайте променливи и мапъри: - -```latte -{* Променливите работят естествено *} - - -{* Укажете мапър с къдрави скоби *} - - -{* Укажете мапър с нотация на масив *} - -``` - - -`asset()` ---------- - -За максимална гъвкавост, използвайте функцията `asset()`: - -```latte -{var $logo = asset('logo.png')} -width} height={$logo->height}> - -{* Или директно *} -Logo -``` - - -Опционални активи ------------------ - -Обработвайте липсващи активи елегантно с `{asset?}`, `n:asset?` и `tryAsset()`: - -```latte -{* Опционален таг - не рендира нищо, ако активът липсва *} -{asset? 'optional-banner.jpg'} - -{* Опционален атрибут - пропуска, ако активът липсва *} -Avatar - -{* С резервен вариант *} -{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')} -Avatar -``` - - -`{preload}` ------------ - -Подобрете производителността на зареждане на страницата: - -```latte -{* Във вашата секция *} -{preload 'critical.css'} -{preload 'important-font.woff2'} -{preload 'hero-image.jpg'} -``` - -Генерира подходящи preload връзки: - -```latte - - - -``` - - -Разширени функции -================= - - -Автоматично откриване на разширения ------------------------------------ - -Автоматично обработвайте множество формати: - -```neon -assets: - mapping: - images: - path: img - extension: [webp, jpg, png] # Опитайте по ред -``` - -Сега можете да изисквате без разширение: - -```latte -{* Намира logo.webp, logo.jpg или logo.png автоматично *} -{asset 'images:logo'} -``` - -Перфектно за прогресивно подобрение с модерни формати. - - -Интелигентно версииране ------------------------ - -Файловете автоматично се версиират въз основа на времето на модификация: - -```latte -{asset 'style.css'} -{* Изход: *} -``` - -Когато актуализирате файла, времевият печат се променя, принуждавайки опресняване на кеша на браузъра. - -Контролирайте версиирането за всеки актив: - -```php -// Деактивирайте версиирането за конкретен актив -$asset = $assets->getAsset('style.css', ['version' => false]); - -// В Latte -{asset 'style.css', version: false} -``` - - -Шрифтови активи ---------------- - -Шрифтовете получават специално отношение с правилен CORS: - -```latte -{* Правилно предварително зареждане с crossorigin *} -{preload 'fonts:OpenSans-Regular.woff2'} - -{* Използвайте в CSS *} - -``` - - -Персонализирани мапъри -====================== - -Създайте персонализирани мапъри за специални нужди като облачно хранилище или динамично генериране: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Регистрирайте в конфигурацията: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Използвайте като всеки друг мапър: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Методът `Helpers::createAssetFromUrl()` автоматично създава правилния тип актив въз основа на разширението на файла. - - -Допълнително четене -=================== - -- [Нетни активи: Най-накрая унифициран API за всичко - от изображения до Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/bg/@left-menu.texy b/assets/bg/@left-menu.texy deleted file mode 100644 index 5b04a76bdb..0000000000 --- a/assets/bg/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Преглед |@home] -- [Vite |vite] -- [Конфигурация |Configuration] diff --git a/assets/bg/@meta.texy b/assets/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/assets/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/assets/bg/configuration.texy b/assets/bg/configuration.texy deleted file mode 100644 index 666a7ec7a1..0000000000 --- a/assets/bg/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Конфигурация на активи -********************** - -.[perex] -Преглед на опциите за конфигурация за Nette Assets. - - -```neon -assets: - # базов път за разрешаване на относителни пътища на мапъри - basePath: ... # (string) по подразбиране е %wwwDir% - - # базов URL за разрешаване на относителни URL адреси на мапъри - baseUrl: ... # (string) по подразбиране е %baseUrl% - - # активиране на версииране на активи глобално? - versioning: ... # (bool) по подразбиране е true - - # дефинира мапъри на активи - mapping: ... # (array) по подразбиране е път 'assets' -``` - -`basePath` задава директорията на файловата система по подразбиране за разрешаване на относителни пътища в мапъри. По подразбиране използва уеб директорията (`%wwwDir%`). - -`baseUrl` задава URL префикса по подразбиране за разрешаване на относителни URL адреси в мапъри. По подразбиране използва основния URL адрес (`%baseUrl%`). - -Опцията `versioning` глобално контролира дали параметрите за версии се добавят към URL адресите на активи за изчистване на кеша. Отделните мапъри могат да презапишат тази настройка. - - -Мапъри ------- - -Мапърите могат да бъдат конфигурирани по три начина: проста нотация на низ, подробна нотация на масив или като препратка към услуга. - -Най-простият начин за дефиниране на мапър: - -```neon -assets: - mapping: - default: assets # Създава мапър на файлова система за %wwwDir%/assets/ - images: img # Създава мапър на файлова система за %wwwDir%/img/ - scripts: js # Създава мапър на файлова система за %wwwDir%/js/ -``` - -Всеки мапър създава `FilesystemMapper`, който: -- Търси файлове в `%wwwDir%/` -- Генерира URL адреси като `%baseUrl%/` -- Наследява глобалната настройка за версииране - - -За повече контрол, използвайте подробната нотация: - -```neon -assets: - mapping: - images: - # директория, където се съхраняват файловете - path: ... # (string) опционално, по подразбиране е '' - - # URL префикс за генерирани връзки - url: ... # (string) опционално, по подразбиране е path - - # активиране на версииране за този мапър? - versioning: ... # (bool) опционално, наследява глобалната настройка - - # автоматично добавяне на разширение(я) при търсене на файлове - extension: ... # (string|array) опционално, по подразбиране е null -``` - -Разбиране как се разрешават стойностите на конфигурацията: - -Разрешаване на пътя: - - Относителните пътища се разрешават от `basePath` (или `%wwwDir%`, ако `basePath` не е зададен) - - Абсолютните пътища се използват такива, каквито са - -Разрешаване на URL: - - Относителните URL адреси се разрешават от `baseUrl` (или `%baseUrl%`, ако `baseUrl` не е зададен) - - Абсолютните URL адреси (със схема или `//`) се използват такива, каквито са - - Ако `url` не е указан, той използва стойността на `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Относителен път и URL - images: - path: img # Разрешено до: /var/www/project/www/img - url: images # Разрешено до: https://example.com/assets/images - - # Абсолютен път и URL - uploads: - path: /var/shared/uploads # Използва се както е: /var/shared/uploads - url: https://cdn.example.com # Използва се както е: https://cdn.example.com - - # Указан е само пътят - styles: - path: css # Път: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Персонализирани мапъри ----------------------- - -За персонализирани мапъри, препратете или дефинирайте услуга: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Vite мапърът изисква само да добавите `type: vite`. Това е пълен списък с опции за конфигурация: - -```neon -assets: - mapping: - default: - # тип мапър (задължителен за Vite) - type: vite # (string) задължителен, трябва да е 'vite' - - # директория за изход на Vite build - path: ... # (string) опционално, по подразбиране е '' - - # URL префикс за изградени активи - url: ... # (string) опционално, по подразбиране е path - - # местоположение на Vite manifest файл - manifest: ... # (string) опционално, по подразбиране е /.vite/manifest.json - - # конфигурация на Vite dev сървър - devServer: ... # (bool|string) опционално, по подразбиране е true - - # версииране за файлове в публична директория - versioning: ... # (bool) опционално, наследява глобалната настройка - - # автоматично разширение за файлове в публична директория - extension: ... # (string|array) опционално, по подразбиране е null -``` - -Опцията `devServer` контролира как се зареждат активи по време на разработка: - -- `true` (по подразбиране) - Автоматично открива Vite dev сървъра на текущия хост и порт. Ако dev сървърът работи **и вашето приложение е в режим на отстраняване на грешки**, активите се зареждат от него с поддръжка на гореща подмяна на модули. Ако dev сървърът не работи, активите се зареждат от изградените файлове в публичната директория. -- `false` - Напълно деактивира интеграцията на dev сървъра. Активите винаги се зареждат от изградените файлове. -- Персонализиран URL (напр. `https://localhost:5173`) - Ръчно указва URL адреса на dev сървъра, включително протокол и порт. Полезно, когато dev сървърът работи на различен хост или порт. - -Опциите `versioning` и `extension` се прилагат само за файлове в публичната директория на Vite, които не се обработват от Vite. - - -Ръчна конфигурация ------------------- - -Когато не използвате Nette DI, конфигурирайте мапърите ръчно: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Добавяне на мапър на файлова система -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Добавяне на Vite мапър -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/bg/vite.texy b/assets/bg/vite.texy deleted file mode 100644 index 45c188b3e3..0000000000 --- a/assets/bg/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Vite интеграция -*************** - -
    - -Модерните JavaScript приложения изискват сложни инструменти за изграждане. Nette Assets предоставя първокласна интеграция с [Vite |https://vitejs.dev/], инструментът за изграждане на фронтенд от следващо поколение. Получете светкавично бързо развитие с Hot Module Replacement (HMR) и оптимизирани продукционни компилации без никакви проблеми с конфигурацията. - -- **Нулева конфигурация** - автоматичен мост между Vite и PHP шаблони -- **Пълно управление на зависимостите** - един таг обработва всички активи -- **Hot Module Replacement** - незабавни JavaScript и CSS актуализации -- **Оптимизирани продукционни компилации** - разделяне на кода и tree shaking - -
    - - -Nette Assets се интегрира безпроблемно с Vite, така че получавате всички тези предимства, докато пишете шаблоните си както обикновено. - - -Настройка на Vite -================= - -Нека настроим Vite стъпка по стъпка. Не се притеснявайте, ако сте нов в инструментите за изграждане - ще обясним всичко! - - -Стъпка 1: Инсталирайте Vite ---------------------------- - -Първо, инсталирайте Vite и Nette плъгина във вашия проект: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Това инсталира Vite и специален плъгин, който помага на Vite да работи перфектно с Nette. - - -Стъпка 2: Структура на проекта ------------------------------- - -Стандартният подход е да поставите изходните файлове на активи в папка `assets/` в корена на проекта, а компилираните версии в `www/assets/`: - -/--pre -web-project/ -├── assets/ ← изходни файлове (SCSS, TypeScript, изходни изображения) -│ ├── public/ ← статични файлове (копират се както са) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← основна входна точка -│ └── style.css ← вашите стилове -└── www/ ← публична директория (документен корен) - ├── assets/ ← компилираните файлове ще отидат тук - └── index.php -\-- - -Папката `assets/` съдържа вашите изходни файлове - кода, който пишете. Vite ще обработи тези файлове и ще постави компилираните версии в `www/assets/`. - - -Стъпка 3: Конфигурирайте Vite ------------------------------ - -Създайте файл `vite.config.ts` в корена на проекта. Този файл казва на Vite къде да намери вашите изходни файлове и къде да постави компилираните. - -Плъгинът Nette Vite идва с интелигентни настройки по подразбиране, които опростяват конфигурацията. Той предполага, че вашите изходни фронтенд файлове са в директорията `assets/` (опция `root`) и компилираните файлове отиват в `www/assets/` (опция `outDir`). Трябва само да укажете [Входни точки |#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Ако искате да укажете друго име на директория за изграждане на вашите активи, ще трябва да промените няколко опции: - -```js -export default defineConfig({ - root: 'assets', // основна директория на изходни активи - - build: { - outDir: '../www/assets', // къде отиват компилираните файлове - }, - - // ... друга конфигурация ... -}); -``` - -.[note] -Пътят `outDir` се счита за относителен спрямо `root`, поради което има `../` в началото. - - -Стъпка 4: Конфигурирайте Nette ------------------------------- - -Кажете на Nette Assets за Vite във вашия `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # казва на Nette да използва ViteMapper - path: assets -``` - - -Стъпка 5: Добавете скриптове ----------------------------- - -Добавете тези скриптове към вашия `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Сега можете: -- `npm run dev` - стартирайте сървър за разработка с горещо презареждане -- `npm run build` - създайте оптимизирани продукционни файлове - - -Входни точки -============ - -**Входна точка** е основният файл, от който започва вашето приложение. От този файл импортирате други файлове (CSS, JavaScript модули, изображения), създавайки дърво на зависимостите. Vite следва тези импорти и пакетира всичко заедно. - -Примерна входна точка `assets/app.js`: - -```js -// Импортиране на стилове -import './style.css' - -// Импортиране на JavaScript модули -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Инициализиране на вашето приложение -netteForms.initOnLoad(); -naja.initialize(); -``` - -В шаблона можете да вмъкнете входна точка, както следва: - -```latte -{asset 'app.js'} -``` - -Nette Assets автоматично генерира всички необходими HTML тагове - JavaScript, CSS и всякакви други зависимости. - - -Множество входни точки ----------------------- - -По-големите приложения често се нуждаят от отделни входни точки: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // публични страници - 'admin.js', // административен панел - ], - }), - ], -}); -``` - -Използвайте ги в различни шаблони: - -```latte -{* В публични страници *} -{asset 'app.js'} - -{* В административен панел *} -{asset 'admin.js'} -``` - - -Важно: Изходни срещу компилирани файлове ----------------------------------------- - -Ключово е да се разбере, че в продукция можете да зареждате само: - -1. **Входни точки**, дефинирани в `entry` -2. **Файлове от директорията `assets/public/`** - -Не можете да зареждате с `{asset}` произволни файлове от `assets/` - само активи, реферирани от JavaScript или CSS файлове. Ако вашият файл не е рефериран никъде, той няма да бъде компилиран. Ако искате да направите Vite наясно с други активи, можете да ги преместите в [Публична папка |#public folder]. - -Моля, имайте предвид, че по подразбиране Vite ще вгради всички активи, по-малки от 4KB, така че няма да можете да реферирате тези файлове директно. (Вижте [документацията на Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Това работи - това е входна точка *} -{asset 'app.js'} - -{* ✓ Това работи - това е в assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Това няма да работи - произволен файл в assets/ *} -{asset 'components/button.js'} -``` - - -Режим на разработка -=================== - -Режимът на разработка е напълно опционален, но предоставя значителни предимства, когато е активиран. Основното предимство е **Hot Module Replacement (HMR)** - вижте промените незабавно, без да губите състоянието на приложението, което прави процеса на разработка много по-плавен и бърз. - -Vite е модерен инструмент за изграждане, който прави разработката невероятно бърза. За разлика от традиционните пакетиращи инструменти, Vite обслужва вашия код директно на браузъра по време на разработка, което означава незабавен старт на сървъра, независимо колко голям е вашият проект, и светкавично бързи актуализации. - - -Стартиране на сървър за разработка ----------------------------------- - -Стартирайте сървъра за разработка: - -```shell -npm run dev -``` - -Ще видите: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Дръжте този терминал отворен, докато разработвате. - -Плъгинът Nette Vite автоматично открива кога: -1. Vite dev сървърът работи -2. Вашето Nette приложение е в режим на отстраняване на грешки - -Когато и двете условия са изпълнени, Nette Assets зарежда файлове от Vite dev сървъра вместо от компилираната директория: - -```latte -{asset 'app.js'} -{* В разработка: *} -{* В продукция: *} -``` - -Не е необходима конфигурация - просто работи! - - -Работа на различни домейни --------------------------- - -Ако вашият сървър за разработка работи на нещо различно от `localhost` (като `myapp.local`), може да срещнете проблеми с CORS (Cross-Origin Resource Sharing). CORS е функция за сигурност в уеб браузърите, която по подразбиране блокира заявки между различни домейни. Когато вашето PHP приложение работи на `myapp.local`, но Vite работи на `localhost:5173`, браузърът ги вижда като различни домейни и блокира заявките. - -Имате две опции за решаване на това: - -**Опция 1: Конфигурирайте CORS** - -Най-простото решение е да разрешите заявки от различни източници от вашето PHP приложение: - -```js -export default defineConfig({ - // ... друга конфигурация ... - - server: { - cors: { - origin: 'http://myapp.local', // URL на вашето PHP приложение - }, - }, -}); -``` -**Опция 2: Пуснете Vite на вашия домейн** - -Другото решение е да накарате Vite да работи на същия домейн като вашето PHP приложение. - -```js -export default defineConfig({ - // ... друга конфигурация ... - - server: { - host: 'myapp.local', // същото като вашето PHP приложение - }, -}); -``` - -Всъщност, дори в този случай, трябва да конфигурирате CORS, защото dev сървърът работи на същия хост, но на различен порт. Въпреки това, в този случай CORS се конфигурира автоматично от плъгина Nette Vite. - - -HTTPS разработка ----------------- - -Ако разработвате на HTTPS, имате нужда от сертификати за вашия Vite сървър за разработка. Най-лесният начин е да използвате плъгин, който генерира сертификати автоматично: - -```shell -npm install -D vite-plugin-mkcert -``` - -Ето как да го конфигурирате във `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... друга конфигурация ... - - plugins: [ - mkcert(), // генерира сертификати автоматично и активира https - nette(), - ], -}); -``` - -Имайте предвид, че ако използвате CORS конфигурацията (Опция 1 отгоре), трябва да актуализирате URL адреса на източника, за да използва `https://` вместо `http://`. - - -Продукционни компилации -======================= - -Създайте оптимизирани продукционни файлове: - -```shell -npm run build -``` - -Vite ще: -- Минифицира целия JavaScript и CSS -- Раздели кода на оптимални части -- Генерира хеширани имена на файлове за кеш-изчистване -- Създаде манифест файл за Nette Assets - -Примерен изход: - -``` -www/assets/ -├── app-4f3a2b1c.js # Вашият основен JavaScript (минифициран) -├── app-7d8e9f2a.css # Извлечен CSS (минифициран) -├── vendor-8c4b5e6d.js # Споделени зависимости -└── .vite/ - └── manifest.json # Мапиране за Nette Assets -``` - -Хешираните имена на файлове гарантират, че браузърите винаги зареждат най-новата версия. - - -Публична папка -============== - -Файловете в директорията `assets/public/` се копират в изхода без обработка: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Реферирайте ги нормално: - -```latte -{* Тези файлове се копират както са *} - - -``` - -За публични файлове можете да използвате функциите на FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Първо опитайте WebP - versioning: true # Добавете cache-busting -``` - -В конфигурацията `vite.config.ts` можете да промените публичната папка, като използвате опцията `publicDir`. - - -Динамични импорти -================= - -Vite автоматично разделя кода за оптимално зареждане. Динамичните импорти ви позволяват да зареждате код само когато е наистина необходим, намалявайки първоначалния размер на пакета: - -```js -// Зареждане на тежки компоненти при поискване -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Динамичните импорти създават отделни части, които се зареждат само когато е необходимо. Това се нарича "разделяне на кода" и е една от най-мощните функции на Vite. Когато използвате динамични импорти, Vite автоматично създава отделни JavaScript файлове за всеки динамично импортиран модул. - -Тагът `{asset 'app.js'}` **не** зарежда автоматично тези динамични части. Това е умишлено поведение - не искаме да изтегляме код, който може никога да не бъде използван. Частите се изтеглят само когато динамичният импорт бъде изпълнен. - -Въпреки това, ако знаете, че определени динамични импорти са критични и ще са необходими скоро, можете да ги предварително заредите: - -```latte -{* Основна входна точка *} -{asset 'app.js'} - -{* Предварително зареждане на критични динамични импорти *} -{preload 'components/chart.js'} -``` - -Това казва на браузъра да изтегли компонента на диаграмата във фонов режим, така че да е готов веднага, когато е необходим. - - -Поддръжка на TypeScript -======================= - -TypeScript работи веднага: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Реферирайте TypeScript файлове нормално: - -```latte -{asset 'main.ts'} -``` - -За пълна поддръжка на TypeScript, инсталирайте го: - -```shell -npm install -D typescript -``` - - -Допълнителна конфигурация на Vite -================================= - -Ето някои полезни опции за конфигурация на Vite с подробни обяснения: - -```js -export default defineConfig({ - // Основна директория, съдържаща изходни активи - root: 'assets', - - // Папка, чието съдържание се копира в изходната директория както е - // По подразбиране: 'public' (относително спрямо 'root') - publicDir: 'public', - - build: { - // Къде да се поставят компилираните файлове (относително спрямо 'root') - outDir: '../www/assets', - - // Изчистване на изходната директория преди изграждане? - // Полезно за премахване на стари файлове от предишни компилации - emptyOutDir: true, - - // Поддиректория в outDir за генерирани части и активи - // Това помага да се организира изходната структура - assetsDir: 'static', - - rollupOptions: { - // Входна(и) точка(и) - може да бъде един файл или масив от файлове - // Всяка входна точка става отделен пакет - input: [ - 'app.js', // основно приложение - 'admin.js', // административен панел - ], - }, - }, - - server: { - // Хост, към който да се свърже сървърът за разработка - // Използвайте '0.0.0.0', за да изложите на мрежата - host: 'localhost', - - // Порт за сървъра за разработка - port: 5173, - - // CORS конфигурация за заявки от различни източници - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Активиране на CSS source maps в разработка - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -Това е! Вече имате модерна система за изграждане, интегрирана с Nette Assets. diff --git a/assets/el/@home.texy b/assets/el/@home.texy deleted file mode 100644 index 5b90e27606..0000000000 --- a/assets/el/@home.texy +++ /dev/null @@ -1,432 +0,0 @@ -Nette Assets -************ - -
    - -Έχετε κουραστεί να διαχειρίζεστε χειροκίνητα στατικά αρχεία στις εφαρμογές ιστού σας; Ξεχάστε την ενσωμάτωση σκληρών διαδρομών, την αντιμετώπιση της ακύρωσης της κρυφής μνήμης ή την ανησυχία για την έκδοση αρχείων. Το Nette Assets μεταμορφώνει τον τρόπο που εργάζεστε με εικόνες, φύλλα στυλ, σενάρια και άλλους στατικούς πόρους. - -- **Έξυπνη έκδοση** διασφαλίζει ότι τα προγράμματα περιήγησης φορτώνουν πάντα τα πιο πρόσφατα αρχεία -- **Αυτόματη ανίχνευση** τύπων αρχείων και διαστάσεων -- **Απρόσκοπτη ενσωμάτωση Latte** με διαισθητικές ετικέτες -- **Ευέλικτη αρχιτεκτονική** που υποστηρίζει συστήματα αρχείων, CDN και Vite -- **Lazy loading** για βέλτιστη απόδοση - -
    - - -Γιατί Nette Assets; -=================== - -Η εργασία με στατικά αρχεία συχνά σημαίνει επαναλαμβανόμενο κώδικα επιρρεπή σε σφάλματα. Κατασκευάζετε χειροκίνητα διευθύνσεις URL, προσθέτετε παραμέτρους έκδοσης για την εκκαθάριση της κρυφής μνήμης και χειρίζεστε διαφορετικούς τύπους αρχείων με διαφορετικό τρόπο. Αυτό οδηγεί σε κώδικα όπως: - -```latte -Logo - -``` - -Με το Nette Assets, όλη αυτή η πολυπλοκότητα εξαφανίζεται: - -```latte -{* Everything automated - URL, versioning, dimensions *} - - - -{* Or just *} -{asset 'css/style.css'} -``` - -Αυτό είναι όλο! Η βιβλιοθήκη αυτόματα: -- Προσθέτει παραμέτρους έκδοσης με βάση την ώρα τροποποίησης του αρχείου -- Ανιχνεύει τις διαστάσεις της εικόνας και τις συμπεριλαμβάνει στο HTML -- Δημιουργεί το σωστό στοιχείο HTML για κάθε τύπο αρχείου -- Χειρίζεται τόσο το περιβάλλον ανάπτυξης όσο και το περιβάλλον παραγωγής - - -Εγκατάσταση -=========== - -Εγκαταστήστε το Nette Assets χρησιμοποιώντας το [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Απαιτεί PHP 8.1 ή νεότερο και λειτουργεί τέλεια με το Nette Framework, αλλά μπορεί επίσης να χρησιμοποιηθεί αυτόνομα. - - -Πρώτα Βήματα -============ - -Το Nette Assets λειτουργεί άμεσα χωρίς καμία ρύθμιση. Τοποθετήστε τα στατικά σας αρχεία στον κατάλογο `www/assets/` και αρχίστε να τα χρησιμοποιείτε: - -```latte -{* Display an image with automatic dimensions *} -{asset 'logo.png'} - -{* Include a stylesheet with versioning *} -{asset 'style.css'} - -{* Load a JavaScript module *} -{asset 'app.js'} -``` - -Για περισσότερο έλεγχο στο παραγόμενο HTML, χρησιμοποιήστε το χαρακτηριστικό `n:asset` ή τη συνάρτηση `asset()`. - - -Πώς Λειτουργεί -============== - -Το Nette Assets βασίζεται σε τρεις βασικές έννοιες που το καθιστούν ισχυρό αλλά απλό στη χρήση: - - -Assets - Τα Αρχεία σας Έγιναν Έξυπνα ------------------------------------- - -Ένα **asset** αντιπροσωπεύει οποιοδήποτε στατικό αρχείο στην εφαρμογή σας. Κάθε αρχείο γίνεται ένα αντικείμενο με χρήσιμες ιδιότητες μόνο για ανάγνωση: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Διαφορετικοί τύποι αρχείων παρέχουν διαφορετικές ιδιότητες: -- **Εικόνες**: πλάτος, ύψος, εναλλακτικό κείμενο, lazy loading -- **Σενάρια**: τύπος ενότητας (module), hashes ακεραιότητας, crossorigin -- **Φύλλα στυλ**: media queries, ακεραιότητα -- **Ήχος/Βίντεο**: διάρκεια, διαστάσεις -- **Γραμματοσειρές**: σωστή προφόρτωση με CORS - -Η βιβλιοθήκη ανιχνεύει αυτόματα τους τύπους αρχείων και δημιουργεί την κατάλληλη κλάση asset. - - -Mappers - Από Πού Προέρχονται τα Αρχεία ---------------------------------------- - -Ένας **mapper** γνωρίζει πώς να βρίσκει αρχεία και να δημιουργεί διευθύνσεις URL για αυτά. Μπορείτε να έχετε πολλούς mappers για διαφορετικούς σκοπούς - τοπικά αρχεία, CDN, αποθήκευση στο cloud ή εργαλεία δημιουργίας (το καθένα από αυτά έχει ένα όνομα). Ο ενσωματωμένος `FilesystemMapper` χειρίζεται τα τοπικά αρχεία, ενώ ο `ViteMapper` ενσωματώνεται με σύγχρονα εργαλεία δημιουργίας. - -Οι mappers ορίζονται στην [Διαμόρφωση |Configuration]. - - -Registry - Η Κύρια Διεπαφή σας ------------------------------- - -Το **registry** διαχειρίζεται όλους τους mappers και παρέχει το κύριο API: - -```php -// Inject the registry in your service -public function __construct( - private Nette\Assets\Registry $assets -) {} - -// Get assets from different mappers -$logo = $this->assets->getAsset('images:logo.png'); // 'image' mapper -$app = $this->assets->getAsset('app:main.js'); // 'app' mapper -$style = $this->assets->getAsset('style.css'); // uses default mapper -``` - -Το registry επιλέγει αυτόματα τον σωστό mapper και αποθηκεύει τα αποτελέσματα στην κρυφή μνήμη για καλύτερη απόδοση. - - -Εργασία με Assets σε PHP -======================== - -Το Registry παρέχει δύο μεθόδους για την ανάκτηση assets: - -```php -// Throws Nette\Assets\AssetNotFoundException if file doesn't exist -$logo = $assets->getAsset('logo.png'); - -// Returns null if file doesn't exist -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Καθορισμός Mappers ------------------- - -Μπορείτε να επιλέξετε ρητά ποιον mapper θα χρησιμοποιήσετε: - -```php -// Use default mapper -$file = $assets->getAsset('document.pdf'); - -// Use specific mapper with prefix -$image = $assets->getAsset('images:photo.jpg'); - -// Use specific mapper with array syntax -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Ιδιότητες και Τύποι Asset -------------------------- - -Κάθε τύπος asset παρέχει σχετικές ιδιότητες μόνο για ανάγνωση: - -```php -// Image properties -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Script properties -$script = $assets->getAsset('app.js'); -echo $script->type; // 'module' or null - -// Audio properties -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // duration in seconds - -// All assets can be cast to string (returns URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Ιδιότητες όπως διαστάσεις ή διάρκεια φορτώνονται με lazy loading μόνο όταν προσπελαστούν, διατηρώντας τη βιβλιοθήκη γρήγορη. - - -Χρήση Assets σε Πρότυπα Latte -============================= - -Το Nette Assets παρέχει διαισθητική ενσωμάτωση [Latte|latte:] με ετικέτες και συναρτήσεις. - - -`{asset}` ---------- - -Η ετικέτα `{asset}` αποδίδει πλήρη στοιχεία HTML: - -```latte -{* Renders: *} -{asset 'hero.jpg'} - -{* Renders: *} -{asset 'app.js'} - -{* Renders: *} -{asset 'style.css'} -``` - -Η ετικέτα αυτόματα: -- Ανιχνεύει τον τύπο asset και δημιουργεί το κατάλληλο HTML -- Περιλαμβάνει έκδοση για την εκκαθάριση της κρυφής μνήμης -- Προσθέτει διαστάσεις για εικόνες -- Ορίζει τα σωστά χαρακτηριστικά (type, media, κ.λπ.) - -Όταν χρησιμοποιείται μέσα σε χαρακτηριστικά HTML, εξάγει μόνο τη διεύθυνση URL: - -```latte -
    - -``` - - -`n:asset` ---------- - -Για πλήρη έλεγχο των χαρακτηριστικών HTML: - -```latte -{* The n:asset attribute fills in src, dimensions, etc. *} -Product - -{* Works with any relevant element *} - - - -``` - -Χρησιμοποιήστε μεταβλητές και mappers: - -```latte -{* Variables work naturally *} - - -{* Specify mapper with curly brackets *} - - -{* Specify mapper with array notation *} - -``` - - -`asset()` ---------- - -Για μέγιστη ευελιξία, χρησιμοποιήστε τη συνάρτηση `asset()`: - -```latte -{var $logo = asset('logo.png')} -width} height={$logo->height}> - -{* Or directly *} -Logo -``` - - -Προαιρετικά Assets ------------------- - -Χειριστείτε τα ελλείποντα assets με χάρη με `{asset?}`, `n:asset?` και `tryAsset()`: - -```latte -{* Optional tag - renders nothing if asset missing *} -{asset? 'optional-banner.jpg'} - -{* Optional attribute - skips if asset missing *} -Avatar - -{* With fallback *} -{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')} -Avatar -``` - - -`{preload}` ------------ - -Βελτιώστε την απόδοση φόρτωσης σελίδας: - -```latte -{* In your section *} -{preload 'critical.css'} -{preload 'important-font.woff2'} -{preload 'hero-image.jpg'} -``` - -Δημιουργεί κατάλληλους συνδέσμους προφόρτωσης: - -```latte - - - -``` - - -Προηγμένες Λειτουργίες -====================== - - -Αυτόματη Ανίχνευση Επέκτασης ----------------------------- - -Χειριστείτε αυτόματα πολλαπλές μορφές: - -```neon -assets: - mapping: - images: - path: img - extension: [webp, jpg, png] # Try in order -``` - -Τώρα μπορείτε να ζητήσετε χωρίς επέκταση: - -```latte -{* Finds logo.webp, logo.jpg, or logo.png automatically *} -{asset 'images:logo'} -``` - -Ιδανικό για προοδευτική βελτίωση με σύγχρονες μορφές. - - -Έξυπνη Έκδοση -------------- - -Τα αρχεία εκδίδονται αυτόματα με βάση την ώρα τροποποίησης: - -```latte -{asset 'style.css'} -{* Output: *} -``` - -Όταν ενημερώνετε το αρχείο, η χρονοσφραγίδα αλλάζει, αναγκάζοντας την ανανέωση της κρυφής μνήμης του προγράμματος περιήγησης. - -Έλεγχος έκδοσης ανά asset: - -```php -// Disable versioning for specific asset -$asset = $assets->getAsset('style.css', ['version' => false]); - -// In Latte -{asset 'style.css', version: false} -``` - - -Assets Γραμματοσειρών ---------------------- - -Οι γραμματοσειρές λαμβάνουν ειδική μεταχείριση με σωστό CORS: - -```latte -{* Proper preload with crossorigin *} -{preload 'fonts:OpenSans-Regular.woff2'} - -{* Use in CSS *} - -``` - - -Προσαρμοσμένοι Mappers -====================== - -Δημιουργήστε προσαρμοσμένους mappers για ειδικές ανάλογες ανάγκες όπως αποθήκευση στο cloud ή δυναμική δημιουργία: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Καταχωρήστε στη διαμόρφωση: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Χρησιμοποιήστε όπως οποιονδήποτε άλλο mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Η μέθοδος `Helpers::createAssetFromUrl()` δημιουργεί αυτόματα τον σωστό τύπο asset με βάση την επέκταση αρχείου. - - -Περαιτέρω ανάγνωση -================== - -- [Nette Assets: για τα πάντα, από εικόνες έως Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/el/@left-menu.texy b/assets/el/@left-menu.texy deleted file mode 100644 index 4e74c3d9a0..0000000000 --- a/assets/el/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Ξεκινώντας |@home] -- [Vite |vite] -- [Διαμόρφωση |Configuration] diff --git a/assets/el/@meta.texy b/assets/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/assets/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/assets/el/configuration.texy b/assets/el/configuration.texy deleted file mode 100644 index 8ec9f2944c..0000000000 --- a/assets/el/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Διαμόρφωση Assets -***************** - -.[perex] -Επισκόπηση των επιλογών διαμόρφωσης για το Nette Assets. - - -```neon -assets: - # base path for resolving relative mapper paths - basePath: ... # (string) defaults to %wwwDir% - - # base URL for resolving relative mapper URLs - baseUrl: ... # (string) defaults to %baseUrl% - - # enable asset versioning globally? - versioning: ... # (bool) defaults to true - - # defines asset mappers - mapping: ... # (array) defaults to path 'assets' -``` - -Το `basePath` ορίζει τον προεπιλεγμένο κατάλογο συστήματος αρχείων για την επίλυση σχετικών διαδρομών σε mappers. Από προεπιλογή, χρησιμοποιεί τον κατάλογο web (`%wwwDir%`). - -Το `baseUrl` ορίζει το προεπιλεγμένο πρόθεμα URL για την επίλυση σχετικών URL σε mappers. Από προεπιλογή, χρησιμοποιεί το root URL (`%baseUrl%`). - -Η επιλογή `versioning` ελέγχει καθολικά εάν προστίθενται παράμετροι έκδοσης στις διευθύνσεις URL των assets για την εκκαθάριση της κρυφής μνήμης. Οι μεμονωμένοι mappers μπορούν να παρακάμψουν αυτήν τη ρύθμιση. - - -Mappers -------- - -Οι Mappers μπορούν να διαμορφωθούν με τρεις τρόπους: απλή σύνταξη συμβολοσειράς, λεπτομερής σύνταξη πίνακα ή ως αναφορά σε μια υπηρεσία. - -Ο απλούστερος τρόπος για να ορίσετε έναν mapper: - -```neon -assets: - mapping: - default: assets # Creates filesystem mapper for %wwwDir%/assets/ - images: img # Creates filesystem mapper for %wwwDir%/img/ - scripts: js # Creates filesystem mapper for %wwwDir%/js/ -``` - -Κάθε mapper δημιουργεί έναν `FilesystemMapper` που: -- Αναζητά αρχεία στο `%wwwDir%/` -- Δημιουργεί διευθύνσεις URL όπως `%baseUrl%/` -- Κληρονομεί την καθολική ρύθμιση έκδοσης - - -Για περισσότερο έλεγχο, χρησιμοποιήστε τη λεπτομερή σύνταξη: - -```neon -assets: - mapping: - images: - # directory where files are stored - path: ... # (string) optional, defaults to '' - - # URL prefix for generated links - url: ... # (string) optional, defaults to path - - # enable versioning for this mapper? - versioning: ... # (bool) optional, inherits global setting - - # auto-add extension(s) when searching for files - extension: ... # (string|array) optional, defaults to null -``` - -Κατανόηση του τρόπου επίλυσης των τιμών διαμόρφωσης: - -Επίλυση Διαδρομής: - - Οι σχετικές διαδρομές επιλύονται από το `basePath` (ή `%wwwDir%` εάν το `basePath` δεν έχει οριστεί) - - Οι απόλυτες διαδρομές χρησιμοποιούνται ως έχουν - -Επίλυση URL: - - Οι σχετικές διευθύνσεις URL επιλύονται από το `baseUrl` (ή `%baseUrl%` εάν το `baseUrl` δεν έχει οριστεί) - - Οι απόλυτες διευθύνσεις URL (με σχήμα ή `//`) χρησιμοποιούνται ως έχουν - - Εάν το `url` δεν έχει καθοριστεί, χρησιμοποιεί την τιμή του `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Relative path and URL - images: - path: img # Resolved to: /var/www/project/www/img - url: images # Resolved to: https://example.com/assets/images - - # Absolute path and URL - uploads: - path: /var/shared/uploads # Used as-is: /var/shared/uploads - url: https://cdn.example.com # Used as-is: https://cdn.example.com - - # Only path specified - styles: - path: css # Path: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Προσαρμοσμένοι Mappers ----------------------- - -Για προσαρμοσμένους mappers, αναφέρετε ή ορίστε μια υπηρεσία: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Ο Vite mapper απαιτεί μόνο να προσθέσετε `type: vite`. Αυτή είναι μια πλήρης λίστα επιλογών διαμόρφωσης: - -```neon -assets: - mapping: - default: - # mapper type (required for Vite) - type: vite # (string) required, must be 'vite' - - # Vite build output directory - path: ... # (string) optional, defaults to '' - - # URL prefix for built assets - url: ... # (string) optional, defaults to path - - # location of Vite manifest file - manifest: ... # (string) optional, defaults to /.vite/manifest.json - - # Vite dev server configuration - devServer: ... # (bool|string) optional, defaults to true - - # versioning for public directory files - versioning: ... # (bool) optional, inherits global setting - - # auto-extension for public directory files - extension: ... # (string|array) optional, defaults to null -``` - -Η επιλογή `devServer` ελέγχει τον τρόπο φόρτωσης των assets κατά την ανάπτυξη: - -- `true` (προεπιλογή) - Ανιχνεύει αυτόματα τον Vite dev server στον τρέχοντα host και port. Εάν ο dev server εκτελείται **και η εφαρμογή σας είναι σε λειτουργία debug**, τα assets φορτώνονται από αυτόν με υποστήριξη hot module replacement. Εάν ο dev server δεν εκτελείται, τα assets φορτώνονται από τα δημιουργημένα αρχεία στον δημόσιο κατάλογο. -- `false` - Απενεργοποιεί πλήρως την ενσωμάτωση του dev server. Τα assets φορτώνονται πάντα από τα δημιουργημένα αρχεία. -- Προσαρμοσμένη διεύθυνση URL (π.χ., `https://localhost:5173`) - Καθορίστε χειροκίνητα τη διεύθυνση URL του dev server συμπεριλαμβανομένου του πρωτοκόλλου και του port. Χρήσιμο όταν ο dev server εκτελείται σε διαφορετικό host ή port. - -Οι επιλογές `versioning` και `extension` ισχύουν μόνο για αρχεία στον δημόσιο κατάλογο του Vite που δεν επεξεργάζονται από το Vite. - - -Μη Αυτόματη Διαμόρφωση ----------------------- - -Όταν δεν χρησιμοποιείτε το Nette DI, διαμορφώστε τους mappers χειροκίνητα: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Add filesystem mapper -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Add Vite mapper -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/el/vite.texy b/assets/el/vite.texy deleted file mode 100644 index d526f68d5e..0000000000 --- a/assets/el/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Ενσωμάτωση Vite -*************** - -
    - -Οι σύγχρονες εφαρμογές JavaScript απαιτούν εξελιγμένα εργαλεία δημιουργίας. Το Nette Assets παρέχει ενσωμάτωση πρώτης κατηγορίας με το [Vite |https://vitejs.dev/], το εργαλείο δημιουργίας frontend επόμενης γενιάς. Αποκτήστε αστραπιαία ανάπτυξη με Hot Module Replacement (HMR) και βελτιστοποιημένες εκδόσεις παραγωγής χωρίς προβλήματα διαμόρφωσης. - -- **Μηδενική διαμόρφωση** - αυτόματη γέφυρα μεταξύ Vite και προτύπων PHP -- **Πλήρης διαχείριση εξαρτήσεων** - μία ετικέτα χειρίζεται όλα τα assets -- **Hot Module Replacement** - άμεσες ενημερώσεις JavaScript και CSS -- **Βελτιστοποιημένες εκδόσεις παραγωγής** - code splitting και tree shaking - -
    - - -Το Nette Assets ενσωματώνεται απρόσκοπτα με το Vite, οπότε έχετε όλα αυτά τα οφέλη ενώ γράφετε τα πρότυπά σας ως συνήθως. - - -Ρύθμιση του Vite -================ - -Ας ρυθμίσουμε το Vite βήμα προς βήμα. Μην ανησυχείτε αν είστε νέοι στα εργαλεία δημιουργίας - θα εξηγήσουμε τα πάντα! - - -Βήμα 1: Εγκατάσταση του Vite ----------------------------- - -Πρώτα, εγκαταστήστε το Vite και το Nette plugin στο έργο σας: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Αυτό εγκαθιστά το Vite και ένα ειδικό plugin που βοηθά το Vite να λειτουργεί τέλεια με το Nette. - - -Βήμα 2: Δομή Έργου ------------------- - -Η τυπική προσέγγιση είναι να τοποθετήσετε τα αρχεία asset πηγής σε έναν φάκελο `assets/` στον ριζικό κατάλογο του έργου σας και τις μεταγλωττισμένες εκδόσεις στο `www/assets/`: - -/--pre -web-project/ -├── assets/ ← αρχεία πηγής (SCSS, TypeScript, εικόνες πηγής) -│ ├── public/ ← στατικά αρχεία (αντιγράφονται ως έχουν) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← κύριο σημείο εισόδου -│ └── style.css ← τα στυλ σας -└── www/ ← δημόσιος κατάλογος (document root) - ├── assets/ ← τα μεταγλωττισμένα αρχεία θα πάνε εδώ - └── index.php -\-- - -Ο φάκελος `assets/` περιέχει τα αρχεία πηγής σας - τον κώδικα που γράφετε. Το Vite θα επεξεργαστεί αυτά τα αρχεία και θα τοποθετήσει τις μεταγλωττισμένες εκδόσεις στο `www/assets/`. - - -Βήμα 3: Διαμόρφωση του Vite ---------------------------- - -Δημιουργήστε ένα αρχείο `vite.config.ts` στον ριζικό κατάλογο του έργου σας. Αυτό το αρχείο λέει στο Vite πού να βρει τα αρχεία πηγής σας και πού να τοποθετήσει τα μεταγλωττισμένα. - -Το Nette Vite plugin έρχεται με έξυπνες προεπιλογές που κάνουν τη διαμόρφωση απλή. Υποθέτει ότι τα αρχεία πηγής frontend βρίσκονται στον κατάλογο `assets/` (επιλογή `root`) και τα μεταγλωττισμένα αρχεία πηγαίνουν στο `www/assets/` (επιλογή `outDir`). Χρειάζεται μόνο να καθορίσετε το [σημείο εισόδου|#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Εάν θέλετε να καθορίσετε άλλο όνομα καταλόγου για να δημιουργήσετε τα assets σας, θα χρειαστεί να αλλάξετε μερικές επιλογές: - -```js -export default defineConfig({ - root: 'assets', // root directory of source assets - - build: { - outDir: '../www/assets', // where compiled files go - }, - - // ... other config ... -}); -``` - -.[note] -Η διαδρομή `outDir` θεωρείται σχετική με το `root`, γι' αυτό υπάρχει το `../` στην αρχή. - - -Βήμα 4: Διαμόρφωση του Nette ----------------------------- - -Ενημερώστε το Nette Assets για το Vite στο `common.neon` σας: - -```neon -assets: - mapping: - default: - type: vite # tells Nette to use the ViteMapper - path: assets -``` - - -Βήμα 5: Προσθήκη σεναρίων -------------------------- - -Προσθέστε αυτά τα σενάρια στο `package.json` σας: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Τώρα μπορείτε: -- `npm run dev` - εκκίνηση του development server με hot reloading -- `npm run build` - δημιουργία βελτιστοποιημένων αρχείων παραγωγής - - -Σημεία Εισόδου -============== - -Ένα **σημείο εισόδου** είναι το κύριο αρχείο από όπου ξεκινά η εφαρμογή σας. Από αυτό το αρχείο, εισάγετε άλλα αρχεία (CSS, μονάδες JavaScript, εικόνες), δημιουργώντας ένα δέντρο εξαρτήσεων. Το Vite ακολουθεί αυτές τις εισαγωγές και ομαδοποιεί τα πάντα μαζί. - -Παράδειγμα σημείου εισόδου `assets/app.js`: - -```js -// Import styles -import './style.css' - -// Import JavaScript modules -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Initialize your application -netteForms.initOnLoad(); -naja.initialize(); -``` - -Στο πρότυπο μπορείτε να εισάγετε ένα σημείο εισόδου ως εξής: - -```latte -{asset 'app.js'} -``` - -Το Nette Assets δημιουργεί αυτόματα όλες τις απαραίτητες ετικέτες HTML - JavaScript, CSS και οποιεσδήποτε άλλες εξαρτήσεις. - - -Πολλαπλά Σημεία Εισόδου ------------------------ - -Μεγαλύτερες εφαρμογές συχνά χρειάζονται ξεχωριστά σημεία εισόδου: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // public pages - 'admin.js', // admin panel - ], - }), - ], -}); -``` - -Χρησιμοποιήστε τα σε διαφορετικά πρότυπα: - -```latte -{* In public pages *} -{asset 'app.js'} - -{* In admin panel *} -{asset 'admin.js'} -``` - - -Σημαντικό: Αρχεία Πηγής έναντι Μεταγλωττισμένων Αρχείων -------------------------------------------------------- - -Είναι κρίσιμο να κατανοήσετε ότι στην παραγωγή μπορείτε να φορτώσετε μόνο: - -1. **Σημεία εισόδου** που ορίζονται στο `entry` -2. **Αρχεία από τον κατάλογο `assets/public/`** - -Δεν μπορείτε να φορτώσετε χρησιμοποιώντας `{asset}` αυθαίρετα αρχεία από το `assets/` - μόνο assets που αναφέρονται από αρχεία JavaScript ή CSS. Εάν το αρχείο σας δεν αναφέρεται πουθενά, δεν θα μεταγλωττιστεί. Εάν θέλετε να κάνετε το Vite να γνωρίζει άλλα assets, μπορείτε να τα μετακινήσετε στον [δημόσιο φάκελο|#public folder]. - -Λάβετε υπόψη ότι από προεπιλογή, το Vite θα ενσωματώσει όλα τα assets μικρότερα από 4KB, οπότε δεν θα μπορείτε να αναφέρετε αυτά τα αρχεία απευθείας. (Δείτε την [τεκμηρίωση του Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ This works - it's an entry point *} -{asset 'app.js'} - -{* ✓ This works - it's in assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ This won't work - random file in assets/ *} -{asset 'components/button.js'} -``` - - -Λειτουργία Ανάπτυξης -==================== - -Η λειτουργία ανάπτυξης είναι εντελώς προαιρετική, αλλά παρέχει σημαντικά οφέλη όταν είναι ενεργοποιημένη. Το κύριο πλεονέκτημα είναι το **Hot Module Replacement (HMR)** - δείτε τις αλλαγές άμεσα χωρίς να χάσετε την κατάσταση της εφαρμογής, κάνοντας την εμπειρία ανάπτυξης πολύ πιο ομαλή και ταχύτερη. - -Το Vite είναι ένα σύγχρονο εργαλείο δημιουργίας που κάνει την ανάπτυξη απίστευτα γρήγορη. Σε αντίθεση με τους παραδοσιακούς bundlers, το Vite εξυπηρετεί τον κώδικά σας απευθείας στο πρόγραμμα περιήγησης κατά την ανάπτυξη, πράγμα που σημαίνει άμεση εκκίνηση του server ανεξάρτητα από το μέγεθος του έργου σας και αστραπιαίες ενημερώσεις. - - -Εκκίνηση του Development Server -------------------------------- - -Εκτελέστε τον development server: - -```shell -npm run dev -``` - -Θα δείτε: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Κρατήστε αυτό το τερματικό ανοιχτό κατά την ανάπτυξη. - -Το Nette Vite plugin ανιχνεύει αυτόματα όταν: -1. Ο Vite dev server εκτελείται -2. Η εφαρμογή Nette σας είναι σε λειτουργία debug - -Όταν πληρούνται και οι δύο προϋποθέσεις, το Nette Assets φορτώνει αρχεία από τον Vite dev server αντί από τον μεταγλωττισμένο κατάλογο: - -```latte -{asset 'app.js'} -{* In development: *} -{* In production: *} -``` - -Δεν απαιτείται διαμόρφωση - απλά λειτουργεί! - - -Εργασία σε Διαφορετικούς Τομείς (Domains) ------------------------------------------ - -Εάν ο development server σας εκτελείται σε κάτι άλλο εκτός από το `localhost` (όπως `myapp.local`), ενδέχεται να αντιμετωπίσετε προβλήματα CORS (Cross-Origin Resource Sharing). Το CORS είναι ένα χαρακτηριστικό ασφαλείας στα προγράμματα περιήγησης ιστού που μπλοκάρει τις αιτήσεις μεταξύ διαφορετικών τομέων από προεπιλογή. Όταν η εφαρμογή PHP σας εκτελείται στο `myapp.local` αλλά το Vite εκτελείται στο `localhost:5173`, το πρόγραμμα περιήγησης τα βλέπει ως διαφορετικούς τομείς και μπλοκάρει τις αιτήσεις. - -Έχετε δύο επιλογές για να το λύσετε: - -**Επιλογή 1: Διαμόρφωση CORS** - -Η απλούστερη λύση είναι να επιτρέψετε αιτήσεις cross-origin από την εφαρμογή PHP σας: - -```js -export default defineConfig({ - // ... other config ... - - server: { - cors: { - origin: 'http://myapp.local', // your PHP app URL - }, - }, -}); -``` -**Επιλογή 2: Εκτελέστε το Vite στον τομέα σας** - -Η άλλη λύση είναι να κάνετε το Vite να εκτελείται στον ίδιο τομέα με την εφαρμογή PHP σας. - -```js -export default defineConfig({ - // ... other config ... - - server: { - host: 'myapp.local', // same as your PHP app - }, -}); -``` - -Πράγματι, ακόμη και σε αυτή την περίπτωση, πρέπει να διαμορφώσετε το CORS επειδή ο dev server εκτελείται στον ίδιο hostname αλλά σε διαφορετικό port. Ωστόσο, σε αυτή την περίπτωση, το CORS διαμορφώνεται αυτόματα από το Nette Vite plugin. - - -Ανάπτυξη HTTPS --------------- - -Εάν αναπτύσσετε σε HTTPS, χρειάζεστε πιστοποιητικά για τον Vite development server σας. Ο ευκολότερος τρόπος είναι να χρησιμοποιήσετε ένα plugin που δημιουργεί αυτόματα πιστοποιητικά: - -```shell -npm install -D vite-plugin-mkcert -``` - -Δείτε πώς να το διαμορφώσετε στο `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... other config ... - - plugins: [ - mkcert(), // generates certificates automatically and enables https - nette(), - ], -}); -``` - -Σημειώστε ότι εάν χρησιμοποιείτε τη διαμόρφωση CORS (Επιλογή 1 από παραπάνω), πρέπει να ενημερώσετε τη διεύθυνση URL προέλευσης για να χρησιμοποιήσετε `https://` αντί για `http://`. - - -Εκδόσεις Παραγωγής -================== - -Δημιουργήστε βελτιστοποιημένα αρχεία παραγωγής: - -```shell -npm run build -``` - -Το Vite θα: -- Συμπιέσει (minify) όλα τα JavaScript και CSS -- Χωρίσει τον κώδικα σε βέλτιστα τμήματα (chunks) -- Δημιουργήσει ονόματα αρχείων με hash για cache-busting -- Δημιουργήσει ένα αρχείο manifest για το Nette Assets - -Παράδειγμα εξόδου: - -``` -www/assets/ -├── app-4f3a2b1c.js # Your main JavaScript (minified) -├── app-7d8e9f2a.css # Extracted CSS (minified) -├── vendor-8c4b5e6d.js # Shared dependencies -└── .vite/ - └── manifest.json # Mapping for Nette Assets -``` - -Τα ονόματα αρχείων με hash διασφαλίζουν ότι τα προγράμματα περιήγησης φορτώνουν πάντα την τελευταία έκδοση. - - -Δημόσιος Φάκελος -================ - -Τα αρχεία στον κατάλογο `assets/public/` αντιγράφονται στην έξοδο χωρίς επεξεργασία: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Αναφερθείτε σε αυτά κανονικά: - -```latte -{* These files are copied as-is *} - - -``` - -Για δημόσια αρχεία, μπορείτε να χρησιμοποιήσετε τις λειτουργίες του FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Try WebP first - versioning: true # Add cache-busting -``` - -Στη διαμόρφωση `vite.config.ts` μπορείτε να αλλάξετε τον δημόσιο φάκελο χρησιμοποιώντας την επιλογή `publicDir`. - - -Δυναμικές Εισαγωγές -=================== - -Το Vite χωρίζει αυτόματα τον κώδικα για βέλτιστη φόρτωση. Οι δυναμικές εισαγωγές σάς επιτρέπουν να φορτώνετε κώδικα μόνο όταν είναι πραγματικά απαραίτητος, μειώνοντας το αρχικό μέγεθος του bundle: - -```js -// Load heavy components on demand -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Οι δυναμικές εισαγωγές δημιουργούν ξεχωριστά τμήματα (chunks) που φορτώνονται μόνο όταν χρειάζονται. Αυτό ονομάζεται "code splitting" και είναι μία από τις πιο ισχυρές λειτουργίες του Vite. Όταν χρησιμοποιείτε δυναμικές εισαγωγές, το Vite δημιουργεί αυτόματα ξεχωριστά αρχεία JavaScript για κάθε δυναμικά εισαγόμενη ενότητα (module). - -Η ετικέτα `{asset 'app.js'}` **δεν** προφορτώνει αυτόματα αυτά τα δυναμικά τμήματα. Αυτή είναι σκόπιμη συμπεριφορά - δεν θέλουμε να κατεβάσουμε κώδικα που μπορεί να μην χρησιμοποιηθεί ποτέ. Τα τμήματα κατεβάζονται μόνο όταν εκτελείται η δυναμική εισαγωγή. - -Ωστόσο, εάν γνωρίζετε ότι ορισμένες δυναμικές εισαγωγές είναι κρίτιμες και θα χρειαστούν σύντομα, μπορείτε να τις προφορτώσετε: - -```latte -{* Main entry point *} -{asset 'app.js'} - -{* Preload critical dynamic imports *} -{preload 'components/chart.js'} -``` - -Αυτό λέει στο πρόγραμμα περιήγησης να κατεβάσει το στοιχείο του γραφήματος στο παρασκήνιο, ώστε να είναι άμεσα διαθέσιμο όταν χρειαστεί. - - -Υποστήριξη TypeScript -===================== - -Το TypeScript λειτουργεί άμεσα: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Αναφερθείτε στα αρχεία TypeScript κανονικά: - -```latte -{asset 'main.ts'} -``` - -Για πλήρη υποστήριξη TypeScript, εγκαταστήστε το: - -```shell -npm install -D typescript -``` - - -Πρόσθετη Διαμόρφωση Vite -======================== - -Ακολουθούν ορισμένες χρήσιμες επιλογές διαμόρφωσης Vite με λεπτομερείς επεξηγήσεις: - -```js -export default defineConfig({ - // Root directory containing source assets - root: 'assets', - - // Folder whose contents are copied to output directory as-is - // Default: 'public' (relative to 'root') - publicDir: 'public', - - build: { - // Where to put compiled files (relative to 'root') - outDir: '../www/assets', - - // Empty output directory before building? - // Useful to remove old files from previous builds - emptyOutDir: true, - - // Subdirectory within outDir for generated chunks and assets - // This helps organize the output structure - assetsDir: 'static', - - rollupOptions: { - // Entry point(s) - can be a single file or array of files - // Each entry point becomes a separate bundle - input: [ - 'app.js', // main application - 'admin.js', // admin panel - ], - }, - }, - - server: { - // Host to bind the dev server to - // Use '0.0.0.0' to expose to network - host: 'localhost', - - // Port for the dev server - port: 5173, - - // CORS configuration for cross-origin requests - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Enable CSS source maps in development - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -Αυτό είναι όλο! Έχετε τώρα ένα σύγχρονο σύστημα δημιουργίας ενσωματωμένο με το Nette Assets. diff --git a/assets/hu/@home.texy b/assets/hu/@home.texy deleted file mode 100644 index b48edbcb14..0000000000 --- a/assets/hu/@home.texy +++ /dev/null @@ -1,432 +0,0 @@ -Nette Assets -************ - -
    - -Eleged van a statikus fájlok manuális kezeléséből a webalkalmazásaidban? Felejtsd el a hardkódolt útvonalakat, a gyorsítótár érvénytelenítésével kapcsolatos problémákat vagy a fájlverziózással kapcsolatos aggodalmakat. A Nette Assets átalakítja a képekkel, stíluslapokkal, szkriptekkel és más statikus erőforrásokkal való munkát. - -- **Intelligens verziózás** biztosítja, hogy a böngészők mindig a legfrissebb fájlokat töltsék be -- Fájltípusok és dimenziók **automatikus felismerése** -- **Zökkenőmentes Latte integráció** intuitív tagekkel -- **Rugalmas architektúra** fájlrendszerek, CDN-ek és Vite támogatásával -- **Lusta betöltés** az optimális teljesítmény érdekében - -
    - - -Miért a Nette Assets? -===================== - -A statikus fájlokkal való munka gyakran ismétlődő, hibára hajlamos kódot jelent. Manuálisan konstruálsz URL-eket, verzióparamétereket adsz hozzá a gyorsítótár törléséhez, és különböző fájltípusokat eltérően kezelsz. Ez olyan kódhoz vezet, mint: - -```latte -Logo - -``` - -A Nette Assets segítségével mindez a bonyolultság eltűnik: - -```latte -{* Minden automatizált - URL, verziózás, dimenziók *} - - - -{* Vagy csak *} -{asset 'css/style.css'} -``` - -Ennyi! A könyvtár automatikusan: -- Hozzáadja a verzióparamétereket a fájl módosítási ideje alapján -- Felismeri a kép dimenzióit és beilleszti azokat a HTML-be -- Létrehozza a megfelelő HTML elemet minden fájltípushoz -- Kezeli a fejlesztői és éles környezeteket is - - -Telepítés -========= - -Telepítsd a Nette Assets-et a [Composer |best-practices:composer] segítségével: - -```shell -composer require nette/assets -``` - -PHP 8.1 vagy újabb verziót igényel, és tökéletesen működik a Nette Frameworkkel, de önállóan is használható. - - -Első lépések -============ - -A Nette Assets konfiguráció nélkül azonnal működik. Helyezd a statikus fájlokat a `www/assets/` könyvtárba, és kezdd el használni őket: - -```latte -{* Kép megjelenítése automatikus dimenziókkal *} -{asset 'logo.png'} - -{* Stíluslap beillesztése verziózással *} -{asset 'style.css'} - -{* JavaScript modul betöltése *} -{asset 'app.js'} -``` - -A generált HTML feletti nagyobb kontroll érdekében használd az `n:asset` attribútumot vagy az `asset()` függvényt. - - -Hogyan működik -============== - -A Nette Assets három alapvető koncepcióra épül, amelyek erőteljessé, mégis egyszerűvé teszik a használatát: - - -Assets - Intelligens fájljaid ------------------------------ - -Az **asset** az alkalmazásodban található bármely statikus fájlt jelenti. Minden fájl egy objektummá válik hasznos csak olvasható tulajdonságokkal: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Különböző fájltípusok különböző tulajdonságokat biztosítanak: -- **Képek**: szélesség, magasság, alternatív szöveg, lusta betöltés -- **Szkriptek**: modul típusa, integritás hash-ek, crossorigin -- **Stíluslapok**: média lekérdezések, integritás -- **Audió/Videó**: időtartam, dimenziók -- **Betűtípusok**: megfelelő előbetöltés CORS-szal - -A könyvtár automatikusan felismeri a fájltípusokat és létrehozza a megfelelő asset osztályt. - - -Mapperek - Honnan jönnek a fájlok ---------------------------------- - -Egy **mapper** tudja, hogyan találja meg a fájlokat és hogyan hozzon létre URL-eket számukra. Több mapper is lehet különböző célokra - helyi fájlok, CDN, felhőtárhely vagy build eszközök (mindegyiknek van neve). A beépített `FilesystemMapper` a helyi fájlokat kezeli, míg a `ViteMapper` integrálódik a modern build eszközökkel. - -A mapperek a [konfigurációban |Configuration] vannak definiálva. - - -Registry - A fő interfészed ---------------------------- - -A **registry** kezeli az összes mappert és biztosítja a fő API-t: - -```php -// Injektáld a registry-t a szolgáltatásodba -public function __construct( - private Nette\Assets\Registry $assets -) {} - -// Assetek lekérése különböző mapperekből -$logo = $this->assets->getAsset('images:logo.png'); // 'image' mapper -$app = $this->assets->getAsset('app:main.js'); // 'app' mapper -$style = $this->assets->getAsset('style.css'); // az alapértelmezett mappert használja -``` - -A registry automatikusan kiválasztja a megfelelő mappert és gyorsítótárazza az eredményeket a teljesítmény érdekében. - - -Assetek használata PHP-ban -========================== - -A Registry két módszert biztosít az assetek lekérésére: - -```php -// Nette\Assets\AssetNotFoundException-t dob, ha a fájl nem létezik -$logo = $assets->getAsset('logo.png'); - -// null-t ad vissza, ha a fájl nem létezik -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Mapperek megadása ------------------ - -Explicit módon kiválaszthatod, melyik mappert használd: - -```php -// Alapértelmezett mapper használata -$file = $assets->getAsset('document.pdf'); - -// Specifikus mapper használata prefixszel -$image = $assets->getAsset('images:photo.jpg'); - -// Specifikus mapper használata tömb szintaxissal -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Asset tulajdonságok és típusok ------------------------------- - -Minden asset típus releváns csak olvasható tulajdonságokat biztosít: - -```php -// Kép tulajdonságok -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Szkript tulajdonságok -$script = $assets->getAsset('app.js'); -echo $script->type; // 'module' vagy null - -// Audió tulajdonságok -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // időtartam másodpercben - -// Minden asset stringgé konvertálható (URL-t ad vissza) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Az olyan tulajdonságok, mint a dimenziók vagy az időtartam, csak akkor töltődnek be lustán, ha hozzáférnek hozzájuk, így a könyvtár gyors marad. - - -Assetek használata Latte sablonokban -==================================== - -A Nette Assets intuitív [Latte |latte:] integrációt biztosít tagekkel és függvényekkel. - - -`{asset}` ---------- - -Az `{asset}` tag teljes HTML elemeket renderel: - -```latte -{* Renderel: *} -{asset 'hero.jpg'} - -{* Renderel: *} -{asset 'app.js'} - -{* Renderel: *} -{asset 'style.css'} -``` - -A tag automatikusan: -- Felismeri az asset típusát és megfelelő HTML-t generál -- Tartalmazza a verziózást a gyorsítótár törléséhez -- Hozzáadja a dimenziókat a képekhez -- Beállítja a megfelelő attribútumokat (típus, média stb.) - -Ha HTML attribútumokon belül használják, csak az URL-t adja ki: - -```latte -
    - -``` - - -`n:asset` ---------- - -A HTML attribútumok teljes ellenőrzéséhez: - -```latte -{* Az n:asset attribútum kitölti a src-t, dimenziókat stb. *} -Product - -{* Bármely releváns elemmel működik *} - - - -``` - -Használj változókat és mappereket: - -```latte -{* A változók természetesen működnek *} - - -{* Mapper megadása kapcsos zárójelekkel *} - - -{* Mapper megadása tömb jelöléssel *} - -``` - - -`asset()` ---------- - -A maximális rugalmasság érdekében használd az `asset()` függvényt: - -```latte -{var $logo = asset('logo.png')} -width} height={$logo->height}> - -{* Vagy közvetlenül *} -Logo -``` - - -Opcionális assetek ------------------- - -Kezeld a hiányzó asseteket elegánsan a `{asset?}`, `n:asset?` és `tryAsset()` segítségével: - -```latte -{* Opcionális tag - semmit sem renderel, ha az asset hiányzik *} -{asset? 'optional-banner.jpg'} - -{* Opcionális attribútum - kihagyja, ha az asset hiányzik *} -Avatar - -{* Tartalék opcióval *} -{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')} -Avatar -``` - - -`{preload}` ------------ - -Javítsd az oldalbetöltési teljesítményt: - -```latte -{* A szekcióban *} -{preload 'critical.css'} -{preload 'important-font.woff2'} -{preload 'hero-image.jpg'} -``` - -Megfelelő preload linkeket generál: - -```latte - - - -``` - - -Haladó funkciók -=============== - - -Kiterjesztés automatikus felismerése ------------------------------------- - -Több formátum kezelése automatikusan: - -```neon -assets: - mapping: - images: - path: img - extension: [webp, jpg, png] # Próbálja sorrendben -``` - -Mostantól kiterjesztés nélkül is kérhetsz: - -```latte -{* Automatikusan megtalálja a logo.webp, logo.jpg vagy logo.png fájlt *} -{asset 'images:logo'} -``` - -Tökéletes a progresszív fejlesztéshez modern formátumokkal. - - -Intelligens verziózás ---------------------- - -A fájlok automatikusan verziózódnak a módosítási idő alapján: - -```latte -{asset 'style.css'} -{* Kimenet: *} -``` - -Amikor frissíted a fájlt, az időbélyeg megváltozik, ami a böngésző gyorsítótárának frissítését kényszeríti. - -Verziózás szabályozása assetenként: - -```php -// Verziózás letiltása specifikus assethez -$asset = $assets->getAsset('style.css', ['version' => false]); - -// Latte-ban -{asset 'style.css', version: false} -``` - - -Betűtípus assetek ------------------ - -A betűtípusok különleges kezelést kapnak megfelelő CORS-szal: - -```latte -{* Megfelelő preload crossorigin-nel *} -{preload 'fonts:OpenSans-Regular.woff2'} - -{* Használat CSS-ben *} - -``` - - -Egyedi mapperek -=============== - -Hozzon létre egyedi mappereket különleges igényekhez, mint például felhőtárhely vagy dinamikus generálás: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Az asset '$reference' nem található"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Regisztrálja a konfigurációban: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Használja, mint bármely más mappert: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -A `Helpers::createAssetFromUrl()` metódus automatikusan létrehozza a megfelelő asset típust a fájlkiterjesztés alapján. - - -További olvasnivalók -==================== - -- [Nette Assets: Végre egységes API a képektől a Vite-ig mindenhez |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/hu/@left-menu.texy b/assets/hu/@left-menu.texy deleted file mode 100644 index 143719af1e..0000000000 --- a/assets/hu/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Első lépések |@home] -- [Vite |vite] -- [Konfiguráció |Configuration] diff --git a/assets/hu/@meta.texy b/assets/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/assets/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/assets/hu/configuration.texy b/assets/hu/configuration.texy deleted file mode 100644 index a4c3bac847..0000000000 --- a/assets/hu/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Assets Konfiguráció -******************* - -.[perex] -A Nette Assets konfigurációs lehetőségeinek áttekintése. - - -```neon -assets: - # alapútvonal a relatív mapper útvonalak feloldásához - basePath: ... # (string) alapértelmezés szerint %wwwDir% - - # alap URL a relatív mapper URL-ek feloldásához - baseUrl: ... # (string) alapértelmezés szerint %baseUrl% - - # asset verziózás engedélyezése globálisan? - versioning: ... # (bool) alapértelmezés szerint true - - # asset mapperek definiálása - mapping: ... # (array) alapértelmezés szerint 'assets' útvonal -``` - -A `basePath` beállítja az alapértelmezett fájlrendszer könyvtárat a mapperek relatív útvonalainak feloldásához. Alapértelmezés szerint a webkönyvtárat (`%wwwDir%`) használja. - -A `baseUrl` beállítja az alapértelmezett URL prefixet a mapperek relatív URL-einek feloldásához. Alapértelmezés szerint a gyökér URL-t (`%baseUrl%`) használja. - -A `versioning` opció globálisan szabályozza, hogy a verzióparaméterek hozzáadódnak-e az asset URL-ekhez a gyorsítótár törléséhez. Az egyes mapperek felülírhatják ezt a beállítást. - - -Mapperek --------- - -A mapperek háromféleképpen konfigurálhatók: egyszerű string jelöléssel, részletes tömb jelöléssel, vagy egy szolgáltatásra való hivatkozással. - -A mapper definiálásának legegyszerűbb módja: - -```neon -assets: - mapping: - default: assets # Fájlrendszer mappert hoz létre a %wwwDir%/assets/ számára - images: img # Fájlrendszer mappert hoz létre a %wwwDir%/img/ számára - scripts: js # Fájlrendszer mappert hoz létre a %wwwDir%/js/ számára -``` - -Minden mapper létrehoz egy `FilesystemMapper`-t, amely: -- Fájlokat keres a `%wwwDir%/`-ban -- URL-eket generál, mint `%baseUrl%/` -- Örökli a globális verziózási beállítást - - -A nagyobb kontroll érdekében használd a részletes jelölést: - -```neon -assets: - mapping: - images: - # könyvtár, ahol a fájlok tárolódnak - path: ... # (string) opcionális, alapértelmezés szerint '' - - # URL prefix a generált linkekhez - url: ... # (string) opcionális, alapértelmezés szerint path - - # verziózás engedélyezése ehhez a mapperhez? - versioning: ... # (bool) opcionális, örökli a globális beállítást - - # automatikus kiterjesztés(ek) hozzáadása fájlok keresésekor - extension: ... # (string|array) opcionális, alapértelmezés szerint null -``` - -A konfigurációs értékek feloldásának megértése: - -Útvonal feloldás: - - A relatív útvonalak a `basePath`-ból (vagy `%wwwDir%`, ha a `basePath` nincs beállítva) oldódnak fel - - Az abszolút útvonalak változatlanul használatosak - -URL feloldás: - - A relatív URL-ek a `baseUrl`-ből (vagy `%baseUrl%`, ha a `baseUrl` nincs beállítva) oldódnak fel - - Az abszolút URL-ek (sémával vagy `//`) változatlanul használatosak - - Ha az `url` nincs megadva, akkor a `path` értékét használja - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Relatív útvonal és URL - images: - path: img # Feloldva: /var/www/project/www/img - url: images # Feloldva: https://example.com/assets/images - - # Abszolút útvonal és URL - uploads: - path: /var/shared/uploads # Változatlanul használva: /var/shared/uploads - url: https://cdn.example.com # Változatlanul használva: https://cdn.example.com - - # Csak az útvonal megadva - styles: - path: css # Útvonal: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Egyedi mapperek ---------------- - -Egyedi mapperek esetén hivatkozzon vagy definiáljon egy szolgáltatást: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -A Vite mapperhez csak a `type: vite` hozzáadása szükséges. Ez a konfigurációs lehetőségek teljes listája: - -```neon -assets: - mapping: - default: - # mapper típus (kötelező a Vite-hez) - type: vite # (string) kötelező, 'vite' kell legyen - - # Vite build kimeneti könyvtár - path: ... # (string) opcionális, alapértelmezés szerint '' - - # URL prefix a beépített assetekhez - url: ... # (string) opcionális, alapértelmezés szerint path - - # Vite manifest fájl helye - manifest: ... # (string) opcionális, alapértelmezés szerint /.vite/manifest.json - - # Vite dev szerver konfiguráció - devServer: ... # (bool|string) opcionális, alapértelmezés szerint true - - # verziózás a public könyvtár fájljaihoz - versioning: ... # (bool) opcionális, örökli a globális beállítást - - # automatikus kiterjesztés a public könyvtár fájljaihoz - extension: ... # (string|array) opcionális, alapértelmezés szerint null -``` - -A `devServer` opció szabályozza, hogyan töltődnek be az assetek fejlesztés közben: - -- `true` (alapértelmezett) - Automatikusan felismeri a Vite dev szervert az aktuális hoston és porton. Ha a dev szerver fut **és az alkalmazásod debug módban van**, az assetek onnan töltődnek be hot module replacement támogatással. Ha a dev szerver nem fut, az assetek a buildelt fájlokból töltődnek be a public könyvtárból. -- `false` - Teljesen letiltja a dev szerver integrációt. Az assetek mindig a buildelt fájlokból töltődnek be. -- Egyedi URL (pl. `https://localhost:5173`) - Manuálisan adja meg a dev szerver URL-jét, beleértve a protokollt és a portot. Hasznos, ha a dev szerver más hoston vagy porton fut. - -Az `versioning` és `extension` opciók csak a Vite public könyvtárában lévő olyan fájlokra vonatkoznak, amelyeket a Vite nem dolgoz fel. - - -Manuális konfiguráció ---------------------- - -Ha nem használja a Nette DI-t, konfigurálja a mappereket manuálisan: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Fájlrendszer mapper hozzáadása -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Vite mapper hozzáadása -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/hu/vite.texy b/assets/hu/vite.texy deleted file mode 100644 index eefb77fdaa..0000000000 --- a/assets/hu/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Vite Integráció -*************** - -
    - -A modern JavaScript alkalmazások kifinomult build eszközöket igényelnek. A Nette Assets első osztályú integrációt biztosít a [Vite |https://vitejs.dev/] nevű, következő generációs frontend build eszközzel. Villámgyors fejlesztést érhet el Hot Module Replacement (HMR) funkcióval és optimalizált éles build-ekkel, nulla konfigurációs gonddal. - -- **Nulla konfiguráció** - automatikus híd a Vite és a PHP sablonok között -- **Teljes függőségkezelés** - egyetlen tag kezeli az összes assetet -- **Hot Module Replacement** - azonnali JavaScript és CSS frissítések -- **Optimalizált éles build-ek** - kód felosztás és tree shaking - -
    - - -A Nette Assets zökkenőmentesen integrálódik a Vite-tel, így az összes előnyét élvezheti, miközben a sablonokat a szokásos módon írja. - - -Vite beállítása -=============== - -Állítsuk be a Vite-et lépésről lépésre. Ne aggódj, ha még új vagy a build eszközök terén - mindent elmagyarázunk! - - -1. lépés: Vite telepítése -------------------------- - -Először telepítsd a Vite-et és a Nette plugint a projektedbe: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Ez telepíti a Vite-et és egy speciális plugint, amely segít a Vite-nek tökéletesen működni a Nette-tel. - - -2. lépés: Projektstruktúra --------------------------- - -A standard megközelítés az, hogy a forrás asset fájlokat a projekt gyökerében lévő `assets/` mappába helyezzük, a fordított verziókat pedig a `www/assets/` mappába: - -/--pre -web-project/ -├── assets/ ← forrásfájlok (SCSS, TypeScript, forrásképek) -│ ├── public/ ← statikus fájlok (változatlanul másolva) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← fő belépési pont -│ └── style.css ← a stíluslapjaid -└── www/ ← nyilvános könyvtár (dokumentum gyökér) - ├── assets/ ← ide kerülnek a fordított fájlok - └── index.php -\-- - -Az `assets/` mappa tartalmazza a forrásfájljaidat - a kódot, amit írsz. A Vite feldolgozza ezeket a fájlokat, és a fordított verziókat a `www/assets/` mappába helyezi. - - -3. lépés: Vite konfigurálása ----------------------------- - -Hozzon létre egy `vite.config.ts` fájlt a projekt gyökerében. Ez a fájl megmondja a Vite-nek, hol találja a forrásfájlokat és hova tegye a fordított fájlokat. - -A Nette Vite plugin intelligens alapértelmezett beállításokkal érkezik, amelyek leegyszerűsítik a konfigurációt. Feltételezi, hogy a frontend forrásfájlok az `assets/` könyvtárban vannak (`root` opció), és a fordított fájlok a `www/assets/` mappába kerülnek (`outDir` opció). Csak a [belépési pontot |#Entry Points] kell megadnia: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Ha másik könyvtárnevet szeretne megadni az assetek buildeléséhez, néhány opciót módosítania kell: - -```js -export default defineConfig({ - root: 'assets', // forrás assetek gyökérkönyvtára - - build: { - outDir: '../www/assets', // ahova a fordított fájlok kerülnek - }, - - // ... egyéb konfiguráció ... -}); -``` - -.[note] -Az `outDir` útvonal a `root`-hoz képest relatív, ezért van `../` az elején. - - -4. lépés: Nette konfigurálása ------------------------------ - -Mondja meg a Nette Assets-nek a Vite-ről a `common.neon` fájlban: - -```neon -assets: - mapping: - default: - type: vite # megmondja a Nette-nek, hogy a ViteMapper-t használja - path: assets -``` - - -5. lépés: Szkriptek hozzáadása ------------------------------- - -Add hozzá ezeket a szkripteket a `package.json` fájlhoz: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Most már tudsz: -- `npm run dev` - fejlesztői szerver indítása hot reloading-gal -- `npm run build` - optimalizált éles fájlok létrehozása - - -Belépési pontok -=============== - -A **belépési pont** az a fő fájl, ahol az alkalmazásod elindul. Ebből a fájlból importálsz más fájlokat (CSS, JavaScript modulok, képek), létrehozva egy függőségi fát. A Vite követi ezeket az importokat és mindent egybe csomagol. - -Példa belépési pont `assets/app.js`: - -```js -// Stílusok importálása -import './style.css' - -// JavaScript modulok importálása -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Alkalmazás inicializálása -netteForms.initOnLoad(); -naja.initialize(); -``` - -A sablonban a belépési pontot a következőképpen illesztheti be: - -```latte -{asset 'app.js'} -``` - -A Nette Assets automatikusan generálja az összes szükséges HTML taget - JavaScript, CSS és bármely más függőség. - - -Több belépési pont ------------------- - -Nagyobb alkalmazásoknak gyakran külön belépési pontokra van szükségük: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // nyilvános oldalak - 'admin.js', // admin panel - ], - }), - ], -}); -``` - -Használd őket különböző sablonokban: - -```latte -{* Nyilvános oldalakon *} -{asset 'app.js'} - -{* Admin panelen *} -{asset 'admin.js'} -``` - - -Fontos: Forrás vs. fordított fájlok ------------------------------------ - -Fontos megérteni, hogy éles környezetben csak a következőket töltheti be: - -1. A `entry` fájlban definiált **belépési pontok** -2. Fájlok az `assets/public/` könyvtárból - -Nem tölthet be `{asset}` segítségével tetszőleges fájlokat az `assets/` könyvtárból - csak azokat az asseteket, amelyekre JavaScript vagy CSS fájlok hivatkoznak. Ha a fájlra sehol sem hivatkoznak, az nem lesz fordítva. Ha más asseteket is tudatosítani szeretne a Vite-tel, áthelyezheti őket a [public mappa |#public folder]-be. - -Kérjük, vegye figyelembe, hogy alapértelmezés szerint a Vite az összes 4KB-nál kisebb assetet beágyazza, így ezekre a fájlokra nem hivatkozhat közvetlenül. (Lásd [Vite dokumentáció |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Ez működik - ez egy belépési pont *} -{asset 'app.js'} - -{* ✓ Ez működik - az assets/public/ mappában van *} -{asset 'favicon.ico'} - -{* ✗ Ez nem fog működni - véletlenszerű fájl az assets/ mappában *} -{asset 'components/button.js'} -``` - - -Fejlesztői mód -============== - -A fejlesztői mód teljesen opcionális, de jelentős előnyökkel jár, ha engedélyezve van. A fő előny a **Hot Module Replacement (HMR)** - azonnal láthatja a változásokat az alkalmazás állapotának elvesztése nélkül, ami sokkal simábbá és gyorsabbá teszi a fejlesztési élményt. - -A Vite egy modern build eszköz, amely hihetetlenül gyorssá teszi a fejlesztést. A hagyományos bundlerekkel ellentétben a Vite közvetlenül a böngészőnek szolgálja ki a kódot fejlesztés közben, ami azt jelenti, hogy azonnali szerverindítás történik, függetlenül a projekt méretétől, és villámgyors frissítések. - - -Fejlesztői szerver indítása ---------------------------- - -Futtassa a fejlesztői szervert: - -```shell -npm run dev -``` - -Látni fogja: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Tartsa nyitva ezt a terminált a fejlesztés során. - -A Nette Vite plugin automatikusan felismeri, ha: -1. A Vite dev szerver fut -2. A Nette alkalmazás debug módban van - -Ha mindkét feltétel teljesül, a Nette Assets a Vite dev szerverről tölti be a fájlokat a fordított könyvtár helyett: - -```latte -{asset 'app.js'} -{* Fejlesztésben: *} -{* Éles környezetben: *} -``` - -Nincs szükség konfigurációra - egyszerűen működik! - - -Különböző domaineken való munka -------------------------------- - -Ha a fejlesztői szervered nem `localhost`-on (például `myapp.local`-on) fut, akkor CORS (Cross-Origin Resource Sharing) problémákkal találkozhatsz. A CORS egy biztonsági funkció a webböngészőkben, amely alapértelmezés szerint blokkolja a különböző domainek közötti kéréseket. Amikor a PHP alkalmazásod `myapp.local`-on fut, de a Vite `localhost:5173`-on, a böngésző ezeket különböző domaineknek tekinti, és blokkolja a kéréseket. - -Két lehetőséged van ennek megoldására: - -**1. opció: CORS konfigurálása** - -A legegyszerűbb megoldás, ha engedélyezi a cross-origin kéréseket a PHP alkalmazásából: - -```js -export default defineConfig({ - // ... egyéb konfiguráció ... - - server: { - cors: { - origin: 'http://myapp.local', // a PHP alkalmazásod URL-je - }, - }, -}); -``` -**2. opció: Futtassa a Vite-et a domainjén** - -A másik megoldás, ha a Vite-et ugyanazon a domainen futtatja, mint a PHP alkalmazását. - -```js -export default defineConfig({ - // ... egyéb konfiguráció ... - - server: { - host: 'myapp.local', // ugyanaz, mint a PHP alkalmazásod - }, -}); -``` - -Valójában ebben az esetben is konfigurálnia kell a CORS-t, mert a dev szerver ugyanazon a hostnéven, de más porton fut. Azonban ebben az esetben a CORS-t a Nette Vite plugin automatikusan konfigurálja. - - -HTTPS fejlesztés ----------------- - -Ha HTTPS-en fejlesztesz, tanúsítványokra lesz szükséged a Vite fejlesztői szerveredhez. A legegyszerűbb módja egy olyan plugin használata, amely automatikusan generál tanúsítványokat: - -```shell -npm install -D vite-plugin-mkcert -``` - -Így konfigurálhatja a `vite.config.ts` fájlban: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... egyéb konfiguráció ... - - plugins: [ - mkcert(), // automatikusan generál tanúsítványokat és engedélyezi a https-t - nette(), - ], -}); -``` - -Ne feledje, hogy ha a CORS konfigurációt használja (az 1. opciót fentebb), akkor frissítenie kell az origin URL-t `https://` használatára `http://` helyett. - - -Éles build-ek -============= - -Hozzon létre optimalizált éles fájlokat: - -```shell -npm run build -``` - -A Vite: -- Minifikálja az összes JavaScriptet és CSS-t -- Optimális részekre osztja a kódot -- Hash-elt fájlneveket generál a gyorsítótár törléséhez -- Létrehoz egy manifest fájlt a Nette Assets számára - -Példa kimenet: - -``` -www/assets/ -├── app-4f3a2b1c.js # A fő JavaScripted (minifikált) -├── app-7d8e9f2a.css # Kinyert CSS (minifikált) -├── vendor-8c4b5e6d.js # Megosztott függőségek -└── .vite/ - └── manifest.json # Leképezés a Nette Assets számára -``` - -A hash-elt fájlnevek biztosítják, hogy a böngészők mindig a legújabb verziót töltsék be. - - -Nyilvános mappa -=============== - -Az `assets/public/` könyvtárban lévő fájlok feldolgozás nélkül másolódnak a kimenetbe: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Hivatkozzon rájuk normálisan: - -```latte -{* Ezek a fájlok változatlanul másolódnak *} - - -``` - -Nyilvános fájlokhoz használhatja a FilesystemMapper funkcióit: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Először a WebP-t próbálja - versioning: true # Gyorsítótár törlés hozzáadása -``` - -A `vite.config.ts` konfigurációban a `publicDir` opcióval módosíthatja a nyilvános mappát. - - -Dinamikus importok -================== - -A Vite automatikusan felosztja a kódot az optimális betöltés érdekében. A dinamikus importok lehetővé teszik, hogy a kódot csak akkor töltse be, amikor arra ténylegesen szükség van, csökkentve az kezdeti csomagméretet: - -```js -// Nehéz komponensek betöltése igény szerint -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -A dinamikus importok külön chunkokat hoznak létre, amelyek csak akkor töltődnek be, amikor ténylegesen szükség van rájuk. Ezt "kód felosztásnak" nevezik, és ez a Vite egyik legerősebb funkciója. Amikor dinamikus importokat használ, a Vite automatikusan külön JavaScript fájlokat hoz létre minden dinamikusan importált modulhoz. - -Az `{asset 'app.js'}` tag **nem** tölti be automatikusan ezeket a dinamikus chunkokat. Ez szándékos viselkedés - nem akarunk olyan kódot letölteni, amelyet esetleg soha nem használnak. A chunkok csak akkor töltődnek le, amikor a dinamikus import végrehajtásra kerül. - -Azonban, ha tudja, hogy bizonyos dinamikus importok kritikusak, és hamarosan szükség lesz rájuk, előtöltheti őket: - -```latte -{* Fő belépési pont *} -{asset 'app.js'} - -{* Kritikus dinamikus importok előtöltése *} -{preload 'components/chart.js'} -``` - -Ez azt mondja a böngészőnek, hogy töltse le a diagramkomponenst a háttérben, így azonnal készen áll, amikor szükség van rá. - - -TypeScript támogatás -==================== - -A TypeScript azonnal működik: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Hivatkozzon a TypeScript fájlokra normálisan: - -```latte -{asset 'main.ts'} -``` - -A teljes TypeScript támogatáshoz telepítse: - -```shell -npm install -D typescript -``` - - -További Vite konfiguráció -========================= - -Íme néhány hasznos Vite konfigurációs opció részletes magyarázattal: - -```js -export default defineConfig({ - // A forrás asseteket tartalmazó gyökérkönyvtár - root: 'assets', - - // Az a mappa, amelynek tartalma változatlanul másolódik a kimeneti könyvtárba - // Alapértelmezett: 'public' (a 'root'-hoz képest relatív) - publicDir: 'public', - - build: { - // Hova kerüljenek a fordított fájlok (a 'root'-hoz képest relatív) - outDir: '../www/assets', - - // Ürítse ki a kimeneti könyvtárat a buildelés előtt? - // Hasznos a régi fájlok eltávolításához az előző buildekből - emptyOutDir: true, - - // Alkönvtár az outDir-en belül a generált chunkok és assetek számára - // Ez segít a kimeneti struktúra rendezésében - assetsDir: 'static', - - rollupOptions: { - // Belépési pont(ok) - lehet egyetlen fájl vagy fájltömb - // Minden belépési pont külön csomaggá válik - input: [ - 'app.js', // fő alkalmazás - 'admin.js', // admin panel - ], - }, - }, - - server: { - // Host, amelyhez a dev szerver kötődik - // Használja a '0.0.0.0'-t a hálózaton való közzétételhez - host: 'localhost', - - // Port a dev szerverhez - port: 5173, - - // CORS konfiguráció a cross-origin kérésekhez - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // CSS forrástérképek engedélyezése fejlesztésben - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -Ennyi! Most már van egy modern build rendszered, amely integrálva van a Nette Assets-szel. diff --git a/assets/pt/@home.texy b/assets/pt/@home.texy deleted file mode 100644 index 23120a41f0..0000000000 --- a/assets/pt/@home.texy +++ /dev/null @@ -1,432 +0,0 @@ -Nette Assets -************ - -
    - -Cansado de gerenciar manualmente arquivos estáticos em suas aplicações web? Esqueça a codificação de caminhos, a invalidação de cache ou a preocupação com o versionamento de arquivos. Nette Assets transforma a maneira como você trabalha com imagens, folhas de estilo, scripts e outros recursos estáticos. - -- **Versionamento inteligente** garante que os navegadores sempre carreguem os arquivos mais recentes -- **Detecção automática** de tipos e dimensões de arquivos -- **Integração perfeita com Latte** com tags intuitivas -- **Arquitetura flexível** suportando sistemas de arquivos, CDNs e Vite -- **Carregamento preguiçoso** para desempenho ideal - -
    - - -Por que Nette Assets? -===================== - -Trabalhar com arquivos estáticos geralmente significa código repetitivo e propenso a erros. Você constrói URLs manualmente, adiciona parâmetros de versão para cache busting e lida com diferentes tipos de arquivos de forma diferente. Isso leva a um código como: - -```latte -Logo - -``` - -Com Nette Assets, toda essa complexidade desaparece: - -```latte -{* Tudo automatizado - URL, versionamento, dimensões *} - - - -{* Ou simplesmente *} -{asset 'css/style.css'} -``` - -É isso! A biblioteca automaticamente: -- Adiciona parâmetros de versão com base na hora de modificação do arquivo -- Detecta dimensões da imagem e as inclui no HTML -- Gera o elemento HTML correto para cada tipo de arquivo -- Lida com ambientes de desenvolvimento e produção - - -Instalação -========== - -Instale Nette Assets usando [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Requer PHP 8.1 ou superior e funciona perfeitamente com Nette Framework, mas também pode ser usado de forma autônoma. - - -Primeiros Passos -================ - -Nette Assets funciona de imediato com configuração zero. Coloque seus arquivos estáticos no diretório `www/assets/` e comece a usá-los: - -```latte -{* Exibe uma imagem com dimensões automáticas *} -{asset 'logo.png'} - -{* Inclui uma folha de estilo com versionamento *} -{asset 'style.css'} - -{* Carrega um módulo JavaScript *} -{asset 'app.js'} -``` - -Para mais controle sobre o HTML gerado, use o atributo `n:asset` ou a função `asset()`. - - -Como Funciona -============= - -Nette Assets é construído em torno de três conceitos centrais que o tornam poderoso e simples de usar: - - -Assets - Seus Arquivos Inteligentes ------------------------------------ - -Um **asset** representa qualquer arquivo estático em sua aplicação. Cada arquivo se torna um objeto com propriedades somente leitura úteis: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Diferentes tipos de arquivo fornecem diferentes propriedades: -- **Imagens**: largura, altura, texto alternativo, carregamento preguiçoso -- **Scripts**: tipo de módulo, hashes de integridade, crossorigin -- **Folhas de estilo**: media queries, integridade -- **Áudio/Vídeo**: duração, dimensões -- **Fontes**: pré-carregamento adequado com CORS - -A biblioteca detecta automaticamente os tipos de arquivo e cria a classe de asset apropriada. - - -Mappers - De Onde Vêm os Arquivos ---------------------------------- - -Um **mapper** sabe como encontrar arquivos e criar URLs para eles. Você pode ter vários mappers para diferentes propósitos - arquivos locais, CDN, armazenamento em nuvem ou ferramentas de construção (cada um deles tem um nome). O `FilesystemMapper` integrado lida com arquivos locais, enquanto o `ViteMapper` se integra com ferramentas de construção modernas. - -Mappers são definidos na [configuração|Configuration]. - - -Registry - Sua Interface Principal ----------------------------------- - -O **registry** gerencia todos os mappers e fornece a API principal: - -```php -// Injeta o registry em seu serviço -public function __construct( - private Nette\Assets\Registry $assets -) {} - -// Obtém assets de diferentes mappers -$logo = $this->assets->getAsset('images:logo.png'); // mapper 'image' -$app = $this->assets->getAsset('app:main.js'); // mapper 'app' -$style = $this->assets->getAsset('style.css'); // usa o mapper padrão -``` - -O registry seleciona automaticamente o mapper correto e armazena os resultados em cache para desempenho. - - -Trabalhando com Assets em PHP -============================= - -O Registry fornece dois métodos para recuperar assets: - -```php -// Lança Nette\Assets\AssetNotFoundException se o arquivo não existir -$logo = $assets->getAsset('logo.png'); - -// Retorna null se o arquivo não existir -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Especificando Mappers ---------------------- - -Você pode escolher explicitamente qual mapper usar: - -```php -// Usa o mapper padrão -$file = $assets->getAsset('document.pdf'); - -// Usa um mapper específico com prefixo -$image = $assets->getAsset('images:photo.jpg'); - -// Usa um mapper específico com sintaxe de array -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Propriedades e Tipos de Asset ------------------------------ - -Cada tipo de asset fornece propriedades somente leitura relevantes: - -```php -// Propriedades da imagem -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Propriedades do script -$script = $assets->getAsset('app.js'); -echo $script->type; // 'module' ou null - -// Propriedades de áudio -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // duração em segundos - -// Todos os assets podem ser convertidos para string (retorna URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Propriedades como dimensões ou duração são carregadas preguiçosamente apenas quando acessadas, mantendo a biblioteca rápida. - - -Usando Assets em Templates Latte -================================ - -Nette Assets fornece integração intuitiva com [Latte|latte:] com tags e funções. - - -`{asset}` ---------- - -A tag `{asset}` renderiza elementos HTML completos: - -```latte -{* Renderiza: *} -{asset 'hero.jpg'} - -{* Renderiza: *} -{asset 'app.js'} - -{* Renderiza: *} -{asset 'style.css'} -``` - -A tag automaticamente: -- Detecta o tipo de asset e gera o HTML apropriado -- Inclui versionamento para cache busting -- Adiciona dimensões para imagens -- Define atributos corretos (tipo, mídia, etc.) - -Quando usado dentro de atributos HTML, ele gera apenas a URL: - -```latte -
    - -``` - - -`n:asset` ---------- - -Para controle total sobre os atributos HTML: - -```latte -{* O atributo n:asset preenche src, dimensões, etc. *} -Product - -{* Funciona com qualquer elemento relevante *} - - - -``` - -Use variáveis e mappers: - -```latte -{* Variáveis funcionam naturalmente *} - - -{* Especifique o mapper com chaves *} - - -{* Especifique o mapper com notação de array *} - -``` - - -`asset()` ---------- - -Para máxima flexibilidade, use a função `asset()`: - -```latte -{var $logo = asset('logo.png')} -width} height={$logo->height}> - -{* Ou diretamente *} -Logo -``` - - -Assets Opcionais ----------------- - -Lide com assets ausentes graciosamente com `{asset?}`, `n:asset?` e `tryAsset()`: - -```latte -{* Tag opcional - não renderiza nada se o asset estiver ausente *} -{asset? 'optional-banner.jpg'} - -{* Atributo opcional - ignora se o asset estiver ausente *} -Avatar - -{* Com fallback *} -{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')} -Avatar -``` - - -`{preload}` ------------ - -Melhore o desempenho de carregamento da página: - -```latte -{* Na sua seção *} -{preload 'critical.css'} -{preload 'important-font.woff2'} -{preload 'hero-image.jpg'} -``` - -Gera links de pré-carregamento apropriados: - -```latte - - - -``` - - -Recursos Avançados -================== - - -Detecção Automática de Extensão -------------------------------- - -Lida com múltiplos formatos automaticamente: - -```neon -assets: - mapping: - images: - path: img - extension: [webp, jpg, png] # Tenta nesta ordem -``` - -Agora você pode requisitar sem extensão: - -```latte -{* Encontra logo.webp, logo.jpg ou logo.png automaticamente *} -{asset 'images:logo'} -``` - -Perfeito para aprimoramento progressivo com formatos modernos. - - -Versionamento Inteligente -------------------------- - -Os arquivos são automaticamente versionados com base na hora de modificação: - -```latte -{asset 'style.css'} -{* Saída: *} -``` - -Quando você atualiza o arquivo, o timestamp muda, forçando a atualização do cache do navegador. - -Controle o versionamento por asset: - -```php -// Desativa o versionamento para um asset específico -$asset = $assets->getAsset('style.css', ['version' => false]); - -// No Latte -{asset 'style.css', version: false} -``` - - -Assets de Fonte ---------------- - -As fontes recebem tratamento especial com CORS adequado: - -```latte -{* Pré-carregamento adequado com crossorigin *} -{preload 'fonts:OpenSans-Regular.woff2'} - -{* Uso em CSS *} - -``` - - -Mappers Personalizados -====================== - -Crie mappers personalizados para necessidades especiais como armazenamento em nuvem ou geração dinâmica: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Registre na configuração: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Use como qualquer outro mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -O método `Helpers::createAssetFromUrl()` cria automaticamente o tipo de asset correto com base na extensão do arquivo. - - -Leitura adicional -================= - -- [Nette Assets: Finalmente uma API unificada para tudo, desde imagens até o Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/pt/@left-menu.texy b/assets/pt/@left-menu.texy deleted file mode 100644 index 67ea06e0d2..0000000000 --- a/assets/pt/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Primeiros Passos |@home] -- [Vite |vite] -- [Configuração |Configuration] diff --git a/assets/pt/@meta.texy b/assets/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/assets/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/assets/pt/configuration.texy b/assets/pt/configuration.texy deleted file mode 100644 index 7bca5174a1..0000000000 --- a/assets/pt/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Configuração de Assets -********************** - -.[perex] -Visão geral das opções de configuração para Nette Assets. - - -```neon -assets: - # caminho base para resolver caminhos de mapper relativos - basePath: ... # (string) padrão para %wwwDir% - - # URL base para resolver URLs de mapper relativas - baseUrl: ... # (string) padrão para %baseUrl% - - # habilitar versionamento de asset globalmente? - versioning: ... # (bool) padrão para true - - # define os mappers de asset - mapping: ... # (array) padrão para o caminho 'assets' -``` - -O `basePath` define o diretório padrão do sistema de arquivos para resolver caminhos relativos em mappers. Por padrão, ele usa o diretório web (`%wwwDir%`). - -A `baseUrl` define o prefixo de URL padrão para resolver URLs relativas em mappers. Por padrão, ele usa a URL raiz (`%baseUrl%`). - -A opção `versioning` controla globalmente se os parâmetros de versão são adicionados às URLs dos assets para cache busting. Mappers individuais podem substituir essa configuração. - - -Mappers -------- - -Mappers podem ser configurados de três maneiras: notação de string simples, notação de array detalhada ou como uma referência a um serviço. - -A maneira mais simples de definir um mapper: - -```neon -assets: - mapping: - default: assets # Cria um mapper de sistema de arquivos para %wwwDir%/assets/ - images: img # Cria um mapper de sistema de arquivos para %wwwDir%/img/ - scripts: js # Cria um mapper de sistema de arquivos para %wwwDir%/js/ -``` - -Cada mapper cria um `FilesystemMapper` que: -- Procura arquivos em `%wwwDir%/` -- Gera URLs como `%baseUrl%/` -- Herda a configuração de versionamento global - - -Para mais controle, use a notação detalhada: - -```neon -assets: - mapping: - images: - # diretório onde os arquivos são armazenados - path: ... # (string) opcional, padrão para '' - - # prefixo de URL para links gerados - url: ... # (string) opcional, padrão para path - - # habilitar versionamento para este mapper? - versioning: ... # (bool) opcional, herda a configuração global - - # adicionar automaticamente extensão(ões) ao procurar arquivos - extension: ... # (string|array) opcional, padrão para null -``` - -Entendendo como os valores de configuração são resolvidos: - -Resolução de Caminho: - - Caminhos relativos são resolvidos a partir de `basePath` (ou `%wwwDir%` se `basePath` não estiver definido) - - Caminhos absolutos são usados como estão - -Resolução de URL: - - URLs relativas são resolvidas a partir de `baseUrl` (ou `%baseUrl%` se `baseUrl` não estiver definido) - - URLs absolutas (com esquema ou `//`) são usadas como estão - - Se `url` não for especificado, ele usa o valor de `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Caminho e URL relativos - images: - path: img # Resolvido para: /var/www/project/www/img - url: images # Resolvido para: https://example.com/assets/images - - # Caminho e URL absolutos - uploads: - path: /var/shared/uploads # Usado como está: /var/shared/uploads - url: https://cdn.example.com # Usado como está: https://cdn.example.com - - # Apenas o caminho especificado - styles: - path: css # Caminho: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Mappers Personalizados ----------------------- - -Para mappers personalizados, faça referência ou defina um serviço: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -O mapper Vite exige apenas que você adicione `type: vite`. Esta é uma lista completa de opções de configuração: - -```neon -assets: - mapping: - default: - # tipo de mapper (obrigatório para Vite) - type: vite # (string) obrigatório, deve ser 'vite' - - # diretório de saída de construção do Vite - path: ... # (string) opcional, padrão para '' - - # prefixo de URL para assets construídos - url: ... # (string) opcional, padrão para path - - # localização do arquivo de manifesto do Vite - manifest: ... # (string) opcional, padrão para /.vite/manifest.json - - # configuração do servidor de desenvolvimento do Vite - devServer: ... # (bool|string) opcional, padrão para true - - # versionamento para arquivos do diretório público - versioning: ... # (bool) opcional, herda a configuração global - - # auto-extensão para arquivos do diretório público - extension: ... # (string|array) opcional, padrão para null -``` - -A opção `devServer` controla como os assets são carregados durante o desenvolvimento: - -- `true` (padrão) - Detecta automaticamente o servidor de desenvolvimento Vite no host e porta atuais. Se o servidor de desenvolvimento estiver em execução **e sua aplicação estiver em modo de depuração**, os assets são carregados dele com suporte a hot module replacement. Se o servidor de desenvolvimento não estiver em execução, os assets são carregados dos arquivos construídos no diretório público. -- `false` - Desativa completamente a integração do servidor de desenvolvimento. Os assets são sempre carregados dos arquivos construídos. -- URL personalizada (por exemplo, `https://localhost:5173`) - Especifique manualmente a URL do servidor de desenvolvimento, incluindo protocolo e porta. Útil quando o servidor de desenvolvimento é executado em um host ou porta diferente. - -As opções `versioning` e `extension` aplicam-se apenas a arquivos no diretório público do Vite que não são processados pelo Vite. - - -Configuração Manual -------------------- - -Quando não estiver usando Nette DI, configure os mappers manualmente: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Adiciona o mapper de sistema de arquivos -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Adiciona o mapper Vite -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/pt/vite.texy b/assets/pt/vite.texy deleted file mode 100644 index 542aae02f8..0000000000 --- a/assets/pt/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Integração com Vite -******************* - -
    - -Aplicações JavaScript modernas exigem ferramentas de construção sofisticadas. Nette Assets oferece integração de primeira classe com [Vite |https://vitejs.dev/], a ferramenta de construção frontend de próxima geração. Obtenha desenvolvimento ultrarrápido com Hot Module Replacement (HMR) e construções de produção otimizadas com zero complicações de configuração. - -- **Zero configuração** - ponte automática entre Vite e templates PHP -- **Gerenciamento completo de dependências** - uma tag lida com todos os assets -- **Hot Module Replacement** - atualizações instantâneas de JavaScript e CSS -- **Construções de produção otimizadas** - code splitting e tree shaking - -
    - - -Nette Assets se integra perfeitamente com Vite, para que você obtenha todos esses benefícios enquanto escreve seus templates como de costume. - - -Configurando o Vite -=================== - -Vamos configurar o Vite passo a passo. Não se preocupe se você é novo em ferramentas de construção - vamos explicar tudo! - - -Passo 1: Instalar o Vite ------------------------- - -Primeiro, instale o Vite e o plugin Nette em seu projeto: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Isso instala o Vite e um plugin especial que ajuda o Vite a funcionar perfeitamente com o Nette. - - -Passo 2: Estrutura do Projeto ------------------------------ - -A abordagem padrão é colocar os arquivos de asset de origem em uma pasta `assets/` na raiz do seu projeto, e as versões compiladas em `www/assets/`: - -/--pre -web-project/ -├── assets/ ← arquivos de origem (SCSS, TypeScript, imagens de origem) -│ ├── public/ ← arquivos estáticos (copiados como estão) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← ponto de entrada principal -│ └── style.css ← seus estilos -└── www/ ← diretório público (document root) - ├── assets/ ← arquivos compilados irão para cá - └── index.php -\-- - -A pasta `assets/` contém seus arquivos de origem - o código que você escreve. O Vite processará esses arquivos e colocará as versões compiladas em `www/assets/`. - - -Passo 3: Configurar o Vite --------------------------- - -Crie um arquivo `vite.config.ts` na raiz do seu projeto. Este arquivo informa ao Vite onde encontrar seus arquivos de origem e onde colocar os compilados. - -O plugin Nette Vite vem com padrões inteligentes que simplificam a configuração. Ele assume que seus arquivos de origem frontend estão no diretório `assets/` (opção `root`) e os arquivos compilados vão para `www/assets/` (opção `outDir`). Você só precisa especificar o [ponto de entrada|#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Se você quiser especificar outro nome de diretório para construir seus assets, precisará alterar algumas opções: - -```js -export default defineConfig({ - root: 'assets', // diretório raiz dos assets de origem - - build: { - outDir: '../www/assets', // onde os arquivos compilados vão - }, - - // ... outras configurações ... -}); -``` - -.[note] -O caminho `outDir` é considerado relativo a `root`, por isso há `../` no início. - - -Passo 4: Configurar o Nette ---------------------------- - -Informe ao Nette Assets sobre o Vite em seu `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # informa ao Nette para usar o ViteMapper - path: assets -``` - - -Passo 5: Adicionar scripts --------------------------- - -Adicione estes scripts ao seu `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Agora você pode: -- `npm run dev` - iniciar o servidor de desenvolvimento com hot reloading -- `npm run build` - criar arquivos de produção otimizados - - -Pontos de Entrada -================= - -Um **ponto de entrada** é o arquivo principal onde sua aplicação começa. A partir deste arquivo, você importa outros arquivos (CSS, módulos JavaScript, imagens), criando uma árvore de dependências. O Vite segue essas importações e agrupa tudo. - -Exemplo de ponto de entrada `assets/app.js`: - -```js -// Importa estilos -import './style.css' - -// Importa módulos JavaScript -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Inicializa sua aplicação -netteForms.initOnLoad(); -naja.initialize(); -``` - -No template você pode inserir um ponto de entrada da seguinte forma: - -```latte -{asset 'app.js'} -``` - -Nette Assets gera automaticamente todas as tags HTML necessárias - JavaScript, CSS e quaisquer outras dependências. - - -Múltiplos Pontos de Entrada ---------------------------- - -Aplicações maiores geralmente precisam de pontos de entrada separados: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // páginas públicas - 'admin.js', // painel de administração - ], - }), - ], -}); -``` - -Use-os em diferentes templates: - -```latte -{* Em páginas públicas *} -{asset 'app.js'} - -{* No painel de administração *} -{asset 'admin.js'} -``` - - -Importante: Arquivos de Origem vs. Compilados ---------------------------------------------- - -É crucial entender que em produção você só pode carregar: - -1. **Pontos de entrada** definidos em `entry` -2. **Arquivos do diretório `assets/public/`** - -Você **não pode** carregar usando `{asset}` arquivos arbitrários de `assets/` - apenas assets referenciados por arquivos JavaScript ou CSS. Se seu arquivo não for referenciado em nenhum lugar, ele não será compilado. Se você quiser que o Vite esteja ciente de outros assets, você pode movê-los para a [pasta pública|#Public Folder]. - -Observe que, por padrão, o Vite incorporará todos os assets menores que 4KB, então você não poderá referenciar esses arquivos diretamente. (Consulte a [documentação do Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Isso funciona - é um ponto de entrada *} -{asset 'app.js'} - -{* ✓ Isso funciona - está em assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Isso não funcionará - arquivo aleatório em assets/ *} -{asset 'components/button.js'} -``` - - -Modo de Desenvolvimento -======================= - -O modo de desenvolvimento é completamente opcional, mas oferece benefícios significativos quando ativado. A principal vantagem é o **Hot Module Replacement (HMR)** - veja as mudanças instantaneamente sem perder o estado da aplicação, tornando a experiência de desenvolvimento muito mais suave e rápida. - -Vite é uma ferramenta de construção moderna que torna o desenvolvimento incrivelmente rápido. Ao contrário dos bundlers tradicionais, o Vite serve seu código diretamente para o navegador durante o desenvolvimento, o que significa um início instantâneo do servidor, não importa o tamanho do seu projeto, e atualizações ultrarrápidas. - - -Iniciando o Servidor de Desenvolvimento ---------------------------------------- - -Execute o servidor de desenvolvimento: - -```shell -npm run dev -``` - -Você verá: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Mantenha este terminal aberto durante o desenvolvimento. - -O plugin Nette Vite detecta automaticamente quando: -1. O servidor de desenvolvimento Vite está em execução -2. Sua aplicação Nette está em modo de depuração - -Quando ambas as condições são atendidas, o Nette Assets carrega os arquivos do servidor de desenvolvimento Vite em vez do diretório compilado: - -```latte -{asset 'app.js'} -{* Em desenvolvimento: *} -{* Em produção: *} -``` - -Nenhuma configuração necessária - simplesmente funciona! - - -Trabalhando em Diferentes Domínios ----------------------------------- - -Se o seu servidor de desenvolvimento estiver sendo executado em algo diferente de `localhost` (como `myapp.local`), você pode encontrar problemas de CORS (Cross-Origin Resource Sharing). CORS é um recurso de segurança em navegadores da web que bloqueia solicitações entre diferentes domínios por padrão. Quando sua aplicação PHP é executada em `myapp.local`, mas o Vite é executado em `localhost:5173`, o navegador os vê como domínios diferentes e bloqueia as solicitações. - -Você tem duas opções para resolver isso: - -**Opção 1: Configurar CORS** - -A solução mais simples é permitir solicitações cross-origin de sua aplicação PHP: - -```js -export default defineConfig({ - // ... outras configurações ... - - server: { - cors: { - origin: 'http://myapp.local', // URL da sua aplicação PHP - }, - }, -}); -``` -**Opção 2: Executar o Vite em seu domínio** - -A outra solução é fazer com que o Vite seja executado no mesmo domínio da sua aplicação PHP. - -```js -export default defineConfig({ - // ... outras configurações ... - - server: { - host: 'myapp.local', // o mesmo da sua aplicação PHP - }, -}); -``` - -Na verdade, mesmo neste caso, você precisa configurar o CORS porque o servidor de desenvolvimento é executado no mesmo hostname, mas em uma porta diferente. No entanto, neste caso, o CORS é configurado automaticamente pelo plugin Nette Vite. - - -Desenvolvimento HTTPS ---------------------- - -Se você desenvolve em HTTPS, precisa de certificados para o seu servidor de desenvolvimento Vite. A maneira mais fácil é usar um plugin que gera certificados automaticamente: - -```shell -npm install -D vite-plugin-mkcert -``` - -Veja como configurá-lo em `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... outras configurações ... - - plugins: [ - mkcert(), // gera certificados automaticamente e habilita https - nette(), - ], -}); -``` - -Observe que, se você estiver usando a configuração CORS (Opção 1 acima), precisará atualizar a URL de origem para usar `https://` em vez de `http://`. - - -Construções de Produção -======================= - -Crie arquivos de produção otimizados: - -```shell -npm run build -``` - -O Vite irá: -- Minificar todo o JavaScript e CSS -- Dividir o código em chunks ideais -- Gerar nomes de arquivo com hash para cache-busting -- Criar um arquivo de manifesto para Nette Assets - -Exemplo de saída: - -``` -www/assets/ -├── app-4f3a2b1c.js # Seu JavaScript principal (minificado) -├── app-7d8e9f2a.css # CSS extraído (minificado) -├── vendor-8c4b5e6d.js # Dependências compartilhadas -└── .vite/ - └── manifest.json # Mapeamento para Nette Assets -``` - -Os nomes de arquivo com hash garantem que os navegadores sempre carreguem a versão mais recente. - - -Pasta Pública -============= - -Os arquivos no diretório `assets/public/` são copiados para a saída sem processamento: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Referencie-os normalmente: - -```latte -{* Estes arquivos são copiados como estão *} - - -``` - -Para arquivos públicos, você pode usar os recursos do FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Tenta WebP primeiro - versioning: true # Adiciona cache-busting -``` - -Na configuração `vite.config.ts` você pode alterar a pasta pública usando a opção `publicDir`. - - -Importações Dinâmicas -===================== - -O Vite divide automaticamente o código para carregamento ideal. As importações dinâmicas permitem que você carregue o código apenas quando ele é realmente necessário, reduzindo o tamanho inicial do bundle: - -```js -// Carrega componentes pesados sob demanda -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -As importações dinâmicas criam chunks separados que são carregados apenas quando necessário. Isso é chamado de "code splitting" e é um dos recursos mais poderosos do Vite. Quando você usa importações dinâmicas, o Vite cria automaticamente arquivos JavaScript separados para cada módulo importado dinamicamente. - -A tag `{asset 'app.js'}` **não** pré-carrega automaticamente esses chunks dinâmicos. Este é um comportamento intencional - não queremos baixar código que talvez nunca seja usado. Os chunks são baixados apenas quando a importação dinâmica é executada. - -No entanto, se você souber que certas importações dinâmicas são críticas e serão necessárias em breve, você pode pré-carregá-las: - -```latte -{* Ponto de entrada principal *} -{asset 'app.js'} - -{* Pré-carrega importações dinâmicas críticas *} -{preload 'components/chart.js'} -``` - -Isso informa ao navegador para baixar o componente do gráfico em segundo plano, para que esteja pronto imediatamente quando necessário. - - -Suporte a TypeScript -==================== - -TypeScript funciona de imediato: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Referencie arquivos TypeScript normalmente: - -```latte -{asset 'main.ts'} -``` - -Para suporte completo a TypeScript, instale-o: - -```shell -npm install -D typescript -``` - - -Configuração Adicional do Vite -============================== - -Aqui estão algumas opções úteis de configuração do Vite com explicações detalhadas: - -```js -export default defineConfig({ - // Diretório raiz contendo os assets de origem - root: 'assets', - - // Pasta cujo conteúdo é copido para o diretório de saída como está - // Padrão: 'public' (relativo a 'root') - publicDir: 'public', - - build: { - // Onde colocar os arquivos compilados (relativo a 'root') - outDir: '../www/assets', - - // Esvaziar o diretório de saída antes de construir? - // Útil para remover arquivos antigos de construções anteriores - emptyOutDir: true, - - // Subdiretório dentro de outDir para chunks e assets gerados - // Isso ajuda a organizar a estrutura de saída - assetsDir: 'static', - - rollupOptions: { - // Ponto(s) de entrada - pode ser um único arquivo ou array de arquivos - // Cada ponto de entrada se torna um bundle separado - input: [ - 'app.js', // aplicação principal - 'admin.js', // painel de administração - ], - }, - }, - - server: { - // Host para o qual o servidor de desenvolvimento deve se vincular - // Use '0.0.0.0' para expor à rede - host: 'localhost', - - // Porta para o servidor de desenvolvimento - port: 5173, - - // Configuração CORS para solicitações cross-origin - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Habilitar sourcemaps CSS em desenvolvimento - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -É isso! Agora você tem um sistema de construção moderno integrado com Nette Assets. diff --git a/assets/ro/@home.texy b/assets/ro/@home.texy deleted file mode 100644 index e1f406932c..0000000000 --- a/assets/ro/@home.texy +++ /dev/null @@ -1,432 +0,0 @@ -Nette Assets -************ - -
    - -V-ați săturat să gestionați manual fișierele statice în aplicațiile dumneavoastră web? Uitați de codificarea manuală a căilor, de gestionarea invalidării cache-ului sau de îngrijorarea legată de versionarea fișierelor. Nette Assets transformă modul în care lucrați cu imagini, foi de stil, scripturi și alte resurse statice. - -- **Versionare inteligentă** asigură că browserele încarcă întotdeauna cele mai recente fișiere -- **Detecție automată** a tipurilor și dimensiunilor fișierelor -- **Integrare perfectă cu Latte** cu tag-uri intuitive -- **Arhitectură flexibilă** care suportă sisteme de fișiere, CDN-uri și Vite -- **Încărcare leneșă** pentru performanță optimă - -
    - - -De ce Nette Assets? -=================== - -Lucrul cu fișiere statice înseamnă adesea cod repetitiv, predispus la erori. Construiți manual URL-uri, adăugați parametri de versiune pentru invalidarea cache-ului și gestionați diferit tipurile de fișiere. Acest lucru duce la cod de genul: - -```latte -Logo - -``` - -Cu Nette Assets, toată această complexitate dispare: - -```latte -{* Totul automatizat - URL, versionare, dimensiuni *} - - - -{* Sau pur și simplu *} -{asset 'css/style.css'} -``` - -Asta e tot! Biblioteca automat: -- Adaugă parametri de versiune bazat pe timpul de modificare al fișierului -- Detectează dimensiunile imaginii și le include în HTML -- Generează elementul HTML corect pentru fiecare tip de fișier -- Gestionează atât mediile de dezvoltare, cât și cele de producție - - -Instalare -========= - -Instalați Nette Assets folosind [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Necesită PHP 8.1 sau o versiune superioară și funcționează perfect cu Nette Framework, dar poate fi folosit și independent. - - -Primii Pași -=========== - -Nette Assets funcționează imediat, fără configurare. Plasați fișierele statice în directorul `www/assets/` și începeți să le utilizați: - -```latte -{* Afișează o imagine cu dimensiuni automate *} -{asset 'logo.png'} - -{* Include o foaie de stil cu versionare *} -{asset 'style.css'} - -{* Încarcă un modul JavaScript *} -{asset 'app.js'} -``` - -Pentru mai mult control asupra HTML-ului generat, utilizați atributul `n:asset` sau funcția `asset()`. - - -Cum Funcționează -================ - -Nette Assets este construit în jurul a trei concepte cheie care îl fac puternic, dar simplu de utilizat: - - -Asset-uri - Fișierele Dumneavoastră Făcute Inteligente ------------------------------------------------------- - -Un **asset** reprezintă orice fișier static din aplicația dumneavoastră. Fiecare fișier devine un obiect cu proprietăți utile, doar pentru citire: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Diferite tipuri de fișiere oferă proprietăți diferite: -- **Imagini**: lățime, înălțime, text alternativ, încărcare leneșă -- **Scripturi**: tip modul, hash-uri de integritate, crossorigin -- **Foi de stil**: interogări media, integritate -- **Audio/Video**: durată, dimensiuni -- **Fonturi**: preîncărcare corectă cu CORS - -Biblioteca detectează automat tipurile de fișiere și creează clasa de asset corespunzătoare. - - -Mapperi - De unde provin fișierele ----------------------------------- - -Un **mapper** știe cum să găsească fișiere și să creeze URL-uri pentru ele. Puteți avea mai mulți mapperi pentru diferite scopuri - fișiere locale, CDN, stocare în cloud sau instrumente de construire (fiecare dintre ele are un nume). FilesystemMapper-ul încorporat gestionează fișierele locale, în timp ce ViteMapper se integrează cu instrumente moderne de construire. - -Mapperii sunt definiți în [Configurare |Configuration]. - - -Registrul - Interfața dumneavoastră principală ----------------------------------------------- - -**Registrul** gestionează toți mapperii și oferă API-ul principal: - -```php -// Injectați registrul în serviciul dumneavoastră -public function __construct( - private Nette\Assets\Registry $assets -) {} - -// Obțineți asset-uri de la diferiți mapperi -$logo = $this->assets->getAsset('images:logo.png'); // mapper 'image' -$app = $this->assets->getAsset('app:main.js'); // mapper 'app' -$style = $this->assets->getAsset('style.css'); // utilizează mapper-ul implicit -``` - -Registrul selectează automat mapper-ul potrivit și memorează în cache rezultatele pentru performanță. - - -Lucrul cu Asset-uri în PHP -========================== - -Registrul oferă două metode pentru recuperarea asset-urilor: - -```php -// Aruncă Nette\Assets\AssetNotFoundException dacă fișierul nu există -$logo = $assets->getAsset('logo.png'); - -// Returnează null dacă fișierul nu există -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Specificarea Mapper-ilor ------------------------- - -Puteți alege explicit ce mapper să utilizați: - -```php -// Utilizează mapper-ul implicit -$file = $assets->getAsset('document.pdf'); - -// Utilizează mapper-ul specific cu prefix -$image = $assets->getAsset('images:photo.jpg'); - -// Utilizează mapper-ul specific cu sintaxă de array -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Proprietăți și Tipuri de Asset-uri ----------------------------------- - -Fiecare tip de asset oferă proprietăți relevante, doar pentru citire: - -```php -// Proprietăți imagine -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Proprietăți script -$script = $assets->getAsset('app.js'); -echo $script->type; // 'module' or null - -// Proprietăți audio -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // duration in seconds - -// Toate asset-urile pot fi convertite la șir (returnează URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Proprietățile precum dimensiunile sau durata sunt încărcate leneș, doar la accesare, menținând biblioteca rapidă. - - -Utilizarea Asset-urilor în Șabloanele Latte -=========================================== - -Nette Assets oferă o integrare intuitivă cu [Latte|latte:] prin tag-uri și funcții. - - -`{asset}` ---------- - -Tag-ul `{asset}` randează elemente HTML complete: - -```latte -{* Randează: *} -{asset 'hero.jpg'} - -{* Randează: *} -{asset 'app.js'} - -{* Randează: *} -{asset 'style.css'} -``` - -Tag-ul automat: -- Detectează tipul asset-ului și generează HTML-ul corespunzător -- Include versionare pentru invalidarea cache-ului -- Adaugă dimensiuni pentru imagini -- Setează atributele corecte (tip, media, etc.) - -Când este utilizat în interiorul atributelor HTML, acesta afișează doar URL-ul: - -```latte -
    - -``` - - -`n:asset` ---------- - -Pentru control complet asupra atributelor HTML: - -```latte -{* Atributul n:asset completează src, dimensiuni etc. *} -Product - -{* Funcționează cu orice element relevant *} - - - -``` - -Utilizați variabile și mapperi: - -```latte -{* Variabilele funcționează natural *} - - -{* Specificați mapper-ul cu acolade *} - - -{* Specificați mapper-ul cu notație de array *} - -``` - - -`asset()` ---------- - -Pentru flexibilitate maximă, utilizați funcția `asset()`: - -```latte -{var $logo = asset('logo.png')} -width} height={$logo->height}> - -{* Sau direct *} -Logo -``` - - -Asset-uri Opționale -------------------- - -Gestionați asset-urile lipsă în mod elegant cu `{asset?}`, `n:asset?` și `tryAsset()`: - -```latte -{* Tag opțional - nu randează nimic dacă asset-ul lipsește *} -{asset? 'optional-banner.jpg'} - -{* Atribut opțional - sare peste dacă asset-ul lipsește *} -Avatar - -{* Cu fallback *} -{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')} -Avatar -``` - - -`{preload}` ------------ - -Îmbunătățiți performanța de încărcare a paginii: - -```latte -{* În secțiunea *} -{preload 'critical.css'} -{preload 'important-font.woff2'} -{preload 'hero-image.jpg'} -``` - -Generează link-uri de preîncărcare adecvate: - -```latte - - - -``` - - -Funcționalități Avansate -======================== - - -Auto-Detecția Extensiilor -------------------------- - -Gestionați automat mai multe formate: - -```neon -assets: - mapping: - images: - path: img - extension: [webp, jpg, png] # Încearcă în ordine -``` - -Acum puteți solicita fără extensie: - -```latte -{* Găsește automat logo.webp, logo.jpg sau logo.png *} -{asset 'images:logo'} -``` - -Perfect pentru îmbunătățirea progresivă cu formate moderne. - - -Versionare Inteligentă ----------------------- - -Fișierele sunt versionate automat pe baza timpului de modificare: - -```latte -{asset 'style.css'} -{* Ieșire: *} -``` - -Când actualizați fișierul, timestamp-ul se modifică, forțând reîmprospătarea cache-ului browserului. - -Controlați versionarea per asset: - -```php -// Dezactivează versionarea pentru un asset specific -$asset = $assets->getAsset('style.css', ['version' => false]); - -// În Latte -{asset 'style.css', version: false} -``` - - -Asset-uri Font --------------- - -Fonturile beneficiază de un tratament special cu CORS adecvat: - -```latte -{* Preîncărcare corectă cu crossorigin *} -{preload 'fonts:OpenSans-Regular.woff2'} - -{* Utilizați în CSS *} - -``` - - -Mapperi Personalizați -===================== - -Creați mapperi personalizați pentru nevoi speciale, cum ar fi stocarea în cloud sau generarea dinamică: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Înregistrați în configurare: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Utilizați ca orice alt mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Metoda `Helpers::createAssetFromUrl()` creează automat tipul corect de asset pe baza extensiei fișierului. - - -Lectură suplimentară -==================== - -- [Nette Assets: În sfârșit, API unificat pentru orice, de la imagini la Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/ro/@left-menu.texy b/assets/ro/@left-menu.texy deleted file mode 100644 index 654f12eacf..0000000000 --- a/assets/ro/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Noțiuni de bază |@home] -- [Vite |vite] -- [Configurare |Configuration] diff --git a/assets/ro/@meta.texy b/assets/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/assets/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/assets/ro/configuration.texy b/assets/ro/configuration.texy deleted file mode 100644 index 75d20c374a..0000000000 --- a/assets/ro/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Configurarea Asset-urilor -************************* - -.[perex] -Prezentare generală a opțiunilor de configurare pentru Nette Assets. - - -```neon -assets: - # cale de bază pentru rezolvarea căilor relative ale mapper-ilor - basePath: ... # (șir de caractere) implicit %wwwDir% - - # URL de bază pentru rezolvarea URL-urilor relative ale mapper-ilor - baseUrl: ... # (șir de caractere) implicit %baseUrl% - - # activează versionarea asset-urilor global? - versioning: ... # (boolean) implicit true - - # definește mapper-ii de asset-uri - mapping: ... # (array) implicit cale 'assets' -``` - -`basePath` setează directorul implicit al sistemului de fișiere pentru rezolvarea căilor relative în mapperi. Implicit, utilizează directorul web (`%wwwDir%`). - -`baseUrl` setează prefixul URL implicit pentru rezolvarea URL-urilor relative în mapperi. Implicit, utilizează URL-ul rădăcină (`%baseUrl%`). - -Opțiunea `versioning` controlează global dacă parametrii de versiune sunt adăugați la URL-urile asset-urilor pentru invalidarea cache-ului. Mapperii individuali pot suprascrie această setare. - - -Mapperi -------- - -Mapperii pot fi configurați în trei moduri: notație simplă de șir, notație detaliată de array sau ca referință la un serviciu. - -Cel mai simplu mod de a defini un mapper: - -```neon -assets: - mapping: - default: assets # Creează un mapper de sistem de fișiere pentru %wwwDir%/assets/ - images: img # Creează un mapper de sistem de fișiere pentru %wwwDir%/img/ - scripts: js # Creează un mapper de sistem de fișiere pentru %wwwDir%/js/ -``` - -Fiecare mapper creează un `FilesystemMapper` care: -- Caută fișiere în `%wwwDir%/` -- Generează URL-uri precum `%baseUrl%/` -- Moștenește setarea globală de versionare - - -Pentru mai mult control, utilizați notația detaliată: - -```neon -assets: - mapping: - images: - # directorul unde sunt stocate fișierele - path: ... # (șir de caractere) opțional, implicit '' - - # prefix URL pentru link-urile generate - url: ... # (șir de caractere) opțional, implicit cale - - # activează versionarea pentru acest mapper? - versioning: ... # (boolean) opțional, moștenește setarea globală - - # adaugă automat extensie(i) la căutarea fișierelor - extension: ... # (șir de caractere|array) opțional, implicit null -``` - -Înțelegerea modului în care valorile de configurare sunt rezolvate: - -Rezolvarea Căii: - - Căile relative sunt rezolvate din `basePath` (sau `%wwwDir%` dacă `basePath` nu este setat) - - Căile absolute sunt utilizate ca atare - -Rezolvarea URL-ului: - - URL-urile relative sunt rezolvate din `baseUrl` (sau `%baseUrl%` dacă `baseUrl` nu este setat) - - URL-urile absolute (cu schemă sau `//`) sunt utilizate ca atare - - Dacă `url` nu este specificat, utilizează valoarea `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Cale și URL relativ - images: - path: img # Rezolvat la: /var/www/project/www/img - url: images # Rezolvat la: https://example.com/assets/images - - # Cale și URL absolut - uploads: - path: /var/shared/uploads # Utilizat ca atare: /var/shared/uploads - url: https://cdn.example.com # Utilizat ca atare: https://cdn.example.com - - # Doar calea specificată - styles: - path: css # Cale: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Mapperi Personalizați ---------------------- - -Pentru mapperi personalizați, referențiați sau definiți un serviciu: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Mapper-ul Vite necesită doar adăugarea `type: vite`. Aceasta este o listă completă de opțiuni de configurare: - -```neon -assets: - mapping: - default: - # tip mapper (obligatoriu pentru Vite) - type: vite # (șir de caractere) obligatoriu, trebuie să fie 'vite' - - # directorul de ieșire al construirii Vite - path: ... # (șir de caractere) opțional, implicit '' - - # prefix URL pentru asset-urile construite - url: ... # (șir de caractere) opțional, implicit cale - - # locația fișierului manifest Vite - manifest: ... # (șir de caractere) opțional, implicit /.vite/manifest.json - - # configurare server de dezvoltare Vite - devServer: ... # (boolean|șir de caractere) opțional, implicit true - - # versionare pentru fișierele din directorul public - versioning: ... # (boolean) opțional, moștenește setarea globală - - # auto-extensie pentru fișierele din directorul public - extension: ... # (șir de caractere|array) opțional, implicit null -``` - -Opțiunea `devServer` controlează modul în care asset-urile sunt încărcate în timpul dezvoltării: - -- `true` (implicit) - Detectează automat serverul de dezvoltare Vite pe gazda și portul curente. Dacă serverul de dezvoltare rulează **și aplicația dumneavoastră este în modul de depanare**, asset-urile sunt încărcate de la acesta cu suport pentru înlocuirea la cald a modulelor (HMR). Dacă serverul de dezvoltare nu rulează, asset-urile sunt încărcate din fișierele construite din directorul public. -- `false` - Dezactivează complet integrarea serverului de dezvoltare. Asset-urile sunt întotdeauna încărcate din fișierele construite. -- URL personalizat (de ex., `https://localhost:5173`) - Specificați manual URL-ul serverului de dezvoltare, inclusiv protocolul și portul. Util atunci când serverul de dezvoltare rulează pe o altă gazdă sau port. - -Opțiunile `versioning` și `extension` se aplică doar fișierelor din directorul public al Vite care nu sunt procesate de Vite. - - -Configurare Manuală -------------------- - -Când nu utilizați Nette DI, configurați mapperii manual: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Adaugă mapper de sistem de fișiere -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Adaugă mapper Vite -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/ro/vite.texy b/assets/ro/vite.texy deleted file mode 100644 index 3415ffd29f..0000000000 --- a/assets/ro/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Integrare Vite -************** - -
    - -Aplicațiile JavaScript moderne necesită instrumente de construire sofisticate. Nette Assets oferă o integrare de primă clasă cu [Vite |https://vitejs.dev/], instrumentul de construire frontend de ultimă generație. Obțineți o dezvoltare ultra-rapidă cu Hot Module Replacement (HMR) și build-uri de producție optimizate, fără bătăi de cap legate de configurare. - -- **Zero configurare** - punte automată între Vite și șabloanele PHP -- **Gestionare completă a dependențelor** - un singur tag gestionează toate asset-urile -- **Hot Module Replacement** - actualizări instantanee JavaScript și CSS -- **Build-uri de producție optimizate** - împărțirea codului și tree shaking - -
    - - -Nette Assets se integrează perfect cu Vite, astfel încât obțineți toate aceste beneficii în timp ce scrieți șabloanele ca de obicei. - - -Configurarea Vite -================= - -Să configurăm Vite pas cu pas. Nu vă faceți griji dacă sunteți nou în lumea instrumentelor de construire - vom explica totul! - - -Pasul 1: Instalarea Vite ------------------------- - -Mai întâi, instalați Vite și plugin-ul Nette în proiectul dumneavoastră: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Aceasta instalează Vite și un plugin special care ajută Vite să funcționeze perfect cu Nette. - - -Pasul 2: Structura Proiectului ------------------------------- - -Abordarea standard este de a plasa fișierele asset sursă într-un folder `assets/` în rădăcina proiectului, iar versiunile compilate în `www/assets/`: - -/--pre -web-project/ -├── assets/ ← fișiere sursă (SCSS, TypeScript, imagini sursă) -│ ├── public/ ← fișiere statice (copiate ca atare) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← punct de intrare principal -│ └── style.css ← stilurile dumneavoastră -└── www/ ← director public (rădăcina documentului) - ├── assets/ ← fișierele compilate vor ajunge aici - └── index.php -\-- - -Folderul `assets/` conține fișierele dumneavoastră sursă - codul pe care îl scrieți. Vite va procesa aceste fișiere și va plasa versiunile compilate în `www/assets/`. - - -Pasul 3: Configurarea Vite --------------------------- - -Creați un fișier `vite.config.ts` în rădăcina proiectului dumneavoastră. Acest fișier îi spune lui Vite unde să găsească fișierele sursă și unde să plaseze cele compilate. - -Plugin-ul Nette Vite vine cu setări implicite inteligente care simplifică configurarea. Presupune că fișierele dumneavoastră sursă de frontend se află în directorul `assets/` (opțiunea `root`) și că fișierele compilate ajung în `www/assets/` (opțiunea `outDir`). Trebuie doar să specificați [punctul de intrare|#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Dacă doriți să specificați un alt nume de director pentru a construi asset-urile, va trebui să modificați câteva opțiuni: - -```js -export default defineConfig({ - root: 'assets', // directorul rădăcină al asset-urilor sursă - - build: { - outDir: '../www/assets', // unde ajung fișierele compilate - }, - - // ... alte configurații ... -}); -``` - -.[note] -Calea `outDir` este considerată relativă la `root`, de aceea există `../` la început. - - -Pasul 4: Configurarea Nette ---------------------------- - -Spuneți Nette Assets despre Vite în fișierul dumneavoastră `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # îi spune lui Nette să utilizeze ViteMapper - path: assets -``` - - -Pasul 5: Adăugați scripturi ---------------------------- - -Adăugați aceste scripturi în `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Acum puteți: -- `npm run dev` - pornește serverul de dezvoltare cu reîncărcare la cald -- `npm run build` - creează fișiere de producție optimizate - - -Puncte de Intrare -================= - -Un **punct de intrare** este fișierul principal de unde pornește aplicația dumneavoastră. Din acest fișier, importați alte fișiere (CSS, module JavaScript, imagini), creând un arbore de dependențe. Vite urmărește aceste importuri și le grupează pe toate împreună. - -Exemplu de punct de intrare `assets/app.js`: - -```js -// Importă stiluri -import './style.css' - -// Importă module JavaScript -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Inițializează aplicația dumneavoastră -netteForms.initOnLoad(); -naja.initialize(); -``` - -În șablon puteți insera un punct de intrare după cum urmează: - -```latte -{asset 'app.js'} -``` - -Nette Assets generează automat toate tag-urile HTML necesare - JavaScript, CSS și orice alte dependențe. - - -Puncte de Intrare Multiple --------------------------- - -Aplicațiile mai mari necesită adesea puncte de intrare separate: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // pagini publice - 'admin.js', // panou de administrare - ], - }), - ], -}); -``` - -Utilizați-le în diferite șabloane: - -```latte -{* În pagini publice *} -{asset 'app.js'} - -{* În panoul de administrare *} -{asset 'admin.js'} -``` - - -Important: Fișiere Sursă vs. Fișiere Compilate ----------------------------------------------- - -Este crucial să înțelegeți că în producție puteți încărca doar: - -1. **Puncte de intrare** definite în `entry` -2. **Fișiere din directorul `assets/public/`** - -Nu **puteți** încărca utilizând `{asset}` fișiere arbitrare din `assets/` - doar asset-uri referențiate de fișiere JavaScript sau CSS. Dacă fișierul dumneavoastră nu este referențiat nicăieri, nu va fi compilat. Dacă doriți ca Vite să fie conștient de alte asset-uri, le puteți muta în [folderul public |#public-folder]. - -Vă rugăm să rețineți că, implicit, Vite va încorpora toate asset-urile mai mici de 4KB, deci nu veți putea referenția aceste fișiere direct. (Vezi [documentația Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Acesta funcționează - este un punct de intrare *} -{asset 'app.js'} - -{* ✓ Acesta funcționează - este în assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Acesta nu va funcționa - fișier aleatoriu în assets/ *} -{asset 'components/button.js'} -``` - - -Modul de Dezvoltare -=================== - -Modul de dezvoltare este complet opțional, dar oferă beneficii semnificative atunci când este activat. Principalul avantaj este **Hot Module Replacement (HMR)** - vedeți modificările instantaneu fără a pierde starea aplicației, făcând experiența de dezvoltare mult mai fluidă și mai rapidă. - -Vite este un instrument modern de construire care face dezvoltarea incredibil de rapidă. Spre deosebire de bundler-ele tradiționale, Vite servește codul dumneavoastră direct browserului în timpul dezvoltării, ceea ce înseamnă pornire instantanee a serverului indiferent de mărimea proiectului și actualizări ultra-rapide. - - -Pornirea Serverului de Dezvoltare ---------------------------------- - -Rulați serverul de dezvoltare: - -```shell -npm run dev -``` - -Veți vedea: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Țineți acest terminal deschis în timpul dezvoltării. - -Plugin-ul Nette Vite detectează automat când: -1. Serverul de dezvoltare Vite rulează -2. Aplicația dumneavoastră Nette este în modul de depanare - -Când ambele condiții sunt îndeplinite, Nette Assets încarcă fișierele de la serverul de dezvoltare Vite în loc de directorul compilat: - -```latte -{asset 'app.js'} -{* În dezvoltare: *} -{* În producție: *} -``` - -Nu este necesară configurare - pur și simplu funcționează! - - -Lucrul pe Domenii Diferite --------------------------- - -Dacă serverul dumneavoastră de dezvoltare rulează pe altceva decât `localhost` (cum ar fi `myapp.local`), s-ar putea să întâmpinați probleme CORS (Cross-Origin Resource Sharing). CORS este o funcționalitate de securitate în browserele web care blochează implicit cererile între domenii diferite. Când aplicația dumneavoastră PHP rulează pe `myapp.local`, dar Vite rulează pe `localhost:5173`, browserul le consideră domenii diferite și blochează cererile. - -Aveți două opțiuni pentru a rezolva acest lucru: - -**Opțiunea 1: Configurați CORS** - -Cea mai simplă soluție este să permiteți cererile cross-origin din aplicația dumneavoastră PHP: - -```js -export default defineConfig({ - // ... alte configurații ... - - server: { - cors: { - origin: 'http://myapp.local', // URL-ul aplicației dumneavoastră PHP - }, - }, -}); -``` -**Opțiunea 2: Rulați Vite pe domeniul dumneavoastră** - -Cealaltă soluție este să faceți Vite să ruleze pe același domeniu ca aplicația dumneavoastră PHP. - -```js -export default defineConfig({ - // ... alte configurații ... - - server: { - host: 'myapp.local', // la fel ca aplicația dumneavoastră PHP - }, -}); -``` - -De fapt, chiar și în acest caz, trebuie să configurați CORS deoarece serverul de dezvoltare rulează pe același hostname, dar pe un port diferit. Totuși, în acest caz, CORS este configurat automat de plugin-ul Nette Vite. - - -Dezvoltare HTTPS ----------------- - -Dacă dezvoltați pe HTTPS, aveți nevoie de certificate pentru serverul dumneavoastră de dezvoltare Vite. Cel mai simplu mod este utilizarea unui plugin care generează certificate automat: - -```shell -npm install -D vite-plugin-mkcert -``` - -Iată cum să-l configurați în `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... alte configurații ... - - plugins: [ - mkcert(), // generează certificate automat și activează https - nette(), - ], -}); -``` - -Rețineți că dacă utilizați configurația CORS (Opțiunea 1 de mai sus), trebuie să actualizați URL-ul de origine pentru a utiliza `https://` în loc de `http://`. - - -Build-uri de Producție -====================== - -Creați fișiere de producție optimizate: - -```shell -npm run build -``` - -Vite va: -- Minifica tot JavaScript-ul și CSS-ul -- Împărți codul în bucăți optime -- Genera nume de fișiere hash-uite pentru invalidarea cache-ului -- Crea un fișier manifest pentru Nette Assets - -Exemplu de ieșire: - -``` -www/assets/ -├── app-4f3a2b1c.js # JavaScript-ul dumneavoastră principal (minificat) -├── app-7d8e9f2a.css # CSS extras (minificat) -├── vendor-8c4b5e6d.js # Dependențe partajate -└── .vite/ - └── manifest.json # Mapare pentru Nette Assets -``` - -Numele de fișiere hash-uite asigură că browserele încarcă întotdeauna cea mai recentă versiune. - - -Folder Public -============= - -Fișierele din directorul `assets/public/` sunt copiate în ieșire fără procesare: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Referențiați-le în mod normal: - -```latte -{* Aceste fișiere sunt copiate ca atare *} - - -``` - -Pentru fișierele publice, puteți utiliza funcționalitățile FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Încearcă WebP mai întâi - versioning: true # Adaugă invalidare cache -``` - -În configurația `vite.config.ts` puteți schimba folderul public utilizând opțiunea `publicDir`. - - -Importuri Dinamice -================== - -Vite împarte automat codul pentru o încărcare optimă. Importurile dinamice vă permit să încărcați codul doar atunci când este efectiv necesar, reducând dimensiunea inițială a bundle-ului: - -```js -// Încarcă componente grele la cerere -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Importurile dinamice creează bucăți separate care sunt încărcate doar atunci când este necesar. Acesta se numește "code splitting" și este una dintre cele mai puternice funcționalități ale Vite. Când utilizați importuri dinamice, Vite creează automat fișiere JavaScript separate pentru fiecare modul importat dinamic. - -Tag-ul `{asset 'app.js'}` **nu** preîncarcă automat aceste bucăți dinamice. Acesta este un comportament intenționat - nu dorim să descărcăm cod care s-ar putea să nu fie folosit niciodată. Bucățile sunt descărcate doar atunci când importul dinamic este executat. - -Totuși, dacă știți că anumite importuri dinamice sunt critice și vor fi necesare în curând, le puteți preîncărca: - -```latte -{* Punct de intrare principal *} -{asset 'app.js'} - -{* Preîncarcă importuri dinamice critice *} -{preload 'components/chart.js'} -``` - -Acest lucru îi spune browserului să descarce componenta grafic în fundal, astfel încât să fie gata imediat când este necesar. - - -Suport TypeScript -================= - -TypeScript funcționează imediat: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Referențiați fișierele TypeScript în mod normal: - -```latte -{asset 'main.ts'} -``` - -Pentru suport complet TypeScript, instalați-l: - -```shell -npm install -D typescript -``` - - -Configurație Suplimentară Vite -============================== - -Iată câteva opțiuni utile de configurare Vite cu explicații detaliate: - -```js -export default defineConfig({ - // Directorul rădăcină care conține asset-urile sursă - root: 'assets', - - // Folderul al cărui conținut este copiat în directorul de ieșire ca atare - // Implicit: 'public' (relativ la 'root') - publicDir: 'public', - - build: { - // Unde să plasezi fișierele compilate (relativ la 'root') - outDir: '../www/assets', - - // Golește directorul de ieșire înainte de construire? - // Util pentru a elimina fișierele vechi din build-urile anterioare - emptyOutDir: true, - - // Subdirector în outDir pentru bucățile și asset-urile generate - // Acest lucru ajută la organizarea structurii de ieșire - assetsDir: 'static', - - rollupOptions: { - // Punct(e) de intrare - poate fi un singur fișier sau un array de fișiere - // Fiecare punct de intrare devine un bundle separat - input: [ - 'app.js', // aplicația principală - 'admin.js', // panoul de administrare - ], - }, - }, - - server: { - // Gazda la care să se lege serverul de dezvoltare - // Utilizați '0.0.0.0' pentru a expune la rețea - host: 'localhost', - - // Port pentru serverul de dezvoltare - port: 5173, - - // Configurare CORS pentru cererile cross-origin - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Activează hărțile sursă CSS în dezvoltare - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -Asta e tot! Acum aveți un sistem de construire modern integrat cu Nette Assets. diff --git a/assets/sl/@home.texy b/assets/sl/@home.texy deleted file mode 100644 index 06520df065..0000000000 --- a/assets/sl/@home.texy +++ /dev/null @@ -1,432 +0,0 @@ -Nette Assets -************ - -
    - -Už vás unavuje manuálna správa statických súborov vo vašich webových aplikáciách? Zabudnite na pevne zakódované cesty, problémy s zneplatnením cache alebo starosti s verzovaním súborov. Nette Assets mení spôsob, akým pracujete s obrázkami, štýlmi, skriptami a inými statickými zdrojmi. - -- **Inteligentné verzovanie** zaisťuje, že prehliadače vždy načítajú najnovšie súbory -- **Automatická detekcia** typov súborov a rozmerov -- **Bezproblémová integrácia s Latte** s intuitívnymi tagmi -- **Flexibilná architektúra** podporujúca súborové systémy, CDN a Vite -- **Lazy loading** pre optimálny výkon - -
    - - -Prečo Nette Assets? -=================== - -Práca so statickými súbormi často znamená opakujúci sa kód náchylný na chyby. Manuálne konštruujete URL adresy, pridávate parametre verzie pre cache busting a rôzne typy súborov spracovávate odlišne. To vedie ku kódu ako: - -```latte -Logo - -``` - -S Nette Assets všetka táto zložitosť zmizne: - -```latte -{* Všetko automatizované - URL, verzovanie, rozmery *} - - - -{* Alebo len *} -{asset 'css/style.css'} -``` - -To je všetko! Knižnica automaticky: -- Pridáva parametre verzie na základe času poslednej úpravy súboru -- Detekuje rozmery obrázka a zahrnie ich do HTML -- Generuje správny HTML element pre každý typ súboru -- Spracováva vývojové aj produkčné prostredia - - -Inštalácia -========== - -Nainštalujte Nette Assets pomocou [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Vyžaduje PHP 8.1 alebo vyššie a funguje perfektne s Nette Frameworkom, ale môže byť použitá aj samostatne. - - -Prvé kroky -========== - -Nette Assets funguje hneď po vybalení bez akejkoľvek konfigurácie. Umiestnite svoje statické súbory do adresára `www/assets/` a začnite ich používať: - -```latte -{* Zobrazí obrázok s automatickými rozmermi *} -{asset 'logo.png'} - -{* Zahrnie štýl s verzovaním *} -{asset 'style.css'} - -{* Načíta JavaScript modul *} -{asset 'app.js'} -``` - -Pre väčšiu kontrolu nad generovaným HTML použite atribút `n:asset` alebo funkciu `asset()`. - - -Ako to funguje -============== - -Nette Assets je postavený na troch základných konceptoch, ktoré ho robia výkonným a zároveň jednoduchým na používanie: - - -Assets – Vaše súbory sú inteligentné ------------------------------------- - -**Asset** predstavuje akýkoľvek statický súbor vo vašej aplikácii. Každý súbor sa stáva objektom s užitočnými readonly vlastnosťami: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Rôzne typy súborov poskytujú rôzne vlastnosti: -- **Obrázky**: šírka, výška, alternatívny text, lazy loading -- **Skripty**: typ modulu, integrity hashe, crossorigin -- **Štýly**: media queries, integrity -- **Audio/Video**: trvanie, rozmery -- **Fonty**: správne preloading s CORS - -Knižnica automaticky detekuje typy súborov a vytvára príslušnú triedu assetu. - - -Mappery – Odkiaľ súbory pochádzajú ----------------------------------- - -**Mapper** vie, ako nájsť súbory a vytvoriť pre ne URL adresy. Môžete mať viacero mapperov na rôzne účely – lokálne súbory, CDN, cloudové úložisko alebo build nástroje (každý z nich má názov). Vstavaný `FilesystemMapper` spracováva lokálne súbory, zatiaľ čo `ViteMapper` sa integruje s modernými build nástrojmi. - -Mappery sú definované v [konfigurácii]. - - -Registry – Vaše hlavné rozhranie --------------------------------- - -**Registry** spravuje všetky mappery a poskytuje hlavné API: - -```php -// Vložte registry do vašej služby -public function __construct( - private Nette\Assets\Registry $assets -) {} - -// Získajte assets z rôznych mapperov -$logo = $this->assets->getAsset('images:logo.png'); // 'image' mapper -$app = $this->assets->getAsset('app:main.js'); // 'app' mapper -$style = $this->assets->getAsset('style.css'); // používa predvolený mapper -``` - -Registry automaticky vyberie správny mapper a cachuje výsledky pre výkon. - - -Práca s Assets v PHP -==================== - -Registry poskytuje dve metódy na získanie assetov: - -```php -// Vyhodí Nette\Assets\AssetNotFoundException, ak súbor neexistuje -$logo = $assets->getAsset('logo.png'); - -// Vráti null, ak súbor neexistuje -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Špecifikácia Mapperov ---------------------- - -Môžete explicitne zvoliť, ktorý mapper použiť: - -```php -// Použite predvolený mapper -$file = $assets->getAsset('document.pdf'); - -// Použite špecifický mapper s prefixom -$image = $assets->getAsset('images:photo.jpg'); - -// Použite špecifický mapper so syntaxou poľa -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Vlastnosti a typy Assetov -------------------------- - -Každý typ assetu poskytuje relevantné readonly vlastnosti: - -```php -// Vlastnosti obrázka -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Vlastnosti skriptu -$script = $assets->getAsset('app.js'); -echo $script->type; // 'module' alebo null - -// Vlastnosti audia -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // trvanie v sekundách - -// Všetky assets môžu byť pretypované na string (vráti URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Vlastnosti ako rozmery alebo trvanie sú načítané len lenivo, keď sú prvýkrát prístupné, čo udržuje knižnicu rýchlu. - - -Používanie Assets v Latte šablónach -=================================== - -Nette Assets poskytuje intuitívnu [Latte|latte:] integráciu s tagmi a funkciami. - - -`{asset}` ---------- - -Tag `{asset}` vykresľuje kompletné HTML elementy: - -```latte -{* Vykreslí: *} -{asset 'hero.jpg'} - -{* Vykreslí: *} -{asset 'app.js'} - -{* Vykreslí: *} -{asset 'style.css'} -``` - -Tag automaticky: -- Detekuje typ assetu a generuje príslušné HTML -- Zahrnie verzovanie pre cache busting -- Pridá rozmery pre obrázky -- Nastaví správne atribúty (typ, media atď.) - -Pri použití vo vnútri HTML atribútov výstupom je len URL: - -```latte -
    - -``` - - -`n:asset` ---------- - -Pre úplnú kontrolu nad HTML atribútmi: - -```latte -{* Atribút n:asset dopĺňa src, rozmery atď. *} -Produkt - -{* Funguje s akýmkoľvek relevantným elementom *} - - - -``` - -Použite premenné a mappery: - -```latte -{* Premenné fungujú prirodzene *} - - -{* Špecifikujte mapper s kučeravými zátvorkami *} - - -{* Špecifikujte mapper s notáciou poľa *} - -``` - - -`asset()` ---------- - -Pre maximálnu flexibilitu použite funkciu `asset()`: - -```latte -{var $logo = asset('logo.png')} -width} height={$logo->height}> - -{* Alebo priamo *} -Logo -``` - - -Voliteľné Assets ----------------- - -Spracujte chýbajúce assets elegantne pomocou `{asset?}`, `n:asset?` a `tryAsset()`: - -```latte -{* Voliteľný tag - nevykreslí nič, ak asset chýba *} -{asset? 'optional-banner.jpg'} - -{* Voliteľný atribút - preskočí, ak asset chýba *} -Avatar - -{* S fallbackom *} -{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')} -Avatar -``` - - -`{preload}` ------------ - -Zlepšite výkon načítania stránky: - -```latte -{* Vo vašej sekcii *} -{preload 'critical.css'} -{preload 'important-font.woff2'} -{preload 'hero-image.jpg'} -``` - -Generuje príslušné preload odkazy: - -```latte - - - -``` - - -Pokročilé funkcie -================= - - -Automatická detekcia prípon ---------------------------- - -Automaticky spracujte viacero formátov: - -```neon -assets: - mapping: - images: - path: img - extension: [webp, jpg, png] # Skúšajte v poradí -``` - -Teraz môžete požiadať bez prípony: - -```latte -{* Automaticky nájde logo.webp, logo.jpg alebo logo.png *} -{asset 'images:logo'} -``` - -Ideálne pre progresívne vylepšenie s modernými formátmi. - - -Inteligentné verzovanie ------------------------ - -Súbory sú automaticky verzované na základe času poslednej úpravy: - -```latte -{asset 'style.css'} -{* Výstup: *} -``` - -Keď aktualizujete súbor, časová pečiatka sa zmení, čo vynúti obnovenie cache prehliadača. - -Kontrola verzovania pre jednotlivé assets: - -```php -// Zakázať verzovanie pre konkrétny asset -$asset = $assets->getAsset('style.css', ['version' => false]); - -// V Latte -{asset 'style.css', version: false} -``` - - -Font Assets ------------ - -Fonty dostávajú špeciálne zaobchádzanie so správnym CORS: - -```latte -{* Správne preload s crossorigin *} -{preload 'fonts:OpenSans-Regular.woff2'} - -{* Použite v CSS *} - -``` - - -Vlastné Mappery -=============== - -Vytvorte vlastné mappery pre špeciálne potreby, ako je cloudové úložisko alebo dynamické generovanie: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Zaregistrujte v konfigurácii: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Použite ako akýkoľvek iný mapper: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Metóda `Helpers::createAssetFromUrl()` automaticky vytvorí správny typ assetu na základe prípony súboru. - - -Nadaljnje branje -================ - -- [Nette Assets: Končno poenoten API za vse, od slik do Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/sl/@left-menu.texy b/assets/sl/@left-menu.texy deleted file mode 100644 index d7dd6293b8..0000000000 --- a/assets/sl/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Začíname |@home] -- [Vite |vite] -- [Konfigurácia |Configuration] diff --git a/assets/sl/@meta.texy b/assets/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/assets/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/assets/sl/configuration.texy b/assets/sl/configuration.texy deleted file mode 100644 index dd0d7fb301..0000000000 --- a/assets/sl/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Konfigurácia Assets -******************* - -.[perex] -Prehľad možností konfigurácie pre Nette Assets. - - -```neon -assets: - # základná cesta pre rozlíšenie relatívnych ciest mapperov - basePath: ... # (string) predvolené na %wwwDir% - - # základná URL pre rozlíšenie relatívnych URL mapperov - baseUrl: ... # (string) predvolené na %baseUrl% - - # povoliť globálne verzovanie assetov? - versioning: ... # (bool) predvolené na true - - # definuje asset mappery - mapping: ... # (array) predvolené na cestu 'assets' -``` - -`basePath` nastavuje predvolený adresár súborového systému pre rozlíšenie relatívnych ciest v mapperoch. Východiskovo používa webový adresár (`%wwwDir%`). - -`baseUrl` nastavuje predvolený URL prefix pre rozlíšenie relatívnych URL v mapperoch. Východiskovo používa koreňovú URL (`%baseUrl%`). - -Možnosť `versioning` globálne riadi, či sa do URL adries assetov pridávajú parametre verzie pre cache busting. Jednotlivé mappery môžu toto nastavenie prepísať. - - -Mappery -------- - -Mappery môžu byť konfigurované tromi spôsobmi: jednoduchou reťazcovou notáciou, detailnou notáciou poľa alebo ako odkaz na službu. - -Najjednoduchší spôsob definovania mappera: - -```neon -assets: - mapping: - default: assets # Vytvorí filesystem mapper pre %wwwDir%/assets/ - images: img # Vytvorí filesystem mapper pre %wwwDir%/img/ - scripts: js # Vytvorí filesystem mapper pre %wwwDir%/js/ -``` - -Každý mapper vytvorí `FilesystemMapper`, ktorý: -- Hľadá súbory v `%wwwDir%/` -- Generuje URL adresy ako `%baseUrl%/` -- Dedí globálne nastavenie verzovania - - -Pre väčšiu kontrolu použite detailnú notáciu: - -```neon -assets: - mapping: - images: - # adresár, kde sú súbory uložené - path: ... # (string) voliteľné, predvolené na '' - - # URL prefix pre generované odkazy - url: ... # (string) voliteľné, predvolené na path - - # povoliť verzovanie pre tento mapper? - versioning: ... # (bool) voliteľné, dedí globálne nastavenie - - # automaticky pridať príponu(y) pri hľadaní súborov - extension: ... # (string|array) voliteľné, predvolené na null -``` - -Pochopenie, ako sa riešia konfiguračné hodnoty: - -Riešenie ciest: - - Relatívne cesty sa riešia z `basePath` (alebo `%wwwDir%`, ak `basePath` nie je nastavená) - - Absolútne cesty sa používajú tak, ako sú - -Riešenie URL: - - Relatívne URL sa riešia z `baseUrl` (alebo `%baseUrl%`, ak `baseUrl` nie je nastavená) - - Absolútne URL (so schémou alebo `//`) sa používajú tak, ako sú - - Ak `url` nie je špecifikovaná, použije sa hodnota `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Relatívna cesta a URL - images: - path: img # Rozlíšené na: /var/www/project/www/img - url: images # Rozlíšené na: https://example.com/assets/images - - # Absolútna cesta a URL - uploads: - path: /var/shared/uploads # Použité tak, ako je: /var/shared/uploads - url: https://cdn.example.com # Použité tak, ako je: https://cdn.example.com - - # Špecifikovaná len cesta - styles: - path: css # Cesta: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Vlastné Mappery ---------------- - -Pre vlastné mappery, odkážte alebo definujte službu: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Vite mapper vyžaduje iba pridanie `type: vite`. Toto je kompletný zoznam konfiguračných možností: - -```neon -assets: - mapping: - default: - # typ mappera (povinný pre Vite) - type: vite # (string) povinné, musí byť 'vite' - - # výstupný adresár Vite buildu - path: ... # (string) voliteľné, predvolené na '' - - # URL prefix pre vybudované assets - url: ... # (string) voliteľné, predvolené na path - - # umiestnenie súboru Vite manifestu - manifest: ... # (string) voliteľné, predvolené na /.vite/manifest.json - - # konfigurácia dev servera Vite - devServer: ... # (bool|string) voliteľné, predvolené na true - - # verzovanie pre súbory vo verejnom adresári - versioning: ... # (bool) voliteľné, dedí globálne nastavenie - - # auto-prípona pre súbory vo verejnom adresári - extension: ... # (string|array) voliteľné, predvolené na null -``` - -Možnosť `devServer` riadi, ako sa assets načítavajú počas vývoja: - -- `true` (predvolené) – Automaticky detekuje Vite dev server na aktuálnom hostiteľovi a porte. Ak dev server beží **a vaša aplikácia je v režime ladenia**, assets sa z neho načítavajú s podporou hot module replacement. Ak dev server nebeží, assets sa načítavajú z vybudovaných súborov vo verejnom adresári. -- `false` – Úplne zakáže integráciu dev servera. Assets sa vždy načítavajú z vybudovaných súborov. -- Vlastná URL (napr. `https://localhost:5173`) – Manuálne špecifikujte URL dev servera vrátane protokolu a portu. Užitočné, keď dev server beží na inom hostiteľovi alebo porte. - -Možnosti `versioning` a `extension` sa vzťahujú iba na súbory vo verejnom adresári Vite, ktoré nie sú spracované Vite. - - -Manuálna konfigurácia ---------------------- - -Ak nepoužívate Nette DI, nakonfigurujte mappery manuálne: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Pridajte filesystem mapper -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Pridajte Vite mapper -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/sl/vite.texy b/assets/sl/vite.texy deleted file mode 100644 index 39fa7d687d..0000000000 --- a/assets/sl/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Integrácia Vite -*************** - -
    - -Moderné JavaScript aplikácie vyžadujú sofistikované build nástroje. Nette Assets poskytuje prvotriednu integráciu s [Vite |https://vitejs.dev/], nástrojom na tvorbu frontendu novej generácie. Získajte bleskurýchly vývoj s Hot Module Replacement (HMR) a optimalizované produkčné buildy bez problémov s konfiguráciou. - -- **Nulová konfigurácia** – automatický most medzi Vite a PHP šablónami -- **Kompletná správa závislostí** – jeden tag spracuje všetky assets -- **Hot Module Replacement** – okamžité aktualizácie JavaScriptu a CSS -- **Optimalizované produkčné buildy** – code splitting a tree shaking - -
    - - -Nette Assets sa bezproblémovo integruje s Vite, takže získate všetky tieto výhody, zatiaľ čo svoje šablóny píšete ako obvykle. - - -Nastavenie Vite -=============== - -Poďme nastaviť Vite krok za krokom. Nebojte sa, ak ste nováčik v build nástrojoch – všetko vysvetlíme! - - -Krok 1: Inštalácia Vite ------------------------ - -Najprv nainštalujte Vite a Nette plugin do vášho projektu: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Tým sa nainštaluje Vite a špeciálny plugin, ktorý pomáha Vite perfektne fungovať s Nette. - - -Krok 2: Štruktúra projektu --------------------------- - -Štandardný prístup je umiestniť zdrojové súbory assetov do priečinka `assets/` v koreni vášho projektu a kompilované verzie do `www/assets/`: - -/--pre -web-project/ -├── assets/ ← zdrojové súbory (SCSS, TypeScript, zdrojové obrázky) -│ ├── public/ ← statické súbory (kopírované tak, ako sú) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← hlavný vstupný bod -│ └── style.css ← vaše štýly -└── www/ ← verejný adresár (document root) - ├── assets/ ← sem pôjdu kompilované súbory - └── index.php -\-- - -Priečinok `assets/` obsahuje vaše zdrojové súbory – kód, ktorý píšete. Vite spracuje tieto súbory a umiestni kompilované verzie do `www/assets/`. - - -Krok 3: Konfigurácia Vite -------------------------- - -Vytvorte súbor `vite.config.ts` v koreni vášho projektu. Tento súbor hovorí Vite, kde nájsť vaše zdrojové súbory a kam umiestniť kompilované súbory. - -Nette Vite plugin prichádza s inteligentnými predvolenými nastaveniami, ktoré zjednodušujú konfiguráciu. Predpokladá, že vaše front-end zdrojové súbory sú v adresári `assets/` (možnosť `root`) a kompilované súbory idú do `www/assets/` (možnosť `outDir`). Potrebujete špecifikovať iba [vstupný bod|#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Ak chcete špecifikovať iný názov adresára pre build vašich assetov, budete musieť zmeniť niekoľko možností: - -```js -export default defineConfig({ - root: 'assets', // koreňový adresár zdrojových assetov - - build: { - outDir: '../www/assets', // kam idú kompilované súbory - }, - - // ... iná konfigurácia ... -}); -``` - -.[note] -Cesta `outDir` sa považuje za relatívnu k `root`, preto je na začiatku `../`. - - -Krok 4: Konfigurácia Nette --------------------------- - -Povedzte Nette Assets o Vite vo vašom `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # hovorí Nette, aby použilo ViteMapper - path: assets -``` - - -Krok 5: Pridajte skripty ------------------------- - -Pridajte tieto skripty do vášho `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Teraz môžete: -- `npm run dev` – spustiť vývojový server s hot reloadingom -- `npm run build` – vytvoriť optimalizované produkčné súbory - - -Vstupné body -============ - -**Vstupný bod** je hlavný súbor, kde sa spúšťa vaša aplikácia. Z tohto súboru importujete ďalšie súbory (CSS, JavaScript moduly, obrázky), čím vytvárate strom závislostí. Vite sleduje tieto importy a všetko zbalí dohromady. - -Príklad vstupného bodu `assets/app.js`: - -```js -// Import štýlov -import './style.css' - -// Import JavaScript modulov -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Inicializujte vašu aplikáciu -netteForms.initOnLoad(); -naja.initialize(); -``` - -V šablóne môžete vložiť vstupný bod nasledovne: - -```latte -{asset 'app.js'} -``` - -Nette Assets automaticky generuje všetky potrebné HTML tagy – JavaScript, CSS a akékoľvek iné závislosti. - - -Viacero vstupných bodov ------------------------ - -Väčšie aplikácie často potrebujú samostatné vstupné body: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // verejné stránky - 'admin.js', // administrátorský panel - ], - }), - ], -}); -``` - -Použite ich v rôznych šablónach: - -```latte -{* Na verejných stránkach *} -{asset 'app.js'} - -{* V administrátorskom paneli *} -{asset 'admin.js'} -``` - - -Dôležité: Zdrojové vs. kompilované súbory ------------------------------------------ - -Je kľúčové pochopiť, že v produkcii môžete načítať iba: - -1. **Vstupné body** definované v `entry` -2. **Súbory z adresára `assets/public/`** - -**Nemôžete** načítať pomocou `{asset}` ľubovoľné súbory z `assets/` – iba assets odkazované JavaScriptovými alebo CSS súbormi. Ak váš súbor nie je nikde odkazovaný, nebude skompilovaný. Ak chcete, aby Vite vedelo o iných assets, môžete ich presunúť do [verejného priečinka |#public folder]. - -Upozorňujeme, že predvolene Vite vloží všetky assets menšie ako 4KB, takže tieto súbory nebudete môcť odkazovať priamo. (Pozri [dokumentáciu Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ Toto funguje - je to vstupný bod *} -{asset 'app.js'} - -{* ✓ Toto funguje - je to v assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ Toto nebude fungovať - náhodný súbor v assets/ *} -{asset 'components/button.js'} -``` - - -Vývojový režim -============== - -Vývojový režim je úplne voliteľný, ale pri jeho povolením poskytuje značné výhody. Hlavnou výhodou je **Hot Module Replacement (HMR)** – okamžité zobrazenie zmien bez straty stavu aplikácie, čo robí vývoj oveľa plynulejším a rýchlejším. - -Vite je moderný build nástroj, ktorý robí vývoj neuveriteľne rýchlym. Na rozdiel od tradičných bundlerov, Vite počas vývoja servíruje váš kód priamo do prehliadača, čo znamená okamžitý štart servera bez ohľadu na veľkosť vášho projektu a bleskurýchle aktualizácie. - - -Spustenie vývojového servera ----------------------------- - -Spustite vývojový server: - -```shell -npm run dev -``` - -Uvidíte: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Tento terminál nechajte otvorený počas vývoja. - -Nette Vite plugin automaticky detekuje, keď: -1. Vite dev server beží -2. Vaša Nette aplikácia je v režime ladenia - -Keď sú splnené obe podmienky, Nette Assets načíta súbory z Vite dev servera namiesto kompilovaného adresára: - -```latte -{asset 'app.js'} -{* Vo vývoji: *} -{* V produkcii: *} -``` - -Nie je potrebná žiadna konfigurácia – jednoducho to funguje! - - -Práca na rôznych doménach -------------------------- - -Ak váš vývojový server beží na niečom inom ako `localhost` (napríklad `myapp.local`), môžete naraziť na problémy s CORS (Cross-Origin Resource Sharing). CORS je bezpečnostná funkcia vo webových prehliadačoch, ktorá predvolene blokuje požiadavky medzi rôznymi doménami. Keď vaša PHP aplikácia beží na `myapp.local`, ale Vite beží na `localhost:5173`, prehliadač ich považuje za rôzne domény a blokuje požiadavky. - -Máte dve možnosti, ako to vyriešiť: - -**Možnosť 1: Konfigurácia CORS** - -Najjednoduchším riešením je povoliť cross-origin požiadavky z vašej PHP aplikácie: - -```js -export default defineConfig({ - // ... iná konfigurácia ... - - server: { - cors: { - origin: 'http://myapp.local', // URL vašej PHP aplikácie - }, - }, -}); -``` -**Možnosť 2: Spustite Vite na vašej doméne** - -Ďalším riešením je spustiť Vite na rovnakej doméne ako vaša PHP aplikácia. - -```js -export default defineConfig({ - // ... iná konfigurácia ... - - server: { - host: 'myapp.local', // rovnaké ako vaša PHP aplikácia - }, -}); -``` - -V skutočnosti aj v tomto prípade musíte nakonfigurovať CORS, pretože dev server beží na rovnakom hostname, ale na inom porte. V tomto prípade však CORS automaticky konfiguruje Nette Vite plugin. - - -Vývoj s HTTPS -------------- - -Ak vyvíjate na HTTPS, potrebujete certifikáty pre váš Vite vývojový server. Najjednoduchší spôsob je použiť plugin, ktorý automaticky generuje certifikáty: - -```shell -npm install -D vite-plugin-mkcert -``` - -Tu je návod, ako ho nakonfigurovať v `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... iná konfigurácia ... - - plugins: [ - mkcert(), // automaticky generuje certifikáty a povolí https - nette(), - ], -}); -``` - -Upozorňujeme, že ak používate konfiguráciu CORS (možnosť 1 z vyššie uvedených), musíte aktualizovať URL pôvodu, aby používala `https://` namiesto `http://`. - - -Produkčné buildy -================ - -Vytvorte optimalizované produkčné súbory: - -```shell -npm run build -``` - -Vite bude: -- Minifikovať všetok JavaScript a CSS -- Rozdeliť kód na optimálne časti -- Generovať hashované názvy súborov pre cache-busting -- Vytvoriť manifest súbor pre Nette Assets - -Príklad výstupu: - -``` -www/assets/ -├── app-4f3a2b1c.js # Váš hlavný JavaScript (minifikovaný) -├── app-7d8e9f2a.css # Extrahovaný CSS (minifikovaný) -├── vendor-8c4b5e6d.js # Zdieľané závislosti -└── .vite/ - └── manifest.json # Mapovanie pre Nette Assets -``` - -Hashované názvy súborov zaisťujú, že prehliadače vždy načítajú najnovšiu verziu. - - -Verejný priečinok -================= - -Súbory v adresári `assets/public/` sú kopírované do výstupu bez spracovania: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Odkazujte na ne normálne: - -```latte -{* Tieto súbory sú kopírované tak, ako sú *} - - -``` - -Pre verejné súbory môžete použiť funkcie FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Skúste najprv WebP - versioning: true # Pridajte cache-busting -``` - -V konfigurácii `vite.config.ts` môžete zmeniť verejný priečinok pomocou možnosti `publicDir`. - - -Dynamické importy -================= - -Vite automaticky rozdeľuje kód pre optimálne načítanie. Dynamické importy vám umožňujú načítať kód iba vtedy, keď je skutočne potrebný, čím sa znižuje počiatočná veľkosť balíka: - -```js -// Načítajte ťažké komponenty na požiadanie -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Dynamické importy vytvárajú samostatné časti, ktoré sa načítavajú iba vtedy, keď sú potrebné. Toto sa nazýva „code splitting“ a je to jedna z najvýkonnejších funkcií Vite. Keď použijete dynamické importy, Vite automaticky vytvorí samostatné JavaScript súbory pre každý dynamicky importovaný modul. - -Tag `{asset 'app.js'}` automaticky **neprednačítava** tieto dynamické časti. Toto je zámerné správanie – nechceme sťahovať kód, ktorý sa možno nikdy nepoužije. Časti sa sťahujú iba vtedy, keď sa vykoná dynamický import. - -Ak však viete, že určité dynamické importy sú kritické a budú čoskoro potrebné, môžete ich prednačítať: - -```latte -{* Hlavný vstupný bod *} -{asset 'app.js'} - -{* Prednačítajte kritické dynamické importy *} -{preload 'components/chart.js'} -``` - -Týmto sa prehliadaču povie, aby stiahol komponent grafu na pozadí, takže je okamžite pripravený, keď je potrebný. - - -Podpora TypeScriptu -=================== - -TypeScript funguje hneď po vybalení: - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Odkazujte na TypeScript súbory normálne: - -```latte -{asset 'main.ts'} -``` - -Pre plnú podporu TypeScriptu ho nainštalujte: - -```shell -npm install -D typescript -``` - - -Dodatočná konfigurácia Vite -=========================== - -Tu sú niektoré užitočné možnosti konfigurácie Vite s podrobnými vysvetleniami: - -```js -export default defineConfig({ - // Koreňový adresár obsahujúci zdrojové assets - root: 'assets', - - // Priečinok, ktorého obsah sa kopíruje do výstupného adresára tak, ako je - // Predvolené: 'public' (relatívne k 'root') - publicDir: 'public', - - build: { - // Kam umiestniť skompilované súbory (relatívne k 'root') - outDir: '../www/assets', - - // Vyprázdniť výstupný adresár pred buildom? - // Užitočné na odstránenie starých súborov z predchádzajúcich buildov - emptyOutDir: true, - - // Podadresár v rámci outDir pre generované časti a assets - // To pomáha organizovať výstupnú štruktúru - assetsDir: 'static', - - rollupOptions: { - // Vstupný(é) bod(y) - môže byť jeden súbor alebo pole súborov - // Každý vstupný bod sa stáva samostatným balíkom - input: [ - 'app.js', // hlavná aplikácia - 'admin.js', // administrátorský panel - ], - }, - }, - - server: { - // Hostiteľ, na ktorý sa má naviazať dev server - // Použite '0.0.0.0' na vystavenie do siete - host: 'localhost', - - // Port pre dev server - port: 5173, - - // Konfigurácia CORS pre cross-origin požiadavky - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Povoliť CSS source mapy vo vývoji - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -To je všetko! Teraz máte moderný build systém integrovaný s Nette Assets. diff --git a/assets/uk/@home.texy b/assets/uk/@home.texy deleted file mode 100644 index 4aaa580ff0..0000000000 --- a/assets/uk/@home.texy +++ /dev/null @@ -1,432 +0,0 @@ -Nette Assets -************ - -
    - -Втомилися вручну керувати статичними файлами у своїх веб-додатках? Забудьте про жорстке кодування шляхів, проблеми з інвалідацією кешу або турботи про версіонування файлів. Nette Assets змінює спосіб роботи з зображеннями, таблицями стилів, скриптами та іншими статичними ресурсами. - -- **Розумне версіонування** гарантує, що браузери завжди завантажують найновіші файли -- **Автоматичне визначення** типів файлів та розмірів -- **Безшовна інтеграція Latte** з інтуїтивно зрозумілими тегами -- **Гнучка архітектура**, що підтримує файлові системи, CDN та Vite -- **Ледаче завантаження** для оптимальної продуктивності - -
    - - -Чому Nette Assets? -================== - -Робота зі статичними файлами часто означає повторюваний, схильний до помилок код. Ви вручну створюєте URL-адреси, додаєте параметри версії для обходу кешу та по-різному обробляєте різні типи файлів. Це призводить до такого коду: - -```latte -Logo - -``` - -З Nette Assets вся ця складність зникає: - -```latte -{* Everything automated - URL, versioning, dimensions *} - - - -{* Or just *} -{asset 'css/style.css'} -``` - -Ось і все! Бібліотека автоматично: -- Додає параметри версії на основі часу модифікації файлу -- Визначає розміри зображень та включає їх у HTML -- Генерує правильний HTML-елемент для кожного типу файлу -- Обробляє як середовища розробки, так і виробничі середовища - - -Встановлення -============ - -Встановіть Nette Assets за допомогою [Composer|best-practices:composer]: - -```shell -composer require nette/assets -``` - -Він вимагає PHP 8.1 або вище та чудово працює з Nette Framework, але також може використовуватися автономно. - - -Перші кроки -=========== - -Nette Assets працює "з коробки" без жодної конфігурації. Розмістіть свої статичні файли в каталозі `www/assets/` і почніть їх використовувати: - -```latte -{* Display an image with automatic dimensions *} -{asset 'logo.png'} - -{* Include a stylesheet with versioning *} -{asset 'style.css'} - -{* Load a JavaScript module *} -{asset 'app.js'} -``` - -Для більшого контролю над згенерованим HTML використовуйте атрибут `n:asset` або функцію `asset()`. - - -Як це працює -============ - -Nette Assets побудовано навколо трьох основних концепцій, які роблять його потужним, але простим у використанні: - - -Активи – Ваші файли стали розумними ------------------------------------ - -**Актив** представляє будь-який статичний файл у вашому додатку. Кожен файл стає об'єктом з корисними властивостями тільки для читання: - -```php -$image = $assets->getAsset('photo.jpg'); -echo $image->url; // '/assets/photo.jpg?v=1699123456' -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' -``` - -Різні типи файлів надають різні властивості: -- **Зображення**: ширина, висота, альтернативний текст, ледаче завантаження -- **Скрипти**: тип модуля, хеші цілісності, crossorigin -- **Таблиці стилів**: медіа-запити, цілісність -- **Аудіо/Відео**: тривалість, розміри -- **Шрифти**: правильне попереднє завантаження з CORS - -Бібліотека автоматично визначає типи файлів та створює відповідний клас активу. - - -Мапери – Звідки беруться файли ------------------------------- - -**Мапер** знає, як знаходити файли та створювати для них URL-адреси. Ви можете мати кілька маперів для різних цілей – локальні файли, CDN, хмарне сховище або інструменти збірки (кожен з них має назву). Вбудований `FilesystemMapper` обробляє локальні файли, тоді як `ViteMapper` інтегрується з сучасними інструментами збірки. - -Мапери визначаються в [конфігурації |Configuration]. - - -Реєстр – Ваш основний інтерфейс -------------------------------- - -**Реєстр** керує всіма маперами та надає основний API: - -```php -// Inject the registry in your service -public function __construct( - private Nette\Assets\Registry $assets -) {} - -// Get assets from different mappers -$logo = $this->assets->getAsset('images:logo.png'); // 'image' mapper -$app = $this->assets->getAsset('app:main.js'); // 'app' mapper -$style = $this->assets->getAsset('style.css'); // uses default mapper -``` - -Реєстр автоматично вибирає правильний мапер та кешує результати для підвищення продуктивності. - - -Робота з активами в PHP -======================= - -Реєстр надає два методи для отримання активів: - -```php -// Throws Nette\Assets\AssetNotFoundException if file doesn't exist -$logo = $assets->getAsset('logo.png'); - -// Returns null if file doesn't exist -$banner = $assets->tryGetAsset('banner.jpg'); -if ($banner) { - echo $banner->url; -} -``` - - -Зазначення маперів ------------------- - -Ви можете явно вибрати, який мапер використовувати: - -```php -// Use default mapper -$file = $assets->getAsset('document.pdf'); - -// Use specific mapper with prefix -$image = $assets->getAsset('images:photo.jpg'); - -// Use specific mapper with array syntax -$script = $assets->getAsset(['scripts', 'app.js']); -``` - - -Властивості та типи активів ---------------------------- - -Кожен тип активу надає відповідні властивості тільки для читання: - -```php -// Image properties -$image = $assets->getAsset('photo.jpg'); -echo $image->width; // 1920 -echo $image->height; // 1080 -echo $image->mimeType; // 'image/jpeg' - -// Script properties -$script = $assets->getAsset('app.js'); -echo $script->type; // 'module' or null - -// Audio properties -$audio = $assets->getAsset('song.mp3'); -echo $audio->duration; // duration in seconds - -// All assets can be cast to string (returns URL) -$url = (string) $assets->getAsset('document.pdf'); -``` - -.[note] -Властивості, такі як розміри або тривалість, завантажуються ледаче лише при доступі, що забезпечує швидку роботу бібліотеки. - - -Використання активів у шаблонах Latte -===================================== - -Nette Assets надає інтуїтивно зрозумілу інтеграцію [Latte|latte:] з тегами та функціями. - - -`{asset}` ---------- - -Тег `{asset}` рендерить повні HTML-елементи: - -```latte -{* Renders: *} -{asset 'hero.jpg'} - -{* Renders: *} -{asset 'app.js'} - -{* Renders: *} -{asset 'style.css'} -``` - -Тег автоматично: -- Визначає тип активу та генерує відповідний HTML -- Включає версіонування для обходу кешу -- Додає розміри для зображень -- Встановлює правильні атрибути (type, media тощо) - -При використанні всередині HTML-атрибутів він виводить лише URL-адресу: - -```latte -
    - -``` - - -`n:asset` ---------- - -Для повного контролю над HTML-атрибутами: - -```latte -{* The n:asset attribute fills in src, dimensions, etc. *} -Product - -{* Works with any relevant element *} - - - -``` - -Використовуйте змінні та мапери: - -```latte -{* Variables work naturally *} - - -{* Specify mapper with curly brackets *} - - -{* Specify mapper with array notation *} - -``` - - -`asset()` ---------- - -Для максимальної гнучкості використовуйте функцію `asset()`: - -```latte -{var $logo = asset('logo.png')} -width} height={$logo->height}> - -{* Or directly *} -Logo -``` - - -Необов'язкові активи --------------------- - -Обробляйте відсутні активи елегантно за допомогою `{asset?}`, `n:asset?` та `tryAsset()`: - -```latte -{* Optional tag - renders nothing if asset missing *} -{asset? 'optional-banner.jpg'} - -{* Optional attribute - skips if asset missing *} -Avatar - -{* With fallback *} -{var $avatar = tryAsset('user-avatar.jpg') ?? asset('default-avatar.jpg')} -Avatar -``` - - -`{preload}` ------------ - -Покращити продуктивність завантаження сторінки: - -```latte -{* In your section *} -{preload 'critical.css'} -{preload 'important-font.woff2'} -{preload 'hero-image.jpg'} -``` - -Генерує відповідні посилання для попереднього завантаження: - -```latte - - - -``` - - -Розширені можливості -==================== - - -Автоматичне визначення розширень --------------------------------- - -Автоматично обробляти кілька форматів: - -```neon -assets: - mapping: - images: - path: img - extension: [webp, jpg, png] # Try in order -``` - -Тепер ви можете запитувати без розширення: - -```latte -{* Finds logo.webp, logo.jpg, or logo.png automatically *} -{asset 'images:logo'} -``` - -Ідеально підходить для прогресивного покращення з сучасними форматами. - - -Розумне версіонування ---------------------- - -Файли автоматично версіонуються на основі часу модифікації: - -```latte -{asset 'style.css'} -{* Output: *} -``` - -Коли ви оновлюєте файл, мітка часу змінюється, що примушує браузер оновити кеш. - -Контролювати версіонування для кожного активу: - -```php -// Disable versioning for specific asset -$asset = $assets->getAsset('style.css', ['version' => false]); - -// In Latte -{asset 'style.css', version: false} -``` - - -Активи шрифтів --------------- - -Шрифти отримують особливу обробку з правильним CORS: - -```latte -{* Proper preload with crossorigin *} -{preload 'fonts:OpenSans-Regular.woff2'} - -{* Use in CSS *} - -``` - - -Користувацькі мапери -==================== - -Створюйте користувацькі мапери для особливих потреб, таких як хмарне сховище або динамічна генерація: - -```php -use Nette\Assets\Mapper; -use Nette\Assets\Asset; -use Nette\Assets\Helpers; - -class CloudStorageMapper implements Mapper -{ - public function __construct( - private CloudClient $client, - private string $bucket, - ) {} - - public function getAsset(string $reference, array $options = []): Asset - { - if (!$this->client->exists($this->bucket, $reference)) { - throw new Nette\Assets\AssetNotFoundException("Asset '$reference' not found"); - } - - $url = $this->client->getPublicUrl($this->bucket, $reference); - return Helpers::createAssetFromUrl($url); - } -} -``` - -Зареєструвати в конфігурації: - -```neon -assets: - mapping: - cloud: CloudStorageMapper(@cloudClient, 'my-bucket') -``` - -Використовувати як будь-який інший мапер: - -```latte -{asset 'cloud:user-uploads/photo.jpg'} -``` - -Метод `Helpers::createAssetFromUrl()` автоматично створює правильний тип активу на основі розширення файлу. - - -Читати далі -=========== - -- [Nette Assets: Нарешті уніфікований API для всього, від зображень до Vite |https://blog.nette.org/en/introducing-nette-assets] diff --git a/assets/uk/@left-menu.texy b/assets/uk/@left-menu.texy deleted file mode 100644 index 2461434065..0000000000 --- a/assets/uk/@left-menu.texy +++ /dev/null @@ -1,5 +0,0 @@ -Nette Assets -************ -- [Початок роботи |@home] -- [Vite |vite] -- [Конфігурація |Configuration] diff --git a/assets/uk/@meta.texy b/assets/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/assets/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/assets/uk/configuration.texy b/assets/uk/configuration.texy deleted file mode 100644 index 712ddbf2b7..0000000000 --- a/assets/uk/configuration.texy +++ /dev/null @@ -1,188 +0,0 @@ -Конфігурація активів -******************** - -.[perex] -Огляд опцій конфігурації для Nette Assets. - - -```neon -assets: - # base path for resolving relative mapper paths - basePath: ... # (string) defaults to %wwwDir% - - # base URL for resolving relative mapper URLs - baseUrl: ... # (string) defaults to %baseUrl% - - # enable asset versioning globally? - versioning: ... # (bool) defaults to true - - # defines asset mappers - mapping: ... # (array) defaults to path 'assets' -``` - -`basePath` встановлює типовий каталог файлової системи для розв'язання відносних шляхів у маперах. За замовчуванням він використовує веб-каталог (`%wwwDir%`). - -`baseUrl` встановлює типовий префікс URL для розв'язання відносних URL у маперах. За замовчуванням він використовує кореневий URL (`%baseUrl%`). - -Опція `versioning` глобально контролює, чи додаються параметри версії до URL-адрес активів для обходу кешу. Окремі мапери можуть перевизначати це налаштування. - - -Мапери ------- - -Мапери можуть бути налаштовані трьома способами: проста рядкова нотація, детальна масивна нотація або як посилання на сервіс. - -Найпростіший спосіб визначити мапер: - -```neon -assets: - mapping: - default: assets # Creates filesystem mapper for %wwwDir%/assets/ - images: img # Creates filesystem mapper for %wwwDir%/img/ - scripts: js # Creates filesystem mapper for %wwwDir%/js/ -``` - -Кожен мапер створює `FilesystemMapper`, який: -- Шукає файли в `%wwwDir%/` -- Генерує URL-адреси, такі як `%baseUrl%/` -- Успадковує глобальні налаштування версіонування - - -Для більшого контролю використовуйте детальну нотацію: - -```neon -assets: - mapping: - images: - # directory where files are stored - path: ... # (string) optional, defaults to '' - - # URL prefix for generated links - url: ... # (string) optional, defaults to path - - # enable versioning for this mapper? - versioning: ... # (bool) optional, inherits global setting - - # auto-add extension(s) when searching for files - extension: ... # (string|array) optional, defaults to null -``` - -Розуміння того, як розв'язуються значення конфігурації: - -Розв'язання шляхів: - - Відносні шляхи розв'язуються з `basePath` (або `%wwwDir%`, якщо `basePath` не встановлено) - - Абсолютні шляхи використовуються як є - -Розв'язання URL: - - Відносні URL-адреси розв'язуються з `baseUrl` (або `%baseUrl%`, якщо `baseUrl` не встановлено) - - Абсолютні URL-адреси (зі схемою або `//`) використовуються як є - - Якщо `url` не вказано, використовується значення `path` - - -```neon -assets: - basePath: /var/www/project/www - baseUrl: https://example.com/assets - - mapping: - # Relative path and URL - images: - path: img # Resolved to: /var/www/project/www/img - url: images # Resolved to: https://example.com/assets/images - - # Absolute path and URL - uploads: - path: /var/shared/uploads # Used as-is: /var/shared/uploads - url: https://cdn.example.com # Used as-is: https://cdn.example.com - - # Only path specified - styles: - path: css # Path: /var/www/project/www/css - # URL: https://example.com/assets/css -``` - - -Користувацькі мапери --------------------- - -Для користувацьких маперів посилайтеся або визначте сервіс: - -```neon -services: - s3mapper: App\Assets\S3Mapper(%s3.bucket%) - -assets: - mapping: - cloud: @s3mapper - database: App\Assets\DatabaseMapper(@database.connection) -``` - - -Vite Mapper ------------ - -Мапер Vite вимагає лише додати `type: vite`. Це повний список опцій конфігурації: - -```neon -assets: - mapping: - default: - # mapper type (required for Vite) - type: vite # (string) required, must be 'vite' - - # Vite build output directory - path: ... # (string) optional, defaults to '' - - # URL prefix for built assets - url: ... # (string) optional, defaults to path - - # location of Vite manifest file - manifest: ... # (string) optional, defaults to /.vite/manifest.json - - # Vite dev server configuration - devServer: ... # (bool|string) optional, defaults to true - - # versioning for public directory files - versioning: ... # (bool) optional, inherits global setting - - # auto-extension for public directory files - extension: ... # (string|array) optional, defaults to null -``` - -Опція `devServer` контролює, як активи завантажуються під час розробки: - -- `true` (за замовчуванням) - Автоматично виявляє dev-сервер Vite на поточному хості та порту. Якщо dev-сервер запущений **і ваш додаток знаходиться в режимі налагодження**, активи завантажуються з нього з підтримкою гарячої заміни модулів. Якщо dev-сервер не запущений, активи завантажуються з збудованих файлів у публічному каталозі. -- `false` - Повністю вимикає інтеграцію dev-сервера. Активи завжди завантажуються з збудованих файлів. -- Користувацький URL (наприклад, `https://localhost:5173`) - Вручну вказує URL dev-сервера, включаючи протокол та порт. Корисно, коли dev-сервер працює на іншому хості або порту. - -Опції `versioning` та `extension` застосовуються лише до файлів у публічному каталозі Vite, які не обробляються Vite. - - -Ручна конфігурація ------------------- - -Якщо не використовуєте Nette DI, налаштуйте мапери вручну: - -```php -use Nette\Assets\Registry; -use Nette\Assets\FilesystemMapper; -use Nette\Assets\ViteMapper; - -$registry = new Registry; - -// Add filesystem mapper -$registry->addMapper('images', new FilesystemMapper( - baseUrl: 'https://example.com/img', - basePath: __DIR__ . '/www/img', - extensions: ['webp', 'jpg', 'png'], - versioning: true, -)); - -// Add Vite mapper -$registry->addMapper('app', new ViteMapper( - baseUrl: '/build', - basePath: __DIR__ . '/www/build', - manifestPath: __DIR__ . '/www/build/.vite/manifest.json', - devServer: 'https://localhost:5173', -)); -``` diff --git a/assets/uk/vite.texy b/assets/uk/vite.texy deleted file mode 100644 index fe44373505..0000000000 --- a/assets/uk/vite.texy +++ /dev/null @@ -1,508 +0,0 @@ -Інтеграція Vite -*************** - -
    - -Сучасні JavaScript-додатки вимагають складних інструментів збірки. Nette Assets надає першокласну інтеграцію з [Vite |https://vitejs.dev/], інструментом збірки фронтенду нового покоління. Отримайте блискавично швидку розробку з Hot Module Replacement (HMR) та оптимізовані виробничі збірки без проблем з конфігурацією. - -- **Нульова конфігурація** – автоматичний міст між Vite та PHP-шаблонами -- **Повне управління залежностями** – один тег обробляє всі активи -- **Гаряча заміна модулів** – миттєві оновлення JavaScript та CSS -- **Оптимізовані виробничі збірки** – розділення коду та tree shaking - -
    - - -Nette Assets бездоганно інтегрується з Vite, тому ви отримуєте всі ці переваги, пишучи свої шаблони як зазвичай. - - -Налаштування Vite -================= - -Давайте налаштуємо Vite крок за кроком. Не хвилюйтеся, якщо ви новачок в інструментах збірки – ми все пояснимо! - - -Крок 1: Встановіть Vite ------------------------ - -Спершу встановіть Vite та плагін Nette у вашому проекті: - -```shell -npm install -D vite @nette/vite-plugin -``` - -Це встановлює Vite та спеціальний плагін, який допомагає Vite ідеально працювати з Nette. - - -Крок 2: Структура проекту -------------------------- - -Стандартний підхід полягає в розміщенні вихідних файлів активів у папці `assets/` у корені вашого проекту, а скомпільованих версій – у `www/assets/`: - -/--pre -web-project/ -├── assets/ ← source files (SCSS, TypeScript, source images) -│ ├── public/ ← static files (copied as-is) -│ │ └── favicon.ico -│ ├── images/ -│ │ └── logo.png -│ ├── app.js ← main entry point -│ └── style.css ← your styles -└── www/ ← public directory (document root) - ├── assets/ ← compiled files will go here - └── index.php -\-- - -Папка `assets/` містить ваші вихідні файли – код, який ви пишете. Vite обробить ці файли та помістить скомпільовані версії в `www/assets/`. - - -Крок 3: Налаштуйте Vite ------------------------ - -Створіть файл `vite.config.ts` у корені вашого проекту. Цей файл вказує Vite, де шукати ваші вихідні файли та куди поміщати скомпільовані. - -Плагін Nette Vite поставляється з розумними значеннями за замовчуванням, які спрощують конфігурацію. Він припускає, що ваші вихідні файли фронтенду знаходяться в каталозі `assets/` (опція `root`), а скомпільовані файли потрапляють до `www/assets/` (опція `outDir`). Вам потрібно лише вказати [точку входу |#Entry Points]: - -```js -import { defineConfig } from 'vite'; -import nette from '@nette/vite-plugin'; - -export default defineConfig({ - plugins: [ - nette({ - entry: 'app.js', - }), - ], -}); -``` - -Якщо ви хочете вказати іншу назву каталогу для збірки ваших активів, вам потрібно буде змінити кілька опцій: - -```js -export default defineConfig({ - root: 'assets', // root directory of source assets - - build: { - outDir: '../www/assets', // where compiled files go - }, - - // ... other config ... -}); -``` - -.[note] -Шлях `outDir` вважається відносним до `root`, тому на початку є `../`. - - -Крок 4: Налаштуйте Nette ------------------------- - -Повідомте Nette Assets про Vite у вашому `common.neon`: - -```neon -assets: - mapping: - default: - type: vite # tells Nette to use the ViteMapper - path: assets -``` - - -Крок 5: Додайте скрипти ------------------------ - -Додайте ці скрипти до вашого `package.json`: - -```json -{ - "scripts": { - "dev": "vite", - "build": "vite build" - } -} -``` - -Тепер ви можете: -- `npm run dev` - запустити dev-сервер з гарячою перезавантаженням -- `npm run build` - створити оптимізовані файли для продакшену - - -Точки входу -=========== - -**Точка входу** – це головний файл, з якого починається ваш додаток. З цього файлу ви імпортуєте інші файли (CSS, модулі JavaScript, зображення), створюючи дерево залежностей. Vite слідує цим імпортам і об'єднує все разом. - -Приклад точки входу `assets/app.js`: - -```js -// Import styles -import './style.css' - -// Import JavaScript modules -import netteForms from 'nette-forms'; -import naja from 'naja'; - -// Initialize your application -netteForms.initOnLoad(); -naja.initialize(); -``` - -У шаблоні ви можете вставити точку входу наступним чином: - -```latte -{asset 'app.js'} -``` - -Nette Assets автоматично генерує всі необхідні HTML-теги – JavaScript, CSS та будь-які інші залежності. - - -Кілька точок входу ------------------- - -Більші додатки часто потребують окремих точок входу: - -```js -export default defineConfig({ - plugins: [ - nette({ - entry: [ - 'app.js', // public pages - 'admin.js', // admin panel - ], - }), - ], -}); -``` - -Використовуйте їх у різних шаблонах: - -```latte -{* In public pages *} -{asset 'app.js'} - -{* In admin panel *} -{asset 'admin.js'} -``` - - -Важливо: Вихідні проти скомпільованих файлів --------------------------------------------- - -Важливо розуміти, що на продакшені ви можете завантажувати лише: - -1. **Точки входу**, визначені в `entry` -2. **Файли з каталогу `assets/public/`** - -Ви **не можете** завантажувати за допомогою `{asset}` довільні файли з `assets/` – лише активи, на які посилаються файли JavaScript або CSS. Якщо на ваш файл ніде немає посилання, він не буде скомпільований. Якщо ви хочете, щоб Vite знав про інші активи, ви можете перемістити їх до [публічної папки |#public folder]. - -Зверніть увагу, що за замовчуванням Vite вбудовуватиме всі активи розміром менше 4 КБ, тому ви не зможете посилатися на ці файли безпосередньо. (Див. [документацію Vite |https://vite.dev/guide/assets.html]). - -```latte -{* ✓ This works - it's an entry point *} -{asset 'app.js'} - -{* ✓ This works - it's in assets/public/ *} -{asset 'favicon.ico'} - -{* ✗ This won't work - random file in assets/ *} -{asset 'components/button.js'} -``` - - -Режим розробки -============== - -Режим розробки є повністю необов'язковим, але надає значні переваги при увімкненні. Головна перевага – це **Гаряча заміна модулів (HMR)** – миттєве відображення змін без втрати стану програми, що робить процес розробки набагато плавніше та швидше. - -Vite – це сучасний інструмент збірки, який робить розробку неймовірно швидкою. На відміну від традиційних бандлерів, Vite під час розробки подає ваш код безпосередньо в браузер, що означає миттєвий запуск сервера незалежно від розміру вашого проекту та блискавичні оновлення. - - -Запуск dev-сервера ------------------- - -Запустіть dev-сервер: - -```shell -npm run dev -``` - -Ви побачите: - -``` - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose -``` - -Залишайте цей термінал відкритим під час розробки. - -Плагін Nette Vite автоматично виявляє, коли: -1. Vite dev-сервер запущений -2. Ваш Nette-додаток знаходиться в режимі налагодження - -Коли обидві умови виконані, Nette Assets завантажує файли з dev-сервера Vite замість скомпільованого каталогу: - -```latte -{asset 'app.js'} -{* In development: *} -{* In production: *} -``` - -Конфігурація не потрібна – просто працює! - - -Робота на різних доменах ------------------------- - -Якщо ваш dev-сервер працює не на `localhost` (наприклад, `myapp.local`), ви можете зіткнутися з проблемами CORS (Cross-Origin Resource Sharing). CORS – це функція безпеки у веб-браузерах, яка за замовчуванням блокує запити між різними доменами. Коли ваш PHP-додаток працює на `myapp.local`, а Vite – на `localhost:5173`, браузер розглядає їх як різні домени та блокує запити. - -У вас є два варіанти вирішення цієї проблеми: - -**Варіант 1: Налаштуйте CORS** - -Найпростіше рішення – дозволити крос-доменні запити з вашого PHP-додатку: - -```js -export default defineConfig({ - // ... other config ... - - server: { - cors: { - origin: 'http://myapp.local', // URL вашого PHP-додатку - }, - }, -}); -``` -**Варіант 2: Запустіть Vite на вашому домені** - -Інше рішення – змусити Vite працювати на тому ж домені, що й ваш PHP-додаток. - -```js -export default defineConfig({ - // ... other config ... - - server: { - host: 'myapp.local', // те саме, що й ваш PHP-додаток - }, -}); -``` - -Насправді, навіть у цьому випадку вам потрібно налаштувати CORS, оскільки dev-сервер працює на тому ж хості, але на іншому порту. Однак у цьому випадку CORS автоматично налаштовується плагіном Nette Vite. - - -Розробка HTTPS --------------- - -Якщо ви розробляєте на HTTPS, вам потрібні сертифікати для вашого dev-сервера Vite. Найпростіший спосіб – використовувати плагін, який автоматично генерує сертифікати: - -```shell -npm install -D vite-plugin-mkcert -``` - -Ось як налаштувати його в `vite.config.ts`: - -```js -import mkcert from 'vite-plugin-mkcert'; - -export default defineConfig({ - // ... other config ... - - plugins: [ - mkcert(), // generates certificates automatically and enables https - nette(), - ], -}); -``` - -Зверніть увагу, що якщо ви використовуєте конфігурацію CORS (Варіант 1 вище), вам потрібно оновити URL походження, щоб використовувати `https://` замість `http://`. - - -Продакшен збірки -================ - -Створіть оптимізовані файли для продакшену: - -```shell -npm run build -``` - -Vite зробить: -- Мініфікувати весь JavaScript та CSS -- Розділити код на оптимальні чанки -- Згенерувати хешовані імена файлів для обходу кешу -- Створити файл маніфесту для Nette Assets - -Приклад виводу: - -``` -www/assets/ -├── app-4f3a2b1c.js # Your main JavaScript (minified) -├── app-7d8e9f2a.css # Extracted CSS (minified) -├── vendor-8c4b5e6d.js # Shared dependencies -└── .vite/ - └── manifest.json # Mapping for Nette Assets -``` - -Хешовані імена файлів гарантують, що браузери завжди завантажують найновішу версію. - - -Публічна папка -============== - -Файли в каталозі `assets/public/` копіюються у вихідний каталог без обробки: - -``` -assets/ -├── public/ -│ ├── favicon.ico -│ ├── robots.txt -│ └── images/ -│ └── og-image.jpg -├── app.js -└── style.css -``` - -Посилайтеся на них звичайно: - -```latte -{* These files are copied as-is *} - - -``` - -Для публічних файлів можна використовувати функції FilesystemMapper: - -```neon -assets: - mapping: - default: - type: vite - path: assets - extension: [webp, jpg, png] # Try WebP first - versioning: true # Add cache-busting -``` - -У конфігурації `vite.config.ts` ви можете змінити публічну папку за допомогою опції `publicDir`. - - -Динамічні імпорти -================= - -Vite автоматично розділяє код для оптимального завантаження. Динамічні імпорти дозволяють завантажувати код лише тоді, коли він дійсно потрібен, зменшуючи початковий розмір бандлу: - -```js -// Load heavy components on demand -button.addEventListener('click', async () => { - let { Chart } = await import('./components/chart.js') - new Chart(data) -}) -``` - -Динамічні імпорти створюють окремі чанки, які завантажуються лише за потреби. Це називається "розділення коду" (code splitting) і є однією з найпотужніших функцій Vite. Коли ви використовуєте динамічні імпорти, Vite автоматично створює окремі файли JavaScript для кожного динамічно імпортованого модуля. - -Тег `{asset 'app.js'}` **не** попередньо завантажує ці динамічні чанки автоматично. Це навмисна поведінка – ми не хочемо завантажувати код, який може ніколи не використовуватися. Чанки завантажуються лише тоді, коли виконується динамічний імпорт. - -Однак, якщо ви знаєте, що певні динамічні імпорти є критичними і знадобляться незабаром, ви можете попередньо завантажити їх: - -```latte -{* Main entry point *} -{asset 'app.js'} - -{* Preload critical dynamic imports *} -{preload 'components/chart.js'} -``` - -Це вказує браузеру завантажити компонент діаграми у фоновому режимі, щоб він був готовий негайно, коли це знадобиться. - - -Підтримка TypeScript -==================== - -TypeScript працює "з коробки": - -```ts -// assets/main.ts -interface User { - name: string - email: string -} - -export function greetUser(user: User): void { - console.log(`Hello, ${user.name}!`) -} -``` - -Посилайтеся на файли TypeScript звичайно: - -```latte -{asset 'main.ts'} -``` - -Для повної підтримки TypeScript встановіть його: - -```shell -npm install -D typescript -``` - - -Додаткова конфігурація Vite -=========================== - -Ось деякі корисні опції конфігурації Vite з детальними поясненнями: - -```js -export default defineConfig({ - // Root directory containing source assets - root: 'assets', - - // Folder whose contents are copied to output directory as-is - // Default: 'public' (relative to 'root') - publicDir: 'public', - - build: { - // Where to put compiled files (relative to 'root') - outDir: '../www/assets', - - // Empty output directory before building? - // Useful to remove old files from previous builds - emptyOutDir: true, - - // Subdirectory within outDir for generated chunks and assets - // This helps organize the output structure - assetsDir: 'static', - - rollupOptions: { - // Entry point(s) - can be a single file or array of files - // Each entry point becomes a separate bundle - input: [ - 'app.js', // main application - 'admin.js', // admin panel - ], - }, - }, - - server: { - // Host to bind the dev server to - // Use '0.0.0.0' to expose to network - host: 'localhost', - - // Port for the dev server - port: 5173, - - // CORS configuration for cross-origin requests - cors: { - origin: 'http://myapp.local', - }, - }, - - css: { - // Enable CSS source maps in development - devSourcemap: true, - }, - - plugins: [ - nette(), - ], -}); -``` - -Ось і все! Тепер у вас є сучасна система збірки, інтегрована з Nette Assets. diff --git a/best-practices/bg/@home.texy b/best-practices/bg/@home.texy deleted file mode 100644 index 808c4d31b7..0000000000 --- a/best-practices/bg/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Ръководства и процедури -*********************** - -.[perex] -Ръководства, решения на често срещани задачи и *добри практики* за Nette. - - -
    -
    - - -Nette Приложения ----------------- -- [Методи и атрибути inject |inject-method-attribute] -- [Съставяне на презентери от trait |presenter-traits] -- [Предаване на настройки към презентери |passing-settings-to-presenters] -- [Как да се върнем към предишна страница |restore-request] -- [Странициране на резултати от база данни |pagination] -- [Динамични снипети |dynamic-snippets] -- [Как да използваме атрибута #Requires |attribute-requires] -- [Как правилно да използваме POST връзки |post-links] - -
    -
    - - -Форми ------ -- [Повторно използване на форми |form-reuse] -- [Форма за създаване и редактиране на запис |creating-editing-form] -- [Създаваме контактна форма |lets-create-contact-form] -- [Зависими селектбокси |https://blog.nette.org/bg/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -
    -
    - - -Общи ----- -- [Как да заредим конфигурационен файл |bootstrap:] -- [Как да пишем микро-уебсайтове |microsites] -- [Защо Nette използва PascalCase нотация за константи? |https://blog.nette.org/bg/for-less-screaming-in-the-code] -- [Защо Nette не използва суфикс Interface? |https://blog.nette.org/bg/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: съвети за използване |composer] -- [Съвети за редактори & инструменти |editors-and-tools] -- [Въведение в обектно-ориентираното програмиране |nette:introduction-to-object-oriented-programming] - -
    -
    - - -Примерни решения ----------------- -- [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] -- [Бърз старт |quickstart:] - -
    -
    - - -Видеа ------ -Стотици записи от Posledních sobot и видеа за Nette можете да намерите под един покрив в "Youtube канала на Nette Framework":https://www.youtube.com/user/NetteFramework. - -
    -
    diff --git a/best-practices/bg/@meta.texy b/best-practices/bg/@meta.texy deleted file mode 100644 index dc4e6c5b2b..0000000000 --- a/best-practices/bg/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Ръководства и процедури}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/bg/attribute-requires.texy b/best-practices/bg/attribute-requires.texy deleted file mode 100644 index c779502d44..0000000000 --- a/best-practices/bg/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Как да използваме атрибута `#[Requires]` -**************************************** - -.[perex] -Когато пишете уеб приложение, често се сблъсквате с необходимостта да ограничите достъпа до определени части от вашето приложение. Може би искате някои заявки да могат да изпращат данни само чрез формуляр (т.е. с метод POST), или да бъдат достъпни само за AJAX извиквания. В Nette Framework 3.2 се появи нов инструмент, който ви позволява да настроите такива ограничения много елегантно и прегледно: атрибутът `#[Requires]`. - -Атрибутът е специална маркировка в PHP, която добавяте преди дефиницията на клас или метод. Тъй като всъщност е клас, за да работят следващите примери, е необходимо да се посочи клаузата use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Атрибутът `#[Requires]` можете да използвате при самия клас на презентера, както и на тези методи: - -- `action()` -- `render()` -- `handle()` -- `createComponent()` - -Последните два метода се отнасят и до компоненти, т.е. атрибутът можете да използвате и при тях. - -Ако не са изпълнени условията, които атрибутът посочва, ще се предизвика HTTP грешка 4xx. - - -Методи HTTP ------------ - -Можете да специфицирате кои HTTP методи (като GET, POST и т.н.) са разрешени за достъп. Например, ако искате да разрешите достъп само чрез изпращане на формуляр, настройте: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Защо трябва да използвате POST вместо GET за действия, променящи състоянието, и как да го направите? [Прочетете ръководството |post-links]. - -Можете да посочите метод или масив от методи. Специален случай е стойността `'*'`, която разрешава всички методи, което стандартно презентерите [от съображения за сигурност не позволяват |application:presenters#Проверка на HTTP метода]. - - -AJAX извикване --------------- - -Ако искате презентерът или методът да бъдат достъпни само за AJAX заявки, използвайте: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Същият произход ---------------- - -За повишаване на сигурността можете да изисквате заявката да бъде направена от същия домейн. С това предотвратявате [уязвимостта CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -При методите `handle()` достъпът от същия домейн се изисква автоматично. Така че, ако обратно искате да разрешите достъп от всеки домейн, посочете: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Достъп чрез forward -------------------- - -Понякога е полезно да се ограничи достъпът до презентера така, че да бъде достъпен само непряко, например с използването на метода `forward()` или `switch()` от друг презентер. Така например се защитават error-presenter-ите, за да не могат да бъдат извикани от URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -На практика често е необходимо да се маркират определени views, до които може да се стигне едва въз основа на логиката в презентера. Тоест отново, за да не могат да бъдат отворени директно: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Конкретни действия ------------------- - -Можете също така да ограничите, че определен код, например създаване на компонент, ще бъде достъпен само за специфични действия в презентера: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -В случай на едно действие не е необходимо да се записва масив: `#[Requires(actions: 'default')]` - - -Собствени атрибути ------------------- - -Ако искате да използвате атрибута `#[Requires]` многократно със същите настройки, можете да си създадете собствен атрибут, който ще наследява `#[Requires]` и ще го настрои според нуждите. - -Например `#[SingleAction]` ще позволи достъп само чрез действието `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Или `#[RestMethods]` ще позволи достъп чрез всички HTTP методи, използвани за REST API: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Заключение ----------- - -Атрибутът `#[Requires]` ви дава голяма гъвкавост и контрол върху това как са достъпни вашите уеб страници. С помощта на прости, но мощни правила можете да повишите сигурността и правилното функциониране на вашето приложение. Както виждате, използването на атрибути в Nette може не само да улесни вашата работа, но и да я обезопаси. diff --git a/best-practices/bg/composer.texy b/best-practices/bg/composer.texy deleted file mode 100644 index 2346c9485f..0000000000 --- a/best-practices/bg/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: съвети за употреба -**************************** - -
    - -Composer е инструмент за управление на зависимости в PHP. Позволява ни да изброим библиотеките, от които зависи нашият проект, и ще ги инсталира и актуализира вместо нас. Ще покажем: - -- как да инсталираме Composer -- неговото използване в нов или съществуващ проект - -
    - - -Инсталация -========== - -Composer е изпълним `.phar` файл, който изтегляте и инсталирате по следния начин: - - -Windows -------- - -Използвайте официалния инсталатор [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Достатъчни са 4 команди, които копирайте от [тази страница |https://getcomposer.org/download/]. - -Освен това, като го поставите в папка, която е в системния `PATH`, Composer става достъпен глобално: - -```shell -$ mv ./composer.phar ~/bin/composer # или /usr/local/bin/composer -``` - - -Използване в проект -=================== - -За да можем да започнем да използваме Composer в нашия проект, се нуждаем само от файл `composer.json`. Той описва зависимостите на нашия проект и може също да съдържа други метаданни. Основният `composer.json` следователно може да изглежда така: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Тук казваме, че нашето приложение (или библиотека) изисква пакета `nette/database` (името на пакета се състои от името на организацията и името на проекта) и иска версия, която отговаря на условието `^3.0` (т.е. най-новата версия 3). - -Имаме следователно в корена на проекта файл `composer.json` и стартираме инсталацията: - -```shell -composer update -``` - -Composer ще изтегли Nette Database в папката `vendor/`. Освен това ще създаде файл `composer.lock`, който съдържа информация за това кои версии на библиотеките точно е инсталирал. - -Composer генерира файл `vendor/autoload.php`, който можем лесно да включим и да започнем да използваме библиотеките без никаква друга работа: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Актуализация на пакетите до най-новите версии -============================================= - -Актуализацията на използваните библиотеки до най-новите версии според условията, дефинирани в `composer.json`, се извършва от командата `composer update`. Напр. при зависимост `"nette/database": "^3.0"` ще инсталира най-новата версия 3.x.x, но не и версия 4. - -За актуализация на условията във файла `composer.json`, например на `"nette/database": "^4.1"`, за да може да се инсталира най-новата версия, използвайте командата `composer require nette/database`. - -За актуализация на всички използвани пакети на Nette би било необходимо всички те да се изброят в командния ред, напр.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Което е непрактично. Затова използвайте простия скрипт "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, който ще го направи вместо вас: - -```shell -php composer-frontline.php -``` - - -Създаване на нов проект -======================= - -Нов проект на Nette създавате с една-единствена команда: - -```shell -composer create-project nette/web-project име-на-проекта -``` - -Като `име-на-проекта` въведете името на директорията за вашия проект и потвърдете. Composer ще изтегли хранилището `nette/web-project` от GitHub, което вече съдържа файл `composer.json`, и веднага след това Nette Framework. Трябва вече да е достатъчно само да [настроите правата |nette:troubleshooting#Настройка на правата на директориите] за запис в папките `temp/` и `log/` и проектът трябва да оживее. - -Ако знаете на коя версия на PHP ще бъде хостван проектът, не забравяйте [да я настроите |#Версия на PHP]. - - -Версия на PHP -============= - -Composer винаги инсталира тези версии на пакетите, които са съвместими с версията на PHP, която в момента използвате (по-точно с версията на PHP, използвана в командния ред при стартиране на Composer). Което обаче най-вероятно не е същата версия, която използва вашият хостинг. Затова е много важно да добавите в файла `composer.json` информация за версията на PHP на хостинга. След това ще се инсталират само версии на пакетите, съвместими с хостинга. - -Това, че проектът ще работи например на PHP 8.2.3, настройваме с командата: - -```shell -composer config platform.php 8.2.3 -``` - -Така версията се записва във файла `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Въпреки това, номерът на версията на PHP се посочва и на друго място във файла, а именно в секцията `require`. Докато първото число определя за коя версия ще се инсталират пакетите, второто число казва за коя версия е написано самото приложение. И според него например PhpStorm настройва *PHP language level*. (Разбира се, няма смисъл тези версии да се различават, така че двойният запис е недомислица.) Тази версия настройвате с командата: - -```shell -composer require php 8.2.3 --no-update -``` - -Или директно във файла `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Игнориране на версията на PHP -============================= - -Пакетите обикновено имат посочена както най-ниската версия на PHP, с която са съвместими, така и най-високата, с която са тествани. Ако се готвите да използвате версия на PHP още по-нова, например с цел тестване, Composer ще откаже да инсталира такъв пакет. Решението е опцията `--ignore-platform-req=php+`, която кара Composer да игнорира горните граници на изискваната версия на PHP. - - -Фалшиви съобщения -================= - -При надграждане на пакети или промени в номерата на версиите се случва да възникне конфликт. Един пакет има изисквания, които са в противоречие с друг и подобни. Composer обаче понякога изписва фалшиви съобщения. Съобщава за конфликт, който реално не съществува. В такъв случай помага да се изтрие файлът `composer.lock` и да се опита отново. - -Ако съобщението за грешка продължава, тогава е сериозно и трябва да се разчете от него какво и как да се промени. - - -Packagist.org - централно хранилище -=================================== - -[Packagist |https://packagist.org] е основното хранилище, в което Composer се опитва да търси пакети, ако не му кажем друго. Тук можем да публикуваме и собствени пакети. - - -Какво, ако не искаме да използваме централното хранилище? ---------------------------------------------------------- - -Ако имаме вътрешнофирмени приложения, които просто не можем да хостваме публично, тогава ще си създадем фирмено хранилище за тях. - -Повече по темата за хранилищата [в официалната документация |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Ключова характеристика на Composer е, че предоставя autoloading за всички инсталирани от него класове, който стартирате, като включите файла `vendor/autoload.php`. - -Въпреки това е възможно да използвате Composer и за зареждане на други класове и извън папката `vendor`. Първата възможност е да оставите Composer да претърси дефинираните папки и подпапки, да намери всички класове и да ги включи в autoloader-а. Това постигате, като настроите `autoload > classmap` в `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # включва папката src/ и нейните подпапки - ] - } -} -``` - -След това е необходимо при всяка промяна да се стартира командата `composer dumpautoload` и да се оставят autoloading таблиците да се прегенерират. Това е изключително неудобно и далеч по-добре е тази задача да се повери на [RobotLoader|robot-loader:], който извършва същата дейност автоматично във фонов режим и много по-бързо. - -Втората възможност е да се спазва [PSR-4|https://www.php-fig.org/psr/psr-4/]. Опростено казано, става въпрос за система, при която именните пространства и имената на класовете съответстват на директорийната структура и имената на файловете, т.е. напр. `App\Core\RouterFactory` ще бъде във файла `/path/to/App/Core/RouterFactory.php`. Пример за конфигурация: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # именното пространство App\ е в директорията app/ - } - } -} -``` - -Как точно да конфигурирате поведението ще научите в [документацията на Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Тестване на нови версии -======================= - -Искате да тествате нова разработваща се версия на пакет. Как да го направите? Първо в файла `composer.json` добавете тази двойка опции, която позволява инсталиране на разработващи се версии на пакети, но прибягва до това само в случай, че не съществува никаква комбинация от стабилни версии, която да удовлетворява изискванията: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Освен това препоръчваме да изтриете файла `composer.lock`, понякога Composer необяснимо отказва инсталацията и това решава проблема. - -Да кажем, че става въпрос за пакет `nette/utils` и новата версия има номер 4.0. Инсталирате я с командата: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Или можете да инсталирате конкретна версия, например 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Но ако от библиотеката зависи друг пакет, който е заключен на по-стара версия (напр. `^3.1`), тогава е идеално пакетът да се актуализира, за да работи с новата версия. Ако обаче искате само да заобиколите ограничението и да принудите Composer да инсталира разработващата се версия и да се преструва, че става въпрос за по-стара версия (напр. 3.1.6), можете да използвате ключовата дума `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Извикване на команди -==================== - -Чрез Composer могат да се извикват собствени предварително подготвени команди и скриптове, сякаш става въпрос за нативни команди на Composer. При скриптове, които се намират в папката `vendor/bin`, не е необходимо тази папка да се посочва. - -Като пример ще дефинираме във файла `composer.json` скрипт, който с помощта на [Nette Tester|tester:] стартира тестове: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Тестовете след това стартираме с помощта на `composer tester`. Командата можем да извикаме и в случай, че не сме в коренната папка на проекта, а в някоя поддиректория. - - -Изпратете благодарност -====================== - -Ще ви покажем трик, с който ще зарадвате авторите на open source. По прост начин ще дадете звездичка в GitHub на библиотеките, които вашият проект използва. Достатъчно е да инсталирате библиотеката `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -И след това да стартирате: - -```shell -composer thanks -``` - -Опитайте! - - -Конфигурация -============ - -Composer е тясно свързан с инструмента за версиониране [Git |https://git-scm.com]. Ако не го имате инсталиран, трябва да кажете на Composer да не го използва: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/bg/creating-editing-form.texy b/best-practices/bg/creating-editing-form.texy deleted file mode 100644 index 5f4f84a06c..0000000000 --- a/best-practices/bg/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Форма за създаване и редактиране на запис -***************************************** - -.[perex] -Как правилно да се реализира добавяне и редактиране на запис в Nette, като се използва една и съща форма и за двете? - -В много случаи формите за добавяне и редактиране на записи са еднакви, като се различават само по етикета на бутона. Ще покажем примери за прости презентери, където ще използваме формата първо за добавяне на запис, след това за редактиране и накрая ще комбинираме двете решения. - - -Добавяне на запис ------------------ - -Пример за презентер, използван за добавяне на запис. Ще оставим действителната работа с базата данни на класа `Facade`, чийто код не е от съществено значение за примера. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... добавяме полета към формата ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // добавяне на запис в базата данни - $this->flashMessage('Успешно добавено'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Редактиране на запис --------------------- - -Сега ще покажем как би изглеждал презентер, използван за редактиране на запис: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // проверка за съществуване на запис - || !$this->facade->isEditAllowed(/*...*/) // проверка на правата - ) { - $this->error(); // грешка 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // проверяваме дали действието е 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... добавяме полета към формата ... - - $form->setDefaults($this->record); // задаване на стойности по подразбиране - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // актуализиране на запис - $this->flashMessage('Успешно актуализирано'); - $this->redirect('...'); - } -} -``` - -В метода *action*, който се стартира в самото начало на [жизнения цикъл на презентера |application:presenters#Жизнен цикъл на презентера], проверяваме съществуването на записа и правата на потребителя да го редактира. - -Запазваме записа в свойството `$record`, за да го имаме на разположение в метода `createComponentRecordForm()` за задаване на стойности по подразбиране и в `recordFormSucceeded()` заради ID. Алтернативно решение би било да зададем стойностите по подразбиране директно в `actionEdit()` и да получим стойността на ID, която е част от URL адреса, като използваме `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // проверка за съществуване и проверка на правата - ) { - $this->error(); - } - - // задаване на стойности по подразбиране на формата - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Въпреки това, и това трябва да бъде **най-важният извод от целия код**, трябва да се уверим при създаването на формата, че действието наистина е `edit`. В противен случай проверката в метода `actionEdit()` изобщо няма да се извърши! - - -Същата форма за добавяне и редактиране --------------------------------------- - -И сега комбинираме двата презентера в един. Можем или да разграничим в метода `createComponentRecordForm()` кое е действието и да конфигурираме формата съответно, или можем да го оставим директно на action-методите и да се отървем от условието: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private 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 // проверка за съществуване на запис - || !$this->facade->isEditAllowed(/*...*/) // проверка на правата - ) { - $this->error(); // грешка 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // задаване на стойности по подразбиране - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // проверяваме дали действието е 'add' или 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... добавяме полета към формата ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // добавяне на запис в базата данни - $this->flashMessage('Успешно добавено'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // актуализиране на запис - $this->flashMessage('Успешно актуализирано'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/bg/dynamic-snippets.texy b/best-practices/bg/dynamic-snippets.texy deleted file mode 100644 index c8baa990be..0000000000 --- a/best-practices/bg/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Динамични снипети -***************** - -Доста често при разработването на приложения възниква необходимостта от извършване на AJAX операции, например върху отделни редове на таблица или елементи от списък. Като пример можем да вземем списък със статии, като за всяка от тях ще позволим на влезлия потребител да избере оценка "харесвам/не харесвам". Кодът на презентера и съответният шаблон без AJAX ще изглеждат приблизително по следния начин (представям най-важните части, кодът предполага съществуването на сървис за маркиране на оценките и получаване на колекция от статии - конкретната реализация не е важна за целите на това ръководство): - -```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'); -} -``` - -Шаблон: - -```latte - -``` - - -Ajaxизация -========== - -Нека сега оборудваме това просто приложение с AJAX. Промяната на оценката на статията не е толкова важна, че да изисква пренасочване, затова в идеалния случай тя трябва да се извършва чрез AJAX във фонов режим. Ще използваме [обслужващия скрипт от добавките |application:ajax#Naja] с обичайната конвенция, че AJAX връзките имат CSS клас `ajax`. - -Но как да го направим конкретно? Nette предлага 2 начина: пътя на т.нар. динамични снипети и пътя на компонентите. И двата имат своите плюсове и минуси, затова ще ги покажем един по един. - - -Пътят на динамичните снипети -============================ - -Динамичен снипет в терминологията на Latte означава специфичен случай на използване на макроса `{snippet}`, при който в името на снипета се използва променлива. Такъв снипет не може да се намира навсякъде в шаблона - той трябва да бъде обвит в статичен снипет, т.е. обикновен, или вътре в `{snippetArea}`. Можем да модифицираме нашия шаблон по следния начин. - - -```latte -{snippet articlesContainer} - -{/snippet} -``` - -Всяка статия сега дефинира един снипет, който има ID на статията в името си. Всички тези снипети след това са обвити заедно в един снипет с име `articlesContainer`. Ако пропуснем този обвиващ снипет, Latte ще ни предупреди с изключение. - -Остава ни да добавим прерисуването в презентера - достатъчно е да прерисуваме статичната обвивка. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- не е необходимо - } else { - $this->redirect('this'); - } -} -``` - -По същия начин модифицираме и сестринския метод `handleUnlike()`, и AJAX работи! - -Решението обаче има и една тъмна страна. Ако разгледаме по-подробно как протича AJAX заявката, ще открием, че въпреки че приложението изглежда икономично отвън (връща само един снипет за дадената статия), всъщност на сървъра то е рендирало всички снипети. Желаният снипет е поставен в payload-а, а останалите са изхвърлени (следователно са били извлечени от базата данни напълно ненужно). - -За да оптимизираме този процес, ще трябва да се намесим там, където предаваме колекцията `$articles` към шаблона (да речем в метода `renderDefault()`). Ще използваме факта, че обработката на сигналите се извършва преди методите `render`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Сега, при обработката на сигнала, вместо колекция с всички статии, към шаблона се предава само масив с една статия - тази, която искаме да рендираме и изпратим в payload-а към браузъра. Следователно `{foreach}` ще се изпълни само веднъж и няма да се рендират допълнителни снипети. - - -Пътят на компонентите -===================== - -Напълно различен начин на решаване избягва динамичните снипети. Трикът се състои в прехвърлянето на цялата логика в отделен компонент - отсега нататък въвеждането на оценки няма да се обработва от презентера, а от специализирания `LikeControl`. Класът ще изглежда по следния начин (освен това ще съдържа и методите `render`, `handleUnlike` и т.н.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private 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'); - } - } -} -``` - -Шаблон на компонента: - -```latte -{snippet} - {if !$article->liked} - харесвам - {else} - вече не ми харесва - {/if} -{/snippet} -``` - -Разбира се, шаблонът на изгледа ще се промени и ще трябва да добавим фабрика към презентера. Тъй като ще създадем компонента толкова пъти, колкото статии получим от базата данни, ще използваме класа [application:Multiplier] за неговото "размножаване". - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Шаблонът на изгледа се свежда до необходимия минимум (и е напълно лишен от снипети!): - -```latte -
    -

    {$article->title}

    -
    {$article->content}
    - {control "likeControl-$article->id"} -
    -``` - -Почти сме готови: приложението вече ще работи с AJAX. И тук трябва да оптимизираме приложението, защото поради използването на Nette Database, при обработката на сигнала ненужно се зареждат всички статии от базата данни вместо само една. Предимството обаче е, че те няма да бъдат рендирани, тъй като ще се рендира само нашият компонент. - -{{priority: -1}} diff --git a/best-practices/bg/editors-and-tools.texy b/best-practices/bg/editors-and-tools.texy deleted file mode 100644 index 89751ce259..0000000000 --- a/best-practices/bg/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Редактори & инструменти -*********************** - -.[perex] -Може да сте опитен програмист, но само с добри инструменти ще станете майстор. В тази глава ще намерите съвети за важни инструменти, редактори и плъгини. - - -IDE редактор -============ - -Определено препоръчваме да използвате пълнофункционално IDE за разработка, като PhpStorm, NetBeans, VS Code, а не само текстов редактор с поддръжка на PHP. Разликата е наистина съществена. Няма причина да се задоволявате само с редактор, който може да оцветява синтаксиса, но не достига възможностите на водещо IDE, което точно подсказва, следи за грешки, може да рефакторира код и много повече. Някои IDE са платени, други дори безплатни. - -**NetBeans IDE** има вградена поддръжка за Nette, Latte и NEON. - -**PhpStorm**: инсталирайте тези плъгини в `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: намерете плъгина "Nette Latte + Neon" в marketplace. - -Свържете също Tracy с редактора си. Когато се покаже страница с грешка, ще можете да кликнете върху имената на файловете и те ще се отворят в редактора с курсор на съответния ред. Прочетете [как да конфигурирате системата |tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan е инструмент, който открива логически грешки в кода, преди да го стартирате. - -Инсталираме го с помощта на Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Създаваме конфигурационен файл `phpstan.neon` в проекта: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -И след това го оставяме да анализира класовете в папката `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Изчерпателна документация можете да намерите директно на [уебсайта на PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] проверява и евентуално коригира някои от формалните грешки във вашия изходен код: - -- премахва [BOM |nette:glossary#BOM] -- проверява валидността на [Latte |latte:] шаблоните -- проверява валидността на файловете `.neon`, `.php` и `.json` -- проверява за наличие на [контролни знаци |nette:glossary#Контролни знаци] -- проверява дали файлът е кодиран в UTF-8 -- проверява за неправилно записани `/* @anotace */` (липсва звездичка) -- премахва затварящия таг `?>` от PHP файловете -- премахва интервалите в края на реда и ненужните редове в края на файла -- нормализира разделителите на редове до системните (ако посочите опцията `-l`) - - -Composer -======== - -[Composer |Composer] е инструмент за управление на зависимости в PHP. Позволява ни да декларираме произволно сложни зависимости на отделни библиотеки и след това ги инсталира вместо нас в нашия проект. - - -Requirements Checker -==================== - -Това беше инструмент, който тестваше средата за изпълнение на сървъра и информираше дали (и до каква степен) е възможно да се използва framework-ът. В момента Nette може да се използва на всеки сървър, който има минималната изисквана версия на PHP. diff --git a/best-practices/bg/form-reuse.texy b/best-practices/bg/form-reuse.texy deleted file mode 100644 index 1af34cdd6f..0000000000 --- a/best-practices/bg/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Повторно използване на форми на няколко места -********************************************* - -.[perex] -В Nette имате на разположение няколко опции как да използвате една и съща форма на няколко места и да не дублирате код. В тази статия ще покажем различни решения, включително тези, които трябва да избягвате. - - -Фабрика за форми -================ - -Един от основните подходи за използване на един и същ компонент на няколко места е създаването на метод или клас, който генерира този компонент, и последващото извикване на този метод на различни места в приложението. Такъв метод или клас се нарича *фабрика*. Моля, не го бъркайте с дизайн патърна *factory method*, който описва специфичен начин за използване на фабрики и не е свързан с тази тема. - -Като пример ще създадем фабрика, която ще изгражда форма за редактиране: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Заглавие:'); - // тук се добавят други полета на формата - $form->addSubmit('send', 'Изпрати'); - return $form; - } -} -``` - -Сега можете да използвате тази фабрика на различни места във вашето приложение, например в презентери или компоненти. И това става, като я [поискаме като зависимост |dependency-injection:passing-dependencies]. Първо, записваме класа в конфигурационния файл: - -```neon -services: - - FormFactory -``` - -И след това я използваме в презентера: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // обработка на изпратените данни - }; - return $form; - } -} -``` - -Можете да разширите фабриката за форми с допълнителни методи за създаване на други видове форми според нуждите на вашето приложение. И разбира се, можем да добавим и метод, който създава основна форма без елементи, и този метод ще бъде използван от другите методи: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Заглавие:'); - // тук се добавят други полета на формата - $form->addSubmit('send', 'Изпрати'); - return $form; - } -} -``` - -Методът `createForm()` засега не прави нищо полезно, но това бързо ще се промени. - - -Зависимости на фабриката -======================== - -С времето ще се окаже, че се нуждаем формите да бъдат многоезични. Това означава, че трябва да зададем т.нар. [translator |forms:rendering#Превод] на всички форми. За тази цел ще модифицираме класа `FormFactory`, така че да приема обект `Translator` като зависимост в конструктора и ще го предадем на формата: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Тъй като методът `createForm()` се извиква и от другите методи, създаващи специфични форми, е достатъчно да зададем translator-а само в него. И сме готови. Няма нужда да променяме кода на нито един презентер или компонент, което е страхотно. - - -Множество фабрични класове -========================== - -Алтернативно, можете да създадете множество класове за всяка форма, която искате да използвате във вашето приложение. Този подход може да увеличи четимостта на кода и да улесни управлението на формите. Ще оставим оригиналната `FormFactory` да създава само чиста форма с основна конфигурация (например с поддръжка на преводи) и ще създадем нова фабрика `EditFormFactory` за формата за редактиране. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ използване на композиция -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // тук се добавят други полета на формата - $form->addSubmit('send', 'Изпрати'); - return $form; - } -} -``` - -Много е важно връзката между класовете `FormFactory` и `EditFormFactory` да се реализира чрез [композиция |nette:introduction-to-object-oriented-programming#Композиция], а не чрез [обектно наследяване |nette:introduction-to-object-oriented-programming#Наследяване]: - -```php -// ⛔ НЕ ТАКА! НАСЛЕДЯВАНЕТО НЕ Е ЗА ТУК -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Заглавие:'); - // тук се добавят други полета на формата - $form->addSubmit('send', 'Изпрати'); - return $form; - } -} -``` - -Използването на наследяване в този случай би било напълно контрапродуктивно. Много бързо ще се сблъскате с проблеми. Например, в момента, в който искате да добавите параметри към метода `create()`; PHP ще съобщи за грешка, че неговата сигнатура се различава от родителската. Или при предаване на зависимост към класа `EditFormFactory` чрез конструктора. Ще възникне ситуация, която наричаме [constructor hell |dependency-injection:passing-dependencies#Адът на конструктора]. - -Като цяло е по-добре да се дава предимство на [композицията пред наследяването |dependency-injection:faq#Защо се предпочита композиция пред наследяването]. - - -Обработка на формата -==================== - -Обработката на формата, която се извиква след успешно изпращане, също може да бъде част от фабричния клас. Тя ще работи, като предава изпратените данни на модела за обработка. Евентуални грешки [ще предаде обратно |forms:validation#Грешки при обработка] на формата. Моделът в следващия пример е представен от класа `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Заглавие:'); - // тук се добавят други полета на формата - $form->addSubmit('send', 'Изпрати'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // обработка на изпратените данни - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Самото пренасочване обаче ще оставим на презентера. Той ще добави към събитието `onSuccess` допълнителен handler, който ще извърши пренасочването. Благодарение на това ще бъде възможно да се използва формата в различни презентери и във всеки да се пренасочва към различно място. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Записът е запазен'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Това решение използва свойството на формите, че когато се извика `addError()` върху формата или неин елемент, следващият handler `onSuccess` вече не се извиква. - - -Наследяване от класа Form -========================= - -Изградената форма не трябва да бъде наследник на формата. С други думи, не използвайте това решение: - -```php -// ⛔ НЕ ТАКА! НАСЛЕДЯВАНЕТО НЕ Е ЗА ТУК -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Заглавие:'); - // тук се добавят други полета на формата - $this->addSubmit('send', 'Изпрати'); - $this->setTranslator($translator); - } -} -``` - -Вместо да изграждате формата в конструктора, използвайте фабрика. - -Трябва да се осъзнае, че класът `Form` е преди всичко инструмент за изграждане на форма, т.е. *form builder*. А изградената форма може да се разглежда като неин продукт. Но продуктът не е специфичен случай на builder-а, между тях няма връзка *is a*, която е основата на наследяването. - - -Компонент с форма -================= - -Напълно различен подход представлява създаването на [компонент |application:components], чиято част е форма. Това дава нови възможности, например да се рендира формата по специфичен начин, тъй като компонентът включва и шаблон. Или могат да се използват сигнали за AJAX комуникация и дозареждане на информация във формата, например за подсказки и т.н. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Заглавие:'); - // тук се добавят други полета на формата - $form->addSubmit('send', 'Изпрати'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // обработка на изпратените данни - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // извикване на събитие - $this->onSave($this, $data); - } -} -``` - -Ще създадем и фабрика, която ще произвежда този компонент. Достатъчно е [да запишем нейния интерфейс |application:components#Компоненти със зависимости]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -И да добавим в конфигурационния файл: - -```neon -services: - - EditControlFactory -``` - -И сега вече можем да поискаме фабриката и да я използваме в презентера: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // или пренасочваме към резултата от редактирането, напр.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/bg/inject-method-attribute.texy b/best-practices/bg/inject-method-attribute.texy deleted file mode 100644 index 2d72ead00c..0000000000 --- a/best-practices/bg/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Методи и атрибути inject -************************ - -.[perex] -В тази статия ще разгледаме различните начини за предаване на зависимости към презентерите в Nette framework. Ще сравним предпочитания начин, който е конструкторът, с други възможности като методите и атрибутите `inject`. - -Също и за презентерите важи, че предаването на зависимости чрез [конструктор |dependency-injection:passing-dependencies#Предаване чрез конструктор] е предпочитаният път. Но ако създавате общ родител, от който наследяват други презентери (напр. `BasePresenter`), и този родител също има зависимости, възниква проблем, който наричаме [constructor hell |dependency-injection:passing-dependencies#Адът на конструктора]. Той може да бъде заобиколен чрез алтернативни пътища, които представляват методите и атрибутите (анотациите) `inject`. - - -Методи `inject*()` -================== - -Това е форма на предаване на зависимост чрез [setter |dependency-injection:passing-dependencies#Предаване чрез сетър]. Името на тези сетъри започва с префикса `inject`. Nette DI автоматично извиква така наречените методи веднага след създаването на инстанцията на презентера и им предава всички необходими зависимости. Следователно те трябва да бъдат декларирани като public. - -Методите `inject*()` могат да се разглеждат като вид разширение на конструктора в няколко метода. Благодарение на това `BasePresenter` може да поеме зависимости чрез друг метод и да остави конструктора свободен за своите наследници: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Презентерът може да съдържа произволен брой методи `inject*()` и всеки може да има произволен брой параметри. Те са чудесни и в случаите, когато презентерът е [съставен от trait |presenter-traits] и всеки от тях изисква собствена зависимост. - - -Атрибути `Inject` -================= - -Това е форма на [инжектиране в свойство |dependency-injection:passing-dependencies#Чрез задаване на променлива]. Достатъчно е да се обозначи в кои променливи трябва да се инжектира и Nette DI автоматично ще предаде зависимостите веднага след създаването на инстанцията на презентера. За да може да ги вмъкне, е необходимо те да бъдат декларирани като public. - -Означаваме свойствата с атрибут: (преди се използваше анотацията `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // този ред е важен - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Предимството на този начин на предаване на зависимости беше много икономичната форма на запис. Въпреки това, с появата на [constructor property promotion |https://blog.nette.org/bg/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], изглежда по-лесно да се използва конструктор. - -От друга страна, този начин страда от същите недостатъци като предаването на зависимости към свойства като цяло: нямаме контрол над промените в променливата и същевременно променливата става част от публичния интерфейс на класа, което е нежелателно. diff --git a/best-practices/bg/lets-create-contact-form.texy b/best-practices/bg/lets-create-contact-form.texy deleted file mode 100644 index 9070371fe2..0000000000 --- a/best-practices/bg/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Създаваме контактна форма -************************* - -.[perex] -Ще разгледаме как да създадем контактна форма в Nette, включително изпращане на имейл. И така, да започваме! - -Първо трябва да създадем нов проект. Как да го направите е обяснено на страницата [Първи стъпки |nette:installation]. И след това можем да започнем със създаването на формата. - -Най-лесният начин е да създадете [форма директно в презентера |forms:in-presenter]. Можем да използваме предварително подготвения `HomePresenter`. В него ще добавим компонент `contactForm`, представляващ формата. Ще направим това, като напишем в кода фабричен метод `createComponentContactForm()`, който ще произведе компонента: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Име:') - ->setRequired('Въведете име'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Въведете e-mail'); - $form->addTextarea('message', 'Съобщение:') - ->setRequired('Въведете съобщение'); - $form->addSubmit('send', 'Изпрати'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, $data): void - { - // изпращане на имейл - } -} -``` - -Както виждате, създадохме два метода. Първият метод `createComponentContactForm()` създава нова форма. Тя има полета за име, имейл и съобщение, които добавяме с методите `addText()`, `addEmail()` и `addTextArea()`. Също така добавихме бутон за изпращане на формата. Но какво ще стане, ако потребителят не попълни някое поле? В такъв случай трябва да му съобщим, че това е задължително поле. Постигнахме това с метода `setRequired()`. Накрая добавихме и [събитие |nette:glossary#Събития events] `onSuccess`, което се задейства, ако формата е успешно изпратена. В нашия случай извиква метода `contactFormSucceeded`, който ще се погрижи за обработката на изпратената форма. Ще добавим това в кода след малко. - -Ще оставим компонента `contactForm` да се рендира в шаблона `Home/default.latte`: - -```latte -{block content} -

    Контактна форма

    -{control contactForm} -``` - -За самото изпращане на имейл ще създадем нов клас, който ще наречем `ContactFacade` и ще го поставим във файла `app/Model/ContactFacade.php`: - -```php -addTo('admin@example.com') // вашият имейл - ->setFrom($email, $name) - ->setSubject('Съобщение от контактната форма') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Методът `sendMessage()` създава и изпраща имейл. За целта използва т.нар. mailer, който получава като зависимост чрез конструктора. Прочетете повече за [изпращане на имейли |mail:]. - -Сега ще се върнем към презентера и ще завършим метода `contactFormSucceeded()`. Той ще извика метода `sendMessage()` на класа `ContactFacade` и ще му предаде данните от формата. А как ще получим обекта `ContactFacade`? Ще го получим чрез конструктора: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Съобщението беше изпратено'); - $this->redirect('this'); - } -} -``` - -След като имейлът бъде изпратен, ще покажем на потребителя т.нар. [flash съобщение |application:components#Flash съобщения], потвърждаващо, че съобщението е изпратено, и след това ще пренасочим към следващата страница, за да не може формата да бъде повторно изпратена чрез *refresh* в браузъра. - - -Така, и ако всичко работи, трябва да можете да изпратите имейл от вашата контактна форма. Поздравления! - - -HTML шаблон на имейл --------------------- - -Засега се изпраща обикновен текстов имейл, съдържащ само съобщението, изпратено от формата. Но в имейла можем да използваме HTML и да направим вида му по-атрактивен. Ще създадем за него шаблон в Latte, който ще запишем в `app/Model/contactEmail.latte`: - -```latte - - Съобщение от контактната форма - - -

    Име: {$name}

    -

    E-mail: {$email}

    -

    Съобщение: {$message}

    - - -``` - -Остава да променим `ContactFacade`, за да използва този шаблон. В конструктора ще изискаме класа `LatteFactory`, който може да произведе обект `Latte\Engine`, т.е. [рендериращ механизъм за Latte шаблони |latte:develop#Как да рендираме шаблон]. С помощта на метода `renderToString()` ще рендираме шаблона във файл, първият параметър е пътят до шаблона, а вторият са променливите. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // вашият имейл - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Генерирания HTML имейл след това ще предадем на метода `setHtmlBody()` вместо оригиналния `setBody()`. Също така не е необходимо да посочваме темата на имейла в `setSubject()`, тъй като библиотеката ще я вземе от елемента `` на шаблона. - - -Конфигурация ------------- - -В кода на класа `ContactFacade` все още е твърдо кодиран нашият администраторски имейл `admin@example.com`. Би било по-добре да го преместим в конфигурационния файл. Как да го направим? - -Първо ще променим класа `ContactFacade` и ще заменим низа с имейла с променлива, предадена чрез конструктора: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -А втората стъпка е да посочим стойността на тази променлива в конфигурацията. Във файла `app/config/services.neon` ще запишем: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -И това е. Ако елементите в секцията `services` са много и имате чувството, че имейлът се губи сред тях, можем да го превърнем в променлива. Ще променим записа на: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -И във файла `app/config/common.neon` ще дефинираме тази променлива: - -```neon -parameters: - adminEmail: admin@example.com -``` - -И е готово! diff --git a/best-practices/bg/microsites.texy b/best-practices/bg/microsites.texy deleted file mode 100644 index e29edcb2ed..0000000000 --- a/best-practices/bg/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Как да пишем микро-уебсайтове -***************************** - -Представете си, че трябва бързо да създадете малък уебсайт за предстоящо събитие на вашата фирма. Трябва да е просто, бързо и без излишни усложнения. Може би си мислите, че за такъв малък проект не ви е необходим стабилен framework. Но какво ще стане, ако използването на Nette framework може значително да опрости и ускори този процес? - -Все пак, дори при създаването на прости уебсайтове, не искате да се отказвате от удобството. Не искате да измисляте това, което вече е решено. Бъдете спокойно мързеливи и се оставете да ви глезят. Nette Framework може отлично да се използва и като micro framework. - -Как може да изглежда такъв микросайт? Например така, че целият код на уебсайта да се постави в един файл `index.php` в публичната папка: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// създаване на DI контейнер въз основа на конфигурацията в config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// настройване на маршрутизацията -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// маршрут за URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // откриване на езика на браузъра и пренасочване към URL /en или /de и т.н. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// маршрут за URL https://example.com/cs или https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // показване на съответния шаблон, например ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// стартиране на приложението! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Всичко останало ще бъдат шаблони, съхранени в родителската папка `/templates`. - -PHP кодът в `index.php` първо [подготвя средата |bootstrap:], след това дефинира [маршрутите |application:routing#Динамично маршрутизиране с callback-ове] и накрая стартира приложението. Предимството е, че вторият параметър на функцията `addRoute()` може да бъде callable, който се изпълнява след отваряне на съответната страница. - - -Защо да използвате Nette за микросайт? --------------------------------------- - -- Програмистите, които някога са опитвали [Tracy|tracy:], днес не могат да си представят да програмират нещо без нея. -- Преди всичко обаче ще използвате системата за шаблони [Latte|latte:], защото още от 2 страници ще искате да имате отделен [лейаут и съдържание|latte:template-inheritance]. -- И определено искате да разчитате на [автоматично екраниране |latte:safety-first], за да не възникне уязвимост XSS -- Nette също така гарантира, че при грешка никога няма да се покажат програмни съобщения за грешки на PHP, а разбираема за потребителя страница. -- Ако искате да получавате обратна връзка от потребителите, например под формата на контактна форма, тогава ще добавите и [форми|forms:] и [база данни|database:]. -- Попълнените формуляри можете лесно да [изпращате по имейл|mail:]. -- Понякога може да ви е полезно [кеширането|caching:], например ако изтегляте и показвате фийдове. - -В днешно време, когато скоростта и ефективността са ключови, е важно да имате инструменти, които ви позволяват да постигнете резултати без излишно забавяне. Nette framework ви предлага точно това - бърза разработка, сигурност и широк набор от инструменти, като Tracy и Latte, които опростяват процеса. Достатъчно е да инсталирате няколко Nette пакета и изграждането на такъв микросайт изведнъж става напълно лесно. И знаете, че никъде не се крие никаква дупка в сигурността. diff --git a/best-practices/bg/pagination.texy b/best-practices/bg/pagination.texy deleted file mode 100644 index 15a33b739e..0000000000 --- a/best-practices/bg/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Пагиниране на резултати от база данни -************************************* - -.[perex] -При създаването на уеб приложения много често ще се сблъскате с изискването за ограничаване на броя на изведените елементи на страница. - -Ще изходим от състояние, в което извеждаме всички данни без пагиниране. За избор на данни от базата данни имаме клас ArticleRepository, който освен конструктор съдържа метод `findPublishedArticles`, който връща всички публикувани статии, сортирани низходящо по дата на публикуване. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -В презентера след това инжектираме моделния клас и в render метода изискваме публикуваните статии, които предаваме на шаблона: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -В шаблона `default.latte` след това се грижим за извеждането на статиите: - -```latte -{block content} -<h1>Статии</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -По този начин можем да изведем всички статии, което обаче започва да създава проблеми в момента, когато броят на статиите нарасне. В този момент е подходящо да се внедри механизъм за пагиниране. - -Той гарантира, че всички статии ще бъдат разделени на няколко страници и ние ще покажем само статиите от една текуща страница. [utils:Paginator] сам ще изчисли общия брой страници и разпределението на статиите според това колко статии общо имаме и колко статии на страница искаме да покажем. - -В първата стъпка ще променим метода за получаване на статии в класа на repository така, че да може да връща само статии за една страница. Също така ще добавим метод за установяване на общия брой статии в базата данни, който ще ни е необходим за настройка на Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $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, - ); - } - - /** - * Връща общия брой публикувани статии - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -След това ще се заемем с промените в презентера. В render метода ще предаваме номера на текущо показваната страница. За случая, когато този номер не е част от URL, ще зададем стойност по подразбиране за първата страница. - -Освен това ще разширим render метода с получаване на инстанция на Paginator, неговата настройка и избор на правилните статии за показване в шаблона. HomePresenter след промените ще изглежда така: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Ще установим общия брой публикувани статии - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Ще създадем инстанция на Paginator и ще го настроим - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // общ брой статии - $paginator->setItemsPerPage(10); // брой елементи на страница - $paginator->setPage($page); // номер на текущата страница - - // От базата данни ще изтеглим ограничено множество статии според изчислението на Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // което ще предадем на шаблона - $this->template->articles = $articles; - // и също така самия Paginator за показване на възможностите за пагиниране - $this->template->paginator = $paginator; - } -} -``` - -Шаблонът ни вече итерира само върху статиите от една страница, достатъчно е да добавим връзките за пагиниране: - -```latte -{block content} -<h1>Статии</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Първа</a> -  |  - <a n:href="default, $paginator->page-1">Предишна</a> -  |  - {/if} - - Страница {$paginator->getPage()} от {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Следваща</a> -  |  - <a n:href="default, $paginator->getPageCount()">Последна</a> - {/if} -</div> -``` - - -Така допълнихме страницата с възможност за пагиниране с помощта на Paginator. В случай, че вместо [Nette Database Core |database:sql-way] като слой за база данни използваме [Nette Database Explorer |database:explorer], можем да внедрим пагиниране и без използване на Paginator. Класът `Nette\Database\Table\Selection` съдържа метод [page |api:Nette\Database\Table\Selection::_page] с логика за пагиниране, взета от Paginator. - -Repository при този начин на внедряване ще изглежда така: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -В презентера не е необходимо да създаваме Paginator, вместо него ще използваме метода на класа `Selection`, който ни връща repository: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Ще изтеглим публикуваните статии - $articles = $this->articleRepository->findPublishedArticles(); - - // и в шаблона ще изпратим само тяхната част, ограничена според изчислението на метода page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // и също така необходимите данни за показване на възможностите за пагиниране - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Тъй като в шаблона сега не изпращаме Paginator, ще променим частта, показваща връзките за пагиниране: - -```latte -{block content} -<h1>Статии</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Първа</a> -  |  - <a n:href="default, $page - 1">Предишна</a> -  |  - {/if} - - Страница {$page} от {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Следваща</a> -  |  - <a n:href="default, $lastPage">Последна</a> - {/if} -</div> -``` - -По този начин внедрихме механизъм за пагиниране без използване на Paginator. - -{{priority: -1}} diff --git a/best-practices/bg/passing-settings-to-presenters.texy b/best-practices/bg/passing-settings-to-presenters.texy deleted file mode 100644 index 076be18938..0000000000 --- a/best-practices/bg/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Предаване на настройки към презентерите -*************************************** - -.[perex] -Трябва ли да предавате аргументи към презентерите, които не са обекти (напр. информация дали работят в debug режим, пътища до директории и т.н.), и следователно не могат да бъдат предадени автоматично чрез autowiring? Решението е да ги капсулирате в обект `Settings`. - -Сървисът `Settings` представлява много лесен и същевременно полезен начин за предоставяне на информация за работещото приложение на презентерите. Конкретният му вид зависи изцяло от вашите конкретни нужди. Пример: - -```php -namespace App; - -class Settings -{ - public function __construct( - // от PHP 8.1 е възможно да се посочи readonly - public bool $debugMode, - public string $appDir, - // и така нататък - ) {} -} -``` - -Пример за регистрация в конфигурацията: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Когато презентерът се нуждае от информация, предоставена от този сървис, той просто я изисква в конструктора: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/bg/post-links.texy b/best-practices/bg/post-links.texy deleted file mode 100644 index 452bb3b18a..0000000000 --- a/best-practices/bg/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Как правилно да използваме POST връзки -************************************** - -.[perex] -В уеб приложенията, особено в административните интерфейси, основно правило трябва да бъде, че действията, променящи състоянието на сървъра, не трябва да се извършват чрез HTTP метода GET. Както подсказва името на метода, GET трябва да служи само за получаване на данни, а не за тяхната промяна. За действия като изтриване на записи е по-подходящо да се използва методът POST. Въпреки че идеалният би бил методът DELETE, но той не може да бъде извикан без JavaScript, затова исторически се използва POST. - -Как да го направим на практика? Използвайте този прост трик. В началото на шаблона си създайте помощна форма с идентификатор `postForm`, която след това ще използвате за бутоните за изтриване: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Благодарение на тази форма можете вместо класическа връзка `<a>` да използвате бутон `<button>`, който може да бъде визуално оформен така, че да изглежда като обикновена връзка. Например CSS framework Bootstrap предлага класове `btn btn-link`, с които ще постигнете това, че бутонът няма да се различава визуално от останалите връзки. С помощта на атрибута `form="postForm"` го свързваме с предварително подготвената форма: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">delete</button> - <!-- вместо <a n:href="delete $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -При кликване върху връзката сега се извиква действието `delete`. За да се гарантира, че заявките ще бъдат приемани само чрез метода POST и от същия домейн (което е ефективна защита срещу CSRF атаки), използвайте атрибута `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // хипотетичен код, изтриващ запис - $this->redirect('default'); - } -} -``` - -Атрибутът съществува от Nette Application 3.2 и повече за неговите възможности ще научите на страницата [Как да използваме атрибута #Requires |attribute-requires]. - -Ако вместо действието `actionDelete()` използвате сигнал `handleDelete()`, не е необходимо да посочвате `sameOrigin: true`, тъй като сигналите имат тази защита зададена имплицитно: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Този подход не само подобрява сигурността на вашето приложение, но също така допринася за спазването на правилните уеб стандарти и практики. Чрез използването на методи POST за действия, променящи състоянието, ще постигнете по-стабилно и по-сигурно приложение. diff --git a/best-practices/bg/presenter-traits.texy b/best-practices/bg/presenter-traits.texy deleted file mode 100644 index c2cf88f074..0000000000 --- a/best-practices/bg/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Композиране на презентери от trait -********************************** - -.[perex] -Ако трябва да внедрим един и същ код в няколко презентера (напр. проверка дали потребителят е влязъл), предлага се да поставим кода в общ родител. Втората възможност е създаването на едноцелеви [trait |nette:introduction-to-object-oriented-programming#Traits]. - -Предимството на това решение е, че всеки от презентерите може да използва точно тези trait, които наистина са му необходими, докато множественото наследяване не е възможно в PHP. - -Тези trait могат да използват факта, че при създаването на презентера последователно се извикват всички [inject методи |inject-method-attribute#Методи inject]. Необходимо е само да се гарантира, че името на всеки inject метод е уникално. - -Trait могат да прикачат инициализационен код към събитията [onStartup или onRender |application:presenters#Събития]. - -Примери: - -```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); - }; - } -} -``` - -Презентерът след това просто използва тези trait: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/bg/restore-request.texy b/best-practices/bg/restore-request.texy deleted file mode 100644 index 191350748d..0000000000 --- a/best-practices/bg/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Как да се върнем към предишна страница? -*************************************** - -.[perex] -Какво ще стане, ако потребителят попълва формуляр и сесията му изтече? За да не загуби данните, преди пренасочването към страницата за вход ще запазим данните в сесията. В Nette това е напълно лесно. - -Текущата заявка може да бъде запазена в сесията с помощта на метода `storeRequest()`, който връща нейния идентификатор под формата на кратък низ. Методът запазва името на текущия презентер, изгледа и неговите параметри. В случай, че е изпратен и формуляр, се запазва и съдържанието на полетата (с изключение на качените файлове). - -Възстановяването на заявката се извършва от метода `restoreRequest($key)`, на който предаваме получения идентификатор. Той пренасочва към оригиналния презентер и изглед. Ако обаче запазената заявка съдържа изпращане на формуляр, към оригиналния презентер се преминава с метода `forward()`, на формуляра се предават предишно попълнените стойности и той се рендира отново. По този начин потребителят има възможност да изпрати формуляра отново и никакви данни не се губят. - -Важно е, че `restoreRequest()` проверява дали нововъведеният потребител е същият, който първоначално е попълнил формуляра. Ако не е, заявката се отхвърля и нищо не се прави. - -Ще покажем всичко на пример. Нека имаме презентер `AdminPresenter`, в който се редактират данни и в чийто метод `startup()` проверяваме дали потребителят е влязъл. Ако не е, го пренасочваме към `SignPresenter`. Същевременно запазваме текущата заявка и нейния ключ изпращаме до `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()]); - } - } -} -``` - -Презентерът `SignPresenter` освен формуляра за вход ще съдържа и персистентен параметър `$backlink`, в който се записва ключът. Тъй като параметърът е персистентен, той ще се пренася и след изпращане на формуляра за вход. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... добавяме полета на формуляра ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... тук вписваме потребителя ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -На метода `restoreRequest()` предаваме ключа на запазената заявка и той пренасочва (или преминава) към оригиналния презентер. - -Ако обаче ключът е невалиден (например вече не съществува в сесията), методът не прави нищо. Следователно следва извикването на `$this->redirect('Admin:')`, което пренасочва към `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/el/@home.texy b/best-practices/el/@home.texy deleted file mode 100644 index 0e8088a224..0000000000 --- a/best-practices/el/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Οδηγοί και διαδικασίες -********************** - -.[perex] -Οδηγοί, λύσεις για συχνές εργασίες και *βέλτιστες πρακτικές* για το Nette. - - -<div class=documentation> -<div> - - -Εφαρμογές Nette ---------------- -- [Μέθοδοι και χαρακτηριστικά inject |inject-method-attribute] -- [Σύνθεση presenters από traits |presenter-traits] -- [Πέρασμα ρυθμίσεων σε presenters |passing-settings-to-presenters] -- [Πώς να επιστρέψετε σε προηγούμενη σελίδα |restore-request] -- [Σελίδωση αποτελεσμάτων βάσης δεδομένων |pagination] -- [Δυναμικά snippets |dynamic-snippets] -- [Πώς να χρησιμοποιήσετε το attribute #Requires |attribute-requires] -- [Πώς να χρησιμοποιήσετε σωστά τους συνδέσμους POST |post-links] - -</div> -<div> - - -Φόρμες ------- -- [Επαναχρησιμοποίηση φορμών |form-reuse] -- [Φόρμα για δημιουργία και επεξεργασία εγγραφής |creating-editing-form] -- [Δημιουργούμε φόρμα επικοινωνίας |lets-create-contact-form] -- [Εξαρτώμενα selectboxes |https://blog.nette.org/el/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Γενικά ------- -- [Πώς να φορτώσετε ένα αρχείο διαμόρφωσης |bootstrap:] -- [Πώς να γράψετε micro-sites |microsites] -- [Γιατί το Nette χρησιμοποιεί τη σημειογραφία PascalCase για σταθερές; |https://blog.nette.org/el/for-less-screaming-in-the-code] -- [Γιατί το Nette δεν χρησιμοποιεί το επίθημα Interface; |https://blog.nette.org/el/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: συμβουλές χρήσης |composer] -- [Συμβουλές για editors & εργαλεία |editors-and-tools] -- [Εισαγωγή στον αντικειμενοστρεφή προγραμματισμό |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Δείγματα λύσεων ---------------- -- [Παραδείγματα Nette |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Παραδείγματα Contributte |https://contributte.org/examples.html] -- [Doctrine ORM Website |https://github.com/MinecordNetwork/Website] -- [Quick start |quickstart:] - -</div> -<div> - - -Βίντεο ------- -Εκατοντάδες ηχογραφήσεις από τα Poslední soboty και βίντεο για το Nette μπορείτε να βρείτε κάτω από μία στέγη στο "κανάλι YouTube του Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/el/@meta.texy b/best-practices/el/@meta.texy deleted file mode 100644 index 9ae15ea14a..0000000000 --- a/best-practices/el/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Οδηγοί και διαδικασίες}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/el/attribute-requires.texy b/best-practices/el/attribute-requires.texy deleted file mode 100644 index 6a3211185c..0000000000 --- a/best-practices/el/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Πώς να χρησιμοποιήσετε το attribute `#[Requires]` -************************************************* - -.[perex] -Όταν γράφετε μια διαδικτυακή εφαρμογή, συχνά αντιμετωπίζετε την ανάγκη να περιορίσετε την πρόσβαση σε ορισμένα τμήματα της εφαρμογής σας. Ίσως θέλετε ορισμένα αιτήματα να μπορούν να στέλνουν δεδομένα μόνο μέσω φόρμας (δηλαδή με τη μέθοδο POST), ή να είναι προσβάσιμα μόνο για κλήσεις AJAX. Στο Nette Framework 3.2, εμφανίστηκε ένα νέο εργαλείο που σας επιτρέπει να ορίσετε τέτοιους περιορισμούς με πολύ κομψό και σαφή τρόπο: το attribute `#[Requires]`. - -Το attribute είναι μια ειδική ετικέτα στην PHP, την οποία προσθέτετε πριν από τον ορισμό μιας κλάσης ή μεθόδου. Επειδή είναι στην πραγματικότητα μια κλάση, για να λειτουργήσουν τα παρακάτω παραδείγματα, είναι απαραίτητο να συμπεριλάβετε τη δήλωση use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Μπορείτε να χρησιμοποιήσετε το attribute `#[Requires]` στην ίδια την κλάση του presenter και επίσης σε αυτές τις μεθόδους: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Οι δύο τελευταίες μέθοδοι αφορούν επίσης τα components, οπότε μπορείτε να χρησιμοποιήσετε το attribute και σε αυτά. - -Αν δεν πληρούνται οι προϋποθέσεις που αναφέρει το attribute, προκαλείται σφάλμα HTTP 4xx. - - -Μέθοδοι HTTP ------------- - -Μπορείτε να καθορίσετε ποιες μέθοδοι HTTP (όπως GET, POST κ.λπ.) επιτρέπονται για πρόσβαση. Για παράδειγμα, αν θέλετε να επιτρέψετε την πρόσβαση μόνο με την υποβολή φόρμας, ορίζετε: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Γιατί πρέπει να χρησιμοποιείτε POST αντί για GET για ενέργειες που αλλάζουν την κατάσταση και πώς να το κάνετε; [Διαβάστε τον οδηγό |post-links]. - -Μπορείτε να καθορίσετε μια μέθοδο ή έναν πίνακα μεθόδων. Μια ειδική περίπτωση είναι η τιμή `'*'`, η οποία επιτρέπει όλες τις μεθόδους, κάτι που οι presenters κανονικά [δεν επιτρέπουν για λόγους ασφαλείας |application:presenters#Έλεγχος μεθόδου HTTP]. - - -Κλήσεις AJAX ------------- - -Αν θέλετε ο presenter ή η μέθοδος να είναι διαθέσιμη μόνο για αιτήσεις AJAX, χρησιμοποιήστε: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Ίδια προέλευση --------------- - -Για να αυξήσετε την ασφάλεια, μπορείτε να απαιτήσετε η αίτηση να γίνεται από τον ίδιο τομέα. Αυτό αποτρέπει την [ευπάθεια CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Για τις μεθόδους `handle<Signal>()`, η πρόσβαση από τον ίδιο τομέα απαιτείται αυτόματα. Έτσι, αν αντίθετα θέλετε να επιτρέψετε την πρόσβαση από οποιονδήποτε τομέα, καθορίστε: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Πρόσβαση μέσω forward ---------------------- - -Μερικές φορές είναι χρήσιμο να περιορίσετε την πρόσβαση στον presenter έτσι ώστε να είναι διαθέσιμος μόνο έμμεσα, για παράδειγμα, χρησιμοποιώντας τη μέθοδο `forward()` ή `switch()` από άλλο presenter. Έτσι προστατεύονται, για παράδειγμα, οι error-presenters, ώστε να μην είναι δυνατό να κληθούν από το URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Στην πράξη, συχνά είναι απαραίτητο να επισημάνετε ορισμένα views, στα οποία μπορείτε να φτάσετε μόνο βάσει της λογικής στον presenter. Δηλαδή, ξανά, ώστε να μην είναι δυνατό να ανοίξουν απευθείας: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Συγκεκριμένες ενέργειες ------------------------ - -Μπορείτε επίσης να περιορίσετε ότι ένας συγκεκριμένος κώδικας, όπως η δημιουργία ενός component, θα είναι διαθέσιμος μόνο για συγκεκριμένες actions στον presenter: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -Σε περίπτωση μίας action, δεν χρειάζεται να γράψετε πίνακα: `#[Requires(actions: 'default')]` - - -Προσαρμοσμένα attributes ------------------------- - -Αν θέλετε να χρησιμοποιήσετε το attribute `#[Requires]` επανειλημμένα με τις ίδιες ρυθμίσεις, μπορείτε να δημιουργήσετε το δικό σας attribute που θα κληρονομεί το `#[Requires]` και θα το ρυθμίζει ανάλογα με τις ανάγκες. - -Για παράδειγμα, το `#[SingleAction]` θα επιτρέπει την πρόσβαση μόνο μέσω της action `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Ή το `#[RestMethods]` θα επιτρέπει την πρόσβαση μέσω όλων των μεθόδων HTTP που χρησιμοποιούνται για το REST API: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Συμπέρασμα ----------- - -Το attribute `#[Requires]` σας δίνει μεγάλη ευελιξία και έλεγχο στο πώς είναι προσβάσιμες οι ιστοσελίδες σας. Χρησιμοποιώντας απλούς αλλά ισχυρούς κανόνες, μπορείτε να αυξήσετε την ασφάλεια και τη σωστή λειτουργία της εφαρμογής σας. Όπως βλέπετε, η χρήση attributes στο Nette μπορεί όχι μόνο να διευκολύνει τη δουλειά σας, αλλά και να την ασφαλίσει. diff --git a/best-practices/el/composer.texy b/best-practices/el/composer.texy deleted file mode 100644 index 9e0a6a7715..0000000000 --- a/best-practices/el/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: συμβουλές για χρήση -***************************** - -<div class=perex> - -Ο Composer είναι ένα εργαλείο για τη διαχείριση εξαρτήσεων στην PHP. Μας επιτρέπει να απαριθμήσουμε τις βιβλιοθήκες από τις οποίες εξαρτάται το έργο μας και θα τις εγκαθιστά και θα τις ενημερώνει για εμάς. Θα δείξουμε: - -- πώς να εγκαταστήσετε τον Composer -- τη χρήση του σε ένα νέο ή υπάρχον έργο - -</div> - - -Εγκατάσταση -=========== - -Ο Composer είναι ένα εκτελέσιμο αρχείο `.phar`, το οποίο κατεβάζετε και εγκαθιστάτε ως εξής: - - -Windows -------- - -Χρησιμοποιήστε τον επίσημο εγκαταστάτη [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Αρκούν 4 εντολές, τις οποίες αντιγράφετε από [αυτή τη σελίδα |https://getcomposer.org/download/]. - -Στη συνέχεια, τοποθετώντας τον στον φάκελο που βρίσκεται στο σύστημα `PATH`, ο Composer γίνεται προσβάσιμος καθολικά: - -```shell -$ mv ./composer.phar ~/bin/composer # or /usr/local/bin/composer -``` - - -Χρήση στο έργο -============== - -Για να αρχίσουμε να χρησιμοποιούμε τον Composer στο έργο μας, χρειαζόμαστε μόνο το αρχείο `composer.json`. Αυτό περιγράφει τις εξαρτήσεις του έργου μας και μπορεί επίσης να περιέχει άλλα μεταδεδομένα. Ένα βασικό `composer.json` μπορεί λοιπόν να μοιάζει ως εξής: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Λέμε εδώ ότι η εφαρμογή μας (ή η βιβλιοθήκη) απαιτεί το πακέτο `nette/database` (το όνομα του πακέτου αποτελείται από το όνομα του οργανισμού και το όνομα του έργου) και θέλει μια έκδοση που αντιστοιχεί στη συνθήκη `^3.0` (δηλαδή την τελευταία έκδοση 3). - -Έχουμε λοιπόν στη ρίζα του έργου το αρχείο `composer.json` και εκκινούμε την εγκατάσταση: - -```shell -composer update -``` - -Ο Composer θα κατεβάσει το Nette Database στον φάκελο `vendor/`. Στη συνέχεια, θα δημιουργήσει το αρχείο `composer.lock`, το οποίο περιέχει πληροφορίες για τις ακριβείς εκδόσεις των βιβλιοθηκών που εγκατέστησε. - -Ο Composer θα δημιουργήσει το αρχείο `vendor/autoload.php`, το οποίο μπορούμε απλά να συμπεριλάβουμε και να αρχίσουμε να χρησιμοποιούμε τις βιβλιοθήκες χωρίς καμία περαιτέρω εργασία: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Ενημέρωση πακέτων στις τελευταίες εκδόσεις -========================================== - -Η ενημέρωση των χρησιμοποιούμενων βιβλιοθηκών στις τελευταίες εκδόσεις σύμφωνα με τις συνθήκες που ορίζονται στο `composer.json` γίνεται με την εντολή `composer update`. Για παράδειγμα, για την εξάρτηση `"nette/database": "^3.0"`, θα εγκαταστήσει την τελευταία έκδοση 3.x.x, αλλά όχι την έκδοση 4. - -Για να ενημερώσετε τις συνθήκες στο αρχείο `composer.json`, για παράδειγμα σε `"nette/database": "^4.1"`, ώστε να είναι δυνατή η εγκατάσταση της τελευταίας έκδοσης, χρησιμοποιήστε την εντολή `composer require nette/database`. - -Για να ενημερώσετε όλα τα χρησιμοποιούμενα πακέτα Nette, θα ήταν απαραίτητο να τα απαριθμήσετε όλα στη γραμμή εντολών, π.χ.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Κάτι που είναι μη πρακτικό. Χρησιμοποιήστε επομένως το απλό σενάριο "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, το οποίο θα το κάνει για εσάς: - -```shell -php composer-frontline.php -``` - - -Δημιουργία νέου έργου -===================== - -Δημιουργείτε ένα νέο έργο Nette με μία μόνο εντολή: - -```shell -composer create-project nette/web-project project-name -``` - -Ως `project-name` εισάγετε το όνομα του καταλόγου για το έργο σας και επιβεβαιώστε. Ο Composer θα κατεβάσει το αποθετήριο `nette/web-project` από το GitHub, το οποίο περιέχει ήδη το αρχείο `composer.json`, και αμέσως μετά το Nette Framework. Θα πρέπει ήδη να αρκεί μόνο να [ορίσετε τα δικαιώματα |nette:troubleshooting#Ρύθμιση δικαιωμάτων καταλόγου] εγγραφής στους φακέλους `temp/` και `log/` και το έργο θα πρέπει να ζωντανέψει. - -Αν γνωρίζετε σε ποια έκδοση PHP θα φιλοξενηθεί το έργο, μην ξεχάσετε να [την ορίσετε |#Έκδοση PHP]. - - -Έκδοση PHP -========== - -Ο Composer εγκαθιστά πάντα τις εκδόσεις των πακέτων που είναι συμβατές με την έκδοση PHP που χρησιμοποιείτε αυτή τη στιγμή (καλύτερα, με την έκδοση PHP που χρησιμοποιείται στη γραμμή εντολών κατά την εκτέλεση του Composer). Αυτό όμως πιθανότατα δεν είναι η ίδια έκδοση που χρησιμοποιεί το hosting σας. Γι' αυτό είναι πολύ σημαντικό να προσθέσετε στο αρχείο `composer.json` την πληροφορία για την έκδοση PHP στο hosting. Στη συνέχεια, θα εγκαθίστανται μόνο οι εκδόσεις των πακέτων που είναι συμβατές με το hosting. - -Το ότι το έργο θα εκτελείται, για παράδειγμα, σε PHP 8.2.3, το ορίζουμε με την εντολή: - -```shell -composer config platform.php 8.2.3 -``` - -Έτσι, η έκδοση θα καταγραφεί στο αρχείο `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Ωστόσο, ο αριθμός έκδοσης της PHP αναφέρεται και σε άλλο σημείο του αρχείου, στην ενότητα `require`. Ενώ ο πρώτος αριθμός καθορίζει για ποια έκδοση θα εγκατασταθούν τα πακέτα, ο δεύτερος αριθμός λέει για ποια έκδοση είναι γραμμένη η ίδια η εφαρμογή. Και σύμφωνα με αυτόν, για παράδειγμα, το PhpStorm ορίζει το *PHP language level*. (Φυσικά, δεν έχει νόημα αυτές οι εκδόσεις να διαφέρουν, οπότε η διπλή καταγραφή είναι αβλεψία.) Αυτή την έκδοση την ορίζετε με την εντολή: - -```shell -composer require php 8.2.3 --no-update -``` - -Ή απευθείας στο αρχείο `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Αγνόηση έκδοσης PHP -=================== - -Τα πακέτα συνήθως αναφέρουν τόσο την κατώτατη έκδοση PHP με την οποία είναι συμβατά, όσο και την ανώτατη με την οποία έχουν δοκιμαστεί. Αν σκοπεύετε να χρησιμοποιήσετε μια έκδοση PHP ακόμα νεότερη, για παράδειγμα για λόγους δοκιμών, ο Composer θα αρνηθεί να εγκαταστήσει ένα τέτοιο πακέτο. Η λύση είναι η επιλογή `--ignore-platform-req=php+`, η οποία προκαλεί τον Composer να αγνοήσει τα ανώτατα όρια της απαιτούμενης έκδοσης PHP. - - -Ψευδή μηνύματα -============== - -Κατά την αναβάθμιση πακέτων ή την αλλαγή αριθμών έκδοσης, συμβαίνει να προκύψει σύγκρουση. Ένα πακέτο έχει απαιτήσεις που έρχονται σε αντίθεση με ένα άλλο και παρόμοια. Ο Composer όμως μερικές φορές εμφανίζει ψευδή μηνύματα. Αναφέρει σύγκρουση που στην πραγματικότητα δεν υπάρχει. Σε τέτοια περίπτωση, βοηθά η διαγραφή του αρχείου `composer.lock` και η επανάληψη της προσπάθειας. - -Αν το μήνυμα σφάλματος επιμένει, τότε εννοείται σοβαρά και πρέπει να διαβάσετε από αυτό τι και πώς να τροποποιήσετε. - - -Packagist.org - κεντρικό αποθετήριο -=================================== - -Το [Packagist |https://packagist.org] είναι το κύριο αποθετήριο στο οποίο ο Composer προσπαθεί να αναζητήσει πακέτα, αν δεν του πούμε διαφορετικά. Μπορούμε επίσης να δημοσιεύσουμε εδώ τα δικά μας πακέτα. - - -Τι γίνεται αν δεν θέλουμε να χρησιμοποιήσουμε το κεντρικό αποθετήριο; ---------------------------------------------------------------------- - -Αν έχουμε εσωτερικές εταιρικές εφαρμογές, τις οποίες απλά δεν μπορούμε να φιλοξενήσουμε δημόσια, τότε δημιουργούμε γι' αυτές ένα εταιρικό αποθετήριο. - -Περισσότερα για το θέμα των αποθετηρίων [στην επίσημη τεκμηρίωση |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Ένα θεμελιώδες χαρακτηριστικό του Composer είναι ότι παρέχει αυτόματη φόρτωση για όλες τις κλάσεις που έχει εγκαταστήσει, την οποία ξεκινάτε συμπεριλαμβάνοντας το αρχείο `vendor/autoload.php`. - -Ωστόσο, είναι δυνατό να χρησιμοποιήσετε τον Composer και για τη φόρτωση άλλων κλάσεων εκτός του φακέλου `vendor`. Η πρώτη επιλογή είναι να αφήσετε τον Composer να σαρώσει καθορισμένους φακέλους και υποφακέλους, να βρει όλες τις κλάσεις και να τις συμπεριλάβει στον autoloader. Αυτό επιτυγχάνεται ορίζοντας το `autoload > classmap` στο `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # περιλαμβάνει τον φάκελο src/ και τους υποφακέλους του - ] - } -} -``` - -Στη συνέχεια, είναι απαραίτητο σε κάθε αλλαγή να εκτελείτε την εντολή `composer dumpautoload` και να αφήνετε τους πίνακες αυτόματης φόρτωσης να αναδημιουργηθούν. Αυτό είναι εξαιρετικά άβολο και είναι πολύ καλύτερο να αναθέσετε αυτή την εργασία στο [RobotLoader|robot-loader:], το οποίο εκτελεί την ίδια δραστηριότητα αυτόματα στο παρασκήνιο και πολύ πιο γρήγορα. - -Η δεύτερη επιλογή είναι η τήρηση του [PSR-4|https://www.php-fig.org/psr/psr-4/]. Με απλά λόγια, πρόκειται για ένα σύστημα όπου οι χώροι ονομάτων και τα ονόματα κλάσεων αντιστοιχούν στη δομή καταλόγων και τα ονόματα αρχείων, δηλαδή π.χ. το `App\Core\RouterFactory` θα βρίσκεται στο αρχείο `/path/to/App/Core/RouterFactory.php`. Παράδειγμα διαμόρφωσης: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # ο χώρος ονομάτων App\ βρίσκεται στον κατάλογο app/ - } - } -} -``` - -Πώς ακριβώς να διαμορφώσετε τη συμπεριφορά θα μάθετε στην [τεκμηρίωση του Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Δοκιμή νέων εκδόσεων -==================== - -Θέλετε να δοκιμάσετε μια νέα αναπτυξιακή έκδοση ενός πακέτου. Πώς να το κάνετε; Πρώτα, προσθέστε στο αρχείο `composer.json` αυτό το ζεύγος επιλογών, το οποίο επιτρέπει την εγκατάσταση αναπτυξιακών εκδόσεων πακέτων, αλλά καταφεύγει σε αυτό μόνο στην περίπτωση που δεν υπάρχει κανένας συνδυασμός σταθερών εκδόσεων που να ικανοποιεί τις απαιτήσεις: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Στη συνέχεια, συνιστούμε να διαγράψετε το αρχείο `composer.lock`, μερικές φορές ο Composer ανεξήγητα αρνείται την εγκατάσταση και αυτό λύνει το πρόβλημα. - -Ας υποθέσουμε ότι πρόκειται για το πακέτο `nette/utils` και η νέα έκδοση έχει τον αριθμό 4.0. Την εγκαθιστάτε με την εντολή: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Ή μπορείτε να εγκαταστήσετε μια συγκεκριμένη έκδοση, για παράδειγμα 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Αν όμως από τη βιβλιοθήκη εξαρτάται ένα άλλο πακέτο που είναι κλειδωμένο σε παλαιότερη έκδοση (π.χ. `^3.1`), τότε είναι ιδανικό να ενημερώσετε το πακέτο, ώστε να λειτουργεί με τη νέα έκδοση. Αν όμως θέλετε απλώς να παρακάμψετε τον περιορισμό και να αναγκάσετε τον Composer να εγκαταστήσει την αναπτυξιακή έκδοση και να προσποιηθεί ότι πρόκειται για παλαιότερη έκδοση (π.χ. 3.1.6), μπορείτε να χρησιμοποιήσετε τη λέξη-κλειδί `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Κλήση εντολών -============= - -Μέσω του Composer μπορείτε να καλέσετε τις δικές σας προκαθορισμένες εντολές και σενάρια, σαν να ήταν εγγενείς εντολές του Composer. Για σενάρια που βρίσκονται στον φάκελο `vendor/bin`, δεν χρειάζεται να αναφέρετε αυτόν τον φάκελο. - -Ως παράδειγμα, ορίζουμε στο αρχείο `composer.json` ένα σενάριο που χρησιμοποιεί το [Nette Tester|tester:] για την εκτέλεση δοκιμών: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Στη συνέχεια, εκτελούμε τις δοκιμές χρησιμοποιώντας το `composer tester`. Μπορούμε να καλέσουμε την εντολή ακόμα κι αν δεν βρισκόμαστε στον ριζικό φάκελο του έργου, αλλά σε κάποιον υποφάκελο. - - -Στείλτε ευχαριστίες -=================== - -Θα σας δείξουμε ένα κόλπο με το οποίο θα ευχαριστήσετε τους δημιουργούς open source. Με έναν απλό τρόπο, δίνετε αστέρι στο GitHub στις βιβλιοθήκες που χρησιμοποιεί το έργο σας. Αρκεί να εγκαταστήσετε τη βιβλιοθήκη `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -Και στη συνέχεια να εκτελέσετε: - -```shell -composer thanks -``` - -Δοκιμάστε το! - - -Διαμόρφωση -========== - -Ο Composer είναι στενά συνδεδεμένος με το εργαλείο διαχείρισης εκδόσεων [Git |https://git-scm.com]. Αν δεν το έχετε εγκατεστημένο, πρέπει να πείτε στον Composer να μην το χρησιμοποιεί: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/el/creating-editing-form.texy b/best-practices/el/creating-editing-form.texy deleted file mode 100644 index 4566a81628..0000000000 --- a/best-practices/el/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Φόρμα για τη δημιουργία και την επεξεργασία εγγραφών -**************************************************** - -.[perex] -Πώς να υλοποιήσετε σωστά την προσθήκη και την επεξεργασία εγγραφών στο Nette, χρησιμοποιώντας την ίδια φόρμα και για τις δύο λειτουργίες; - -Σε πολλές περιπτώσεις, οι φόρμες για την προσθήκη και την επεξεργασία εγγραφών είναι ίδιες, διαφέροντας ίσως μόνο στην ετικέτα του κουμπιού. Θα δείξουμε παραδείγματα απλών presenters, όπου θα χρησιμοποιήσουμε τη φόρμα πρώτα για την προσθήκη εγγραφής, μετά για την επεξεργασία και τέλος θα συνδυάσουμε τις δύο λύσεις. - - -Προσθήκη εγγραφής ------------------ - -Παράδειγμα presenter που χρησιμεύει για την προσθήκη εγγραφής. Την πραγματική εργασία με τη βάση δεδομένων θα την αφήσουμε στην κλάση `Facade`, ο κώδικας της οποίας δεν είναι ουσιαστικός για το παράδειγμα. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... προσθέτουμε πεδία φόρμας ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // προσθήκη εγγραφής στη βάση δεδομένων - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Επεξεργασία εγγραφής --------------------- - -Τώρα θα δείξουμε πώς θα έμοιαζε ο presenter που χρησιμεύει για την επεξεργασία εγγραφής: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // έλεγχος ύπαρξης εγγραφής - || !$this->facade->isEditAllowed(/*...*/) // έλεγχος δικαιωμάτων - ) { - $this->error(); // σφάλμα 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // ελέγχουμε ότι η action είναι 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... προσθέτουμε πεδία φόρμας ... - - $form->setDefaults($this->record); // ορισμός προεπιλεγμένων τιμών - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // ενημέρωση εγγραφής - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -Στη μέθοδο *action*, η οποία εκτελείται αμέσως στην αρχή του [κύκλου ζωής του presenter |application:presenters#Κύκλος ζωής του presenter], ελέγχουμε την ύπαρξη της εγγραφής και τα δικαιώματα του χρήστη να την επεξεργαστεί. - -Αποθηκεύουμε την εγγραφή στην ιδιότητα `$record`, ώστε να την έχουμε διαθέσιμη στη μέθοδο `createComponentRecordForm()` για τον ορισμό των προεπιλεγμένων τιμών, και στη `recordFormSucceeded()` για το ID. Μια εναλλακτική λύση θα ήταν να ορίσουμε τις προεπιλεγμένες τιμές απευθείας στην `actionEdit()` και να λάβουμε την τιμή του ID, η οποία είναι μέρος του URL, χρησιμοποιώντας το `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // έλεγχος ύπαρξης και έλεγχος δικαιωμάτων - ) { - $this->error(); - } - - // ορισμός προεπιλεγμένων τιμών της φόρμας - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Ωστόσο, και αυτό θα έπρεπε να είναι **το πιο σημαντικό συμπέρασμα όλου του κώδικα**, πρέπει κατά τη δημιουργία της φόρμας να βεβαιωθούμε ότι η action είναι όντως `edit`. Διότι διαφορετικά, ο έλεγχος στη μέθοδο `actionEdit()` δεν θα είχε πραγματοποιηθεί καθόλου! - - -Ίδια φόρμα για προσθήκη και επεξεργασία ---------------------------------------- - -Και τώρα συνδυάζουμε τους δύο presenters σε έναν. Είτε θα μπορούσαμε στη μέθοδο `createComponentRecordForm()` να διακρίνουμε ποια action είναι και ανάλογα να διαμορφώσουμε τη φόρμα, είτε μπορούμε να το αφήσουμε απευθείας στις action-μεθόδους και να απαλλαγούμε από τη συνθήκη: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private 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 // έλεγχος ύπαρξης εγγραφής - || !$this->facade->isEditAllowed(/*...*/) // έλεγχος δικαιωμάτων - ) { - $this->error(); // σφάλμα 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // ορισμός προεπιλεγμένων τιμών - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // ελέγχουμε ότι η action είναι 'add' ή 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... προσθέτουμε πεδία φόρμας ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // προσθήκη εγγραφής στη βάση δεδομένων - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // ενημέρωση εγγραφής - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/el/dynamic-snippets.texy b/best-practices/el/dynamic-snippets.texy deleted file mode 100644 index 134ee63491..0000000000 --- a/best-practices/el/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Δυναμικά snippets -***************** - -Αρκετά συχνά κατά την ανάπτυξη εφαρμογών προκύπτει η ανάγκη εκτέλεσης λειτουργιών AJAX, για παράδειγμα, σε μεμονωμένες γραμμές πίνακα ή στοιχεία λίστας. Ως παράδειγμα, μπορούμε να επιλέξουμε την εμφάνιση άρθρων, όπου για κάθε ένα από αυτά επιτρέπουμε στον συνδεδεμένο χρήστη να επιλέξει βαθμολογία "μου αρέσει/δεν μου αρέσει". Ο κώδικας του presenter και του αντίστοιχου template χωρίς AJAX θα μοιάζει περίπου ως εξής (παραθέτω τα πιο σημαντικά αποσπάσματα, ο κώδικας υπολογίζει την ύπαρξη μιας υπηρεσίας για την επισήμανση της βαθμολογίας και τη λήψη της συλλογής άρθρων - η συγκεκριμένη υλοποίηση δεν είναι σημαντική για τους σκοπούς αυτού του οδηγού): - -```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 -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>μου αρέσει</a> - {else} - <a n:href="unlike! $article->id" class=ajax>δεν μου αρέσει πια</a> - {/if} -</article> -``` - - -Ajaxification -============= - -Ας εξοπλίσουμε τώρα αυτήν την απλή εφαρμογή με AJAX. Η αλλαγή της βαθμολογίας ενός άρθρου δεν είναι τόσο σημαντική ώστε να απαιτείται ανακατεύθυνση, και επομένως θα έπρεπε ιδανικά να γίνεται με AJAX στο παρασκήνιο. Θα χρησιμοποιήσουμε το [βοηθητικό script από τα add-ons |application:ajax#Naja] με τη συνήθη σύμβαση ότι οι σύνδεσμοι AJAX έχουν την CSS κλάση `ajax`. - -Ωστόσο, πώς να το κάνουμε συγκεκριμένα; Το Nette προσφέρει 2 δρόμους: τον δρόμο των λεγόμενων δυναμικών snippets και τον δρόμο των components. Και οι δύο έχουν τα υπέρ και τα κατά τους, και γι' αυτό θα τους παρουσιάσουμε έναν προς έναν. - - -Ο δρόμος των δυναμικών snippets -=============================== - -Ένα δυναμικό snippet σημαίνει στην ορολογία του Latte μια συγκεκριμένη περίπτωση χρήσης του tag `{snippet}`, όπου στο όνομα του snippet χρησιμοποιείται μια μεταβλητή. Ένα τέτοιο snippet δεν μπορεί να βρίσκεται οπουδήποτε στο template - πρέπει να περιβάλλεται από ένα στατικό snippet, δηλαδή ένα συνηθισμένο, ή μέσα σε `{snippetArea}`. Θα μπορούσαμε να τροποποιήσουμε το template μας ως εξής. - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>μου αρέσει</a> - {else} - <a n:href="unlike! $article->id" class=ajax>δεν μου αρέσει πια</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Κάθε άρθρο ορίζει τώρα ένα snippet, το οποίο έχει στο όνομά του το ID του άρθρου. Όλα αυτά τα snippets είναι στη συνέχεια ομαδοποιημένα μαζί με ένα snippet με το όνομα `articlesContainer`. Αν παραλείπαμε αυτό το περιβάλλον snippet, το Latte θα μας ειδοποιούσε με μια εξαίρεση. - -Μας μένει να συμπληρώσουμε την επανασχεδίαση στον presenter - αρκεί να επανασχεδιάσουμε το στατικό περιτύλιγμα. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- δεν χρειάζεται - } else { - $this->redirect('this'); - } -} -``` - -Ομοίως, τροποποιούμε και την αδελφή μέθοδο `handleUnlike()`, και το AJAX είναι λειτουργικό! - -Η λύση έχει όμως ένα σκοτεινό σημείο. Αν εξετάζαμε περισσότερο πώς διεξάγεται το αίτημα AJAX, θα διαπιστώναμε ότι παρόλο που εξωτερικά η εφαρμογή φαίνεται οικονομική (επιστρέφει μόνο ένα μοναδικό snippet για το συγκεκριμένο άρθρο), στην πραγματικότητα στον server σχεδίασε όλα τα snippets. Το επιθυμητό snippet τοποθετήθηκε στο payload, και τα υπόλοιπα απορρίφθηκαν (εντελώς άσκοπα τα απέκτησε επίσης από τη βάση δεδομένων). - -Για να βελτιστοποιήσουμε αυτή τη διαδικασία, θα πρέπει να παρέμβουμε εκεί όπου περνάμε τη συλλογή `$articles` στο template (ας πούμε στη μέθοδο `renderDefault()`). Θα εκμεταλλευτούμε το γεγονός ότι η επεξεργασία των σημάτων γίνεται πριν από τις μεθόδους `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Τώρα, κατά την επεξεργασία του σήματος, στο template περνιέται αντί για τη συλλογή με όλα τα άρθρα, μόνο ένας πίνακας με ένα μοναδικό άρθρο - αυτό που θέλουμε να σχεδιάσουμε και να στείλουμε στο payload στον browser. Το `{foreach}` λοιπόν θα εκτελεστεί μόνο μία φορά και κανένα επιπλέον snippet δεν θα σχεδιαστεί. - - -Ο δρόμος των components -======================= - -Ένας εντελώς διαφορετικός τρόπος λύσης αποφεύγει τα δυναμικά snippets. Το κόλπο έγκειται στη μεταφορά ολόκληρης της λογικής σε ένα ξεχωριστό component - από τώρα και στο εξής, η εισαγωγή βαθμολογίας δεν θα γίνεται από τον presenter, αλλά από ένα εξειδικευμένο `LikeControl`. Η κλάση θα μοιάζει ως εξής (εκτός από αυτό, θα περιέχει επίσης τις μεθόδους `render`, `handleUnlike` κ.λπ.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private 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 του component: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>μου αρέσει</a> - {else} - <a n:href="unlike!" class=ajax>δεν μου αρέσει πια</a> - {/if} -{/snippet} -``` - -Φυσικά, το template της προβολής θα αλλάξει και θα πρέπει να προσθέσουμε ένα factory στον presenter. Επειδή θα δημιουργήσουμε το component τόσες φορές όσα άρθρα λάβουμε από τη βάση δεδομένων, θα χρησιμοποιήσουμε την κλάση [Multiplier |application:Multiplier] για τον "πολλαπλασιασμό" του. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Το template της προβολής θα μειωθεί στο ελάχιστο απαραίτητο (και εντελώς απαλλαγμένο από snippets!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Έχουμε σχεδόν τελειώσει: η εφαρμογή τώρα θα λειτουργεί με AJAX. Και εδώ μας περιμένει η βελτιστοποίηση της εφαρμογής, επειδή λόγω της χρήσης του Nette Database, κατά την επεξεργασία του σήματος φορτώνονται άσκοπα όλα τα άρθρα από τη βάση δεδομένων αντί για ένα. Το πλεονέκτημα όμως είναι ότι δεν θα γίνει η σχεδίασή τους, επειδή θα αποδοθεί πραγματικά μόνο το component μας. - -{{priority: -1}} diff --git a/best-practices/el/editors-and-tools.texy b/best-practices/el/editors-and-tools.texy deleted file mode 100644 index a5d46f739c..0000000000 --- a/best-practices/el/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Επεξεργαστές & εργαλεία -*********************** - -.[perex] -Μπορεί να είστε ένας ικανός προγραμματιστής, αλλά μόνο με καλά εργαλεία γίνεστε μάστορας. Σε αυτό το κεφάλαιο θα βρείτε συμβουλές για σημαντικά εργαλεία, επεξεργαστές και plugins. - - -IDE editor -========== - -Συνιστούμε ανεπιφύλακτα τη χρήση ενός πλήρους IDE για την ανάπτυξη, όπως το PhpStorm, το NetBeans, το VS Code, και όχι απλώς ενός επεξεργαστή κειμένου με υποστήριξη PHP. Η διαφορά είναι πραγματικά θεμελιώδης. Δεν υπάρχει λόγος να αρκεστείτε σε έναν απλό επεξεργαστή που, αν και μπορεί να χρωματίζει τη σύνταξη, δεν φτάνει τις δυνατότητες ενός κορυφαίου IDE, το οποίο προτείνει με ακρίβεια, ελέγχει για σφάλματα, μπορεί να αναδιαμορφώσει τον κώδικα και πολλά άλλα. Ορισμένα IDE είναι επί πληρωμή, άλλα είναι ακόμη και δωρεάν. - -Το **NetBeans IDE** έχει ενσωματωμένη υποστήριξη για Nette, Latte και NEON. - -**PhpStorm**: εγκαταστήστε αυτά τα plugins στο `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: βρείτε το plugin "Nette Latte + Neon" στο marketplace. - -Συνδέστε επίσης το Tracy με τον επεξεργαστή σας. Όταν εμφανίζεται μια σελίδα σφάλματος, θα μπορείτε να κάνετε κλικ στα ονόματα των αρχείων και αυτά θα ανοίγουν στον επεξεργαστή με τον κέρσορα στην αντίστοιχη γραμμή. Διαβάστε [πώς να διαμορφώσετε το σύστημα |tracy:open-files-in-ide]. - - -PHPStan -======= - -Το PHPStan είναι ένα εργαλείο που εντοπίζει λογικά σφάλματα στον κώδικα πριν τον εκτελέσετε. - -Το εγκαθιστούμε χρησιμοποιώντας το Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Δημιουργούμε στο έργο ένα αρχείο διαμόρφωσης `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Και στη συνέχεια το αφήνουμε να αναλύσει τις κλάσεις στον φάκελο `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Μπορείτε να βρείτε εξαντλητική τεκμηρίωση απευθείας στην [ιστοσελίδα του PHPStan |https://phpstan.org]. - - -Code Checker -============ - -Ο [Code Checker|code-checker:] ελέγχει και ενδεχομένως διορθώνει ορισμένα από τα τυπικά σφάλματα στους πηγαίους κώδικές σας: - -- αφαιρεί το [BOM |nette:glossary#BOM] -- ελέγχει την εγκυρότητα των templates [Latte |latte:] -- ελέγχει την εγκυρότητα των αρχείων `.neon`, `.php` και `.json` -- ελέγχει την ύπαρξη [χαρακτήρων ελέγχου |nette:glossary#Control characters] -- ελέγχει αν το αρχείο είναι κωδικοποιημένο σε UTF-8 -- ελέγχει λανθασμένα γραμμένα `/* @anotace */` (λείπει ο αστερίσκος) -- αφαιρεί το τελικό `?>` από τα αρχεία PHP -- αφαιρεί τα δεξιά κενά και τις περιττές γραμμές στο τέλος του αρχείου -- κανονικοποιεί τους διαχωριστές γραμμών σε συστήματος (αν δώσετε την επιλογή `-l`) - - -Composer -======== - -Ο [Composer |Composer] είναι ένα εργαλείο διαχείρισης εξαρτήσεων στο PHP. Μας επιτρέπει να δηλώνουμε αυθαίρετα πολύπλοκες εξαρτήσεις μεμονωμένων βιβλιοθηκών και στη συνέχεια τις εγκαθιστά για εμάς στο έργο μας. - - -Requirements Checker -==================== - -Ήταν ένα εργαλείο που δοκίμαζε το περιβάλλον εκτέλεσης του server και ενημέρωνε αν (και σε ποιο βαθμό) ήταν δυνατό να χρησιμοποιηθεί το framework. Επί του παρόντος, το Nette μπορεί να χρησιμοποιηθεί σε κάθε server που έχει την ελάχιστη απαιτούμενη έκδοση PHP. diff --git a/best-practices/el/form-reuse.texy b/best-practices/el/form-reuse.texy deleted file mode 100644 index 28f0bf1b38..0000000000 --- a/best-practices/el/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Επαναχρησιμοποίηση φορμών σε πολλαπλά μέρη -****************************************** - -.[perex] -Στο Nette έχετε στη διάθεσή σας αρκετές επιλογές για να χρησιμοποιήσετε την ίδια φόρμα σε πολλαπλά μέρη και να μην επαναλαμβάνετε τον κώδικα. Σε αυτό το άρθρο θα δείξουμε διάφορες λύσεις, συμπεριλαμβανομένων εκείνων που θα έπρεπε να αποφύγετε. - - -Factory φορμών -============== - -Μία από τις βασικές προσεγγίσεις για τη χρήση του ίδιου component σε πολλαπλά μέρη είναι η δημιουργία μιας μεθόδου ή κλάσης που παράγει αυτό το component, και στη συνέχεια η κλήση αυτής της μεθόδου σε διάφορα μέρη της εφαρμογής. Μια τέτοια μέθοδος ή κλάση ονομάζεται *factory*. Μην τη συγχέετε με το design pattern *factory method*, το οποίο περιγράφει έναν συγκεκριμένο τρόπο χρήσης των factories και δεν σχετίζεται με αυτό το θέμα. - -Ως παράδειγμα, θα δημιουργήσουμε ένα factory που θα κατασκευάζει μια φόρμα επεξεργασίας: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - return $form; - } -} -``` - -Τώρα μπορείτε να χρησιμοποιήσετε αυτό το factory σε διάφορα μέρη της εφαρμογής σας, για παράδειγμα σε presenters ή components. Και αυτό γίνεται [ζητώντας το ως εξάρτηση |dependency-injection:passing-dependencies]. Πρώτα, λοιπόν, καταχωρούμε την κλάση στο αρχείο διαμόρφωσης: - -```neon -services: - - FormFactory -``` - -Και στη συνέχεια τη χρησιμοποιούμε στον presenter: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // επεξεργασία των απεσταλμένων δεδομένων - }; - return $form; - } -} -``` - -Μπορείτε να επεκτείνετε το factory φορμών με επιπλέον μεθόδους για τη δημιουργία άλλων τύπων φορμών ανάλογα με τις ανάγκες της εφαρμογής σας. Και φυσικά, μπορούμε να προσθέσουμε και μια μέθοδο που δημιουργεί μια βασική φόρμα χωρίς στοιχεία, και αυτή θα χρησιμοποιείται από τις άλλες μεθόδους: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - return $form; - } -} -``` - -Η μέθοδος `createForm()` προς το παρόν δεν κάνει τίποτα χρήσιμο, αλλά αυτό θα αλλάξει γρήγορα. - - -Εξαρτήσεις του factory -====================== - -Με τον καιρό θα φανεί ότι χρειαζόμαστε οι φόρμες να είναι πολυγλωσσικές. Αυτό σημαίνει ότι σε όλες τις φόρμες πρέπει να ορίσουμε τον λεγόμενο [translator |forms:rendering#Μετάφραση]. Για τον σκοπό αυτό, τροποποιούμε την κλάση `FormFactory` ώστε να δέχεται το αντικείμενο `Translator` ως εξάρτηση στον constructor, και το περνάμε στη φόρμα: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Επειδή η μέθοδος `createForm()` καλείται και από τις άλλες μεθόδους που δημιουργούν συγκεκριμένες φόρμες, αρκεί να ορίσουμε τον translator μόνο σε αυτήν. Και τελειώσαμε. Δεν χρειάζεται να αλλάξουμε τον κώδικα κανενός presenter ή component, πράγμα που είναι εξαιρετικό. - - -Περισσότερες κλάσεις factory -============================ - -Εναλλακτικά, μπορείτε να δημιουργήσετε περισσότερες κλάσεις για κάθε φόρμα που θέλετε να χρησιμοποιήσετε στην εφαρμογή σας. Αυτή η προσέγγιση μπορεί να αυξήσει την αναγνωσιμότητα του κώδικα και να διευκολύνει τη διαχείριση των φορμών. Το αρχικό `FormFactory` θα το αφήσουμε να δημιουργεί μόνο μια καθαρή φόρμα με βασική διαμόρφωση (για παράδειγμα με υποστήριξη μεταφράσεων) και για τη φόρμα επεξεργασίας θα δημιουργήσουμε ένα νέο factory `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ χρήση σύνθεσης -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - return $form; - } -} -``` - -Είναι πολύ σημαντικό η σχέση μεταξύ των κλάσεων `FormFactory` και `EditFormFactory` να υλοποιείται με [σύνθεση |nette:introduction-to-object-oriented-programming#Σύνθεση], και όχι με [κληρονομικότητα αντικειμένων |nette:introduction-to-object-oriented-programming#Κληρονομικότητα]: - -```php -// ⛔ ΟΧΙ ΕΤΣΙ! Η ΚΛΗΡΟΝΟΜΙΚΟΤΗΤΑ ΔΕΝ ΑΝΗΚΕΙ ΕΔΩ -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - return $form; - } -} -``` - -Η χρήση κληρονομικότητας θα ήταν σε αυτή την περίπτωση εντελώς αντιπαραγωγική. Θα αντιμετωπίζατε προβλήματα πολύ γρήγορα. Για παράδειγμα, τη στιγμή που θα θέλατε να προσθέσετε παραμέτρους στη μέθοδο `create()`; η PHP θα ανέφερε σφάλμα ότι η υπογραφή της διαφέρει από την γονική. Ή κατά το πέρασμα εξαρτήσεων στην κλάση `EditFormFactory` μέσω του constructor. Θα προέκυπτε η κατάσταση που ονομάζουμε [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. - -Γενικά, είναι καλύτερο να προτιμάτε τη [σύνθεση έναντι κληρονομικότητας |dependency-injection:faq#Γιατί προτιμάται η σύνθεση composition έναντι της κληρονομικότητας]. - - -Χειρισμός φόρμας -================ - -Ο χειρισμός της φόρμας, που καλείται μετά την επιτυχή υποβολή, μπορεί επίσης να είναι μέρος της κλάσης factory. Θα λειτουργεί έτσι ώστε να παραδίδει τα υποβληθέντα δεδομένα στο μοντέλο για επεξεργασία. Τυχόν σφάλματα θα τα [επιστρέψει |forms:validation#Σφάλματα κατά την Επεξεργασία] στη φόρμα. Το μοντέλο στο ακόλουθο παράδειγμα αντιπροσωπεύεται από την κλάση `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // επεξεργασία των απεσταλμένων δεδομένων - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Την ίδια την ανακατεύθυνση όμως θα την αφήσουμε στον presenter. Αυτός θα προσθέσει στο event `onSuccess` έναν επιπλέον handler που θα πραγματοποιήσει την ανακατεύθυνση. Χάρη σε αυτό, θα είναι δυνατό να χρησιμοποιηθεί η φόρμα σε διάφορους presenters και σε καθέναν να γίνει ανακατεύθυνση αλλού. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Η εγγραφή αποθηκεύτηκε'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Αυτή η λύση εκμεταλλεύεται την ιδιότητα των φορμών ότι όταν καλείται το `addError()` πάνω στη φόρμα ή στα στοιχεία της, ο επόμενος handler `onSuccess` δεν καλείται πλέον. - - -Κληρονομικότητα από την κλάση Form -================================== - -Η συναρμολογημένη φόρμα δεν πρέπει να είναι απόγονος της φόρμας. Με άλλα λόγια, μην χρησιμοποιείτε αυτή τη λύση: - -```php -// ⛔ ΟΧΙ ΕΤΣΙ! Η ΚΛΗΡΟΝΟΜΙΚΟΤΗΤΑ ΔΕΝ ΑΝΗΚΕΙ ΕΔΩ -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $this->addSubmit('send', 'Αποστολή'); - $this->setTranslator($translator); - } -} -``` - -Αντί να συναρμολογείτε τη φόρμα στον constructor, χρησιμοποιήστε ένα factory. - -Πρέπει να συνειδητοποιήσετε ότι η κλάση `Form` είναι πρωτίστως ένα εργαλείο για τη συναρμολόγηση μιας φόρμας, δηλαδή ένας *form builder*. Και η συναρμολογημένη φόρμα μπορεί να θεωρηθεί ως προϊόν της. Όμως το προϊόν δεν είναι μια ειδική περίπτωση του builder, δεν υπάρχει μεταξύ τους σχέση *is a* που αποτελεί τη βάση της κληρονομικότητας. - - -Component με φόρμα -================== - -Μια εντελώς διαφορετική προσέγγιση είναι η δημιουργία ενός [component |application:components], μέρος του οποίου είναι μια φόρμα. Αυτό δίνει νέες δυνατότητες, για παράδειγμα την απόδοση της φόρμας με συγκεκριμένο τρόπο, καθώς μέρος του component είναι και ένα template. Ή μπορεί να χρησιμοποιηθούν σήματα για επικοινωνία AJAX και φόρτωση πληροφοριών στη φόρμα, για παράδειγμα για προτάσεις, κ.λπ. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Τίτλος:'); - // εδώ προστίθενται άλλα πεδία φόρμας - $form->addSubmit('send', 'Αποστολή'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // επεξεργασία των απεσταλμένων δεδομένων - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // εκκίνηση του event - $this->onSave($this, $data); - } -} -``` - -Θα δημιουργήσουμε επίσης ένα factory που θα παράγει αυτό το component. Αρκεί να [καταχωρήσετε το interface του |application:components#Components με Εξαρτήσεις]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -Και να το προσθέσουμε στο αρχείο διαμόρφωσης: - -```neon -services: - - EditControlFactory -``` - -Και τώρα μπορούμε ήδη να ζητήσουμε το factory και να το χρησιμοποιήσουμε στον presenter: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // ή ανακατευθύνουμε στο αποτέλεσμα της επεξεργασίας, π.χ.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/el/inject-method-attribute.texy b/best-practices/el/inject-method-attribute.texy deleted file mode 100644 index a13bfcff48..0000000000 --- a/best-practices/el/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Μέθοδοι και attributes inject -***************************** - -.[perex] -Σε αυτό το άρθρο, θα επικεντρωθούμε στους διάφορους τρόπους περάσματος εξαρτήσεων στους presenters στο Nette framework. Θα συγκρίνουμε τον προτιμώμενο τρόπο, που είναι ο constructor, με άλλες επιλογές, όπως οι μέθοδοι και τα attributes `inject`. - -Και για τους presenters ισχύει ότι το πέρασμα εξαρτήσεων μέσω του [constructor |dependency-injection:passing-dependencies#Παράδοση μέσω κατασκευαστή] είναι ο προτιμώμενος δρόμος. Αν όμως δημιουργείτε έναν κοινό πρόγονο από τον οποίο κληρονομούν άλλοι presenters (π.χ. `BasePresenter`), και αυτός ο πρόγονος έχει επίσης εξαρτήσεις, προκύπτει ένα πρόβλημα που ονομάζουμε [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Αυτό μπορεί να παρακαμφθεί χρησιμοποιώντας εναλλακτικούς δρόμους, που αντιπροσωπεύουν οι μέθοδοι και τα attributes (annotations) `inject`. - - -Μέθοδοι `inject*()` -=================== - -Πρόκειται για μια μορφή περάσματος εξάρτησης με [setter |dependency-injection:passing-dependencies#Παράδοση μέσω setter]. Το όνομα αυτών των setters ξεκινά με το πρόθεμα `inject`. Το Nette DI καλεί αυτόματα τις μεθόδους με αυτό το όνομα αμέσως μετά τη δημιουργία της παρουσίας του presenter και τους περνά όλες τις απαιτούμενες εξαρτήσεις. Πρέπει επομένως να δηλώνονται ως public. - -Οι μέθοδοι `inject*()` μπορούν να θεωρηθούν ως ένα είδος επέκτασης του constructor σε περισσότερες μεθόδους. Χάρη σε αυτό, ο `BasePresenter` μπορεί να λάβει εξαρτήσεις μέσω μιας άλλης μεθόδου και να αφήσει τον constructor ελεύθερο για τους απογόνους του: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Ο presenter μπορεί να περιέχει οποιονδήποτε αριθμό μεθόδων `inject*()` και καθεμία μπορεί να έχει οποιονδήποτε αριθμό παραμέτρων. Ταιριάζουν εξαιρετικά επίσης σε περιπτώσεις όπου ο presenter [αποτελείται από traits |presenter-traits] και καθεμία από αυτές απαιτεί τη δική της εξάρτηση. - - -Attributes `Inject` -=================== - -Πρόκειται για μια μορφή [injection στην ιδιότητα |dependency-injection:passing-dependencies#Ρύθμιση μεταβλητής]. Αρκεί να επισημάνετε σε ποιες μεταβλητές πρέπει να γίνει inject, και το Nette DI περνά αυτόματα τις εξαρτήσεις αμέσως μετά τη δημιουργία της παρουσίας του presenter. Για να μπορέσει να τις εισαγάγει, είναι απαραίτητο να δηλώνονται ως public. - -Επισημαίνουμε τις properties με το attribute: (παλαιότερα χρησιμοποιούνταν η annotation `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // αυτή η γραμμή είναι σημαντική - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Το πλεονέκτημα αυτού του τρόπου περάσματος εξαρτήσεων ήταν η πολύ οικονομική μορφή γραφής. Ωστόσο, με την έλευση του [constructor property promotion |https://blog.nette.org/el/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], φαίνεται ευκολότερο να χρησιμοποιηθεί ο constructor. - -Αντίθετα, αυτός ο τρόπος πάσχει από τις ίδιες αδυναμίες με το πέρασμα εξαρτήσεων σε properties γενικά: δεν έχουμε έλεγχο στις αλλαγές στη μεταβλητή και ταυτόχρονα η μεταβλητή γίνεται μέρος του δημόσιου interface της κλάσης, πράγμα που είναι ανεπιθύμητο. diff --git a/best-practices/el/lets-create-contact-form.texy b/best-practices/el/lets-create-contact-form.texy deleted file mode 100644 index fa4e52cc69..0000000000 --- a/best-practices/el/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Δημιουργούμε μια φόρμα επικοινωνίας -*********************************** - -.[perex] -Θα δούμε πώς να δημιουργήσουμε μια φόρμα επικοινωνίας στο Nette, συμπεριλαμβανομένης της αποστολής μέσω email. Ας ξεκινήσουμε λοιπόν! - -Πρώτα πρέπει να δημιουργήσουμε ένα νέο έργο. Πώς να το κάνετε αυτό εξηγείται στη σελίδα [Ξεκινώντας |nette:installation]. Και μετά μπορούμε ήδη να αρχίσουμε να δημιουργούμε τη φόρμα. - -Ο ευκολότερος τρόπος είναι να δημιουργήσετε τη [φόρμα απευθείας στον presenter |forms:in-presenter]. Μπορούμε να χρησιμοποιήσουμε τον προετοιμασμένο `HomePresenter`. Σε αυτόν θα προσθέσουμε το component `contactForm` που αντιπροσωπεύει τη φόρμα. Θα το κάνουμε γράφοντας στον κώδικα τη μέθοδο factory `createComponentContactForm()`, η οποία θα κατασκευάσει το component: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Όνομα:') - ->setRequired('Εισάγετε όνομα'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Εισάγετε e-mail'); - $form->addTextarea('message', 'Μήνυμα:') - ->setRequired('Εισάγετε μήνυμα'); - $form->addSubmit('send', 'Αποστολή'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, $data): void - { - // αποστολή email - } -} -``` - -Όπως βλέπετε, δημιουργήσαμε δύο μεθόδους. Η πρώτη μέθοδος `createComponentContactForm()` δημιουργεί μια νέα φόρμα. Αυτή έχει πεδία για όνομα, email και μήνυμα, τα οποία προσθέτουμε με τις μεθόδους `addText()`, `addEmail()` και `addTextArea()`. Προσθέσαμε επίσης ένα κουμπί για την αποστολή της φόρμας. Αλλά τι γίνεται αν ο χρήστης δεν συμπληρώσει κάποιο πεδίο; Σε αυτή την περίπτωση, θα πρέπει να τον ενημερώσουμε ότι είναι υποχρεωτικό πεδίο. Αυτό το πετύχαμε με τη μέθοδο `setRequired()`. Τέλος, προσθέσαμε επίσης το [event |nette:glossary#Events] `onSuccess`, το οποίο ενεργοποιείται εάν η φόρμα υποβληθεί επιτυχώς. Στην περίπτωσή μας, καλεί τη μέθοδο `contactFormSucceeded`, η οποία θα αναλάβει την επεξεργασία της υποβληθείσας φόρμας. Αυτό θα το συμπληρώσουμε στον κώδικα σε μια στιγμή. - -Το component `contactForm` θα το αφήσουμε να αποδοθεί στο template `Home/default.latte`: - -```latte -{block content} -<h1>Φόρμα επικοινωνίας</h1> -{control contactForm} -``` - -Για την ίδια την αποστολή του email θα δημιουργήσουμε μια νέα κλάση, την οποία θα ονομάσουμε `ContactFacade` και θα την τοποθετήσουμε στο αρχείο `app/Model/ContactFacade.php`: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // το email σας - ->setFrom($email, $name) - ->setSubject('Μήνυμα από τη φόρμα επικοινωνίας') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Η μέθοδος `sendMessage()` δημιουργεί και στέλνει το email. Χρησιμοποιεί για αυτό τον λεγόμενο mailer, τον οποίο λαμβάνει ως εξάρτηση μέσω του constructor. Διαβάστε περισσότερα για την [αποστολή emails |mail:]. - -Τώρα θα επιστρέψουμε στον presenter και θα ολοκληρώσουμε τη μέθοδο `contactFormSucceeded()`. Αυτή θα καλέσει τη μέθοδο `sendMessage()` της κλάσης `ContactFacade` και θα της παραδώσει τα δεδομένα από τη φόρμα. Και πώς θα αποκτήσουμε το αντικείμενο `ContactFacade`; Θα το ζητήσουμε να μας παραδοθεί μέσω του constructor: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Το μήνυμα στάλθηκε'); - $this->redirect('this'); - } -} -``` - -Αφού σταλεί το email, θα εμφανίσουμε επίσης στον χρήστη ένα λεγόμενο [flash message |application:components#Flash Μηνύματα], επιβεβαιώνοντας ότι το μήνυμα στάλθηκε, και στη συνέχεια θα ανακατευθύνουμε σε άλλη σελίδα, ώστε να μην είναι δυνατή η επανειλημμένη αποστολή της φόρμας μέσω *refresh* στον browser. - - -Λοιπόν, και αν όλα λειτουργούν, θα πρέπει να μπορείτε να στείλετε email από τη φόρμα επικοινωνίας σας. Συγχαρητήρια! - - -HTML template email -------------------- - -Μέχρι στιγμής, αποστέλλεται ένα απλό email κειμένου που περιέχει μόνο το μήνυμα που στάλθηκε από τη φόρμα. Στο email όμως μπορούμε να χρησιμοποιήσουμε HTML και να κάνουμε την εμφάνισή του πιο ελκυστική. Θα δημιουργήσουμε γι' αυτό ένα template στο Latte, το οποίο θα γράψουμε στο `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Μήνυμα από τη φόρμα επικοινωνίας - - -

    Όνομα: {$name}

    -

    E-mail: {$email}

    -

    Μήνυμα: {$message}

    - - -``` - -Μένει να τροποποιήσουμε το `ContactFacade`, ώστε να χρησιμοποιεί αυτό το template. Στον constructor θα ζητήσουμε την κλάση `LatteFactory`, η οποία μπορεί να δημιουργήσει ένα αντικείμενο `Latte\Engine`, δηλαδή τον [Latte template renderer |latte:develop#Πώς να Αποδώσετε ένα Πρότυπο]. Με τη μέθοδο `renderToString()` θα αποδώσουμε το template σε αρχείο, η πρώτη παράμετρος είναι η διαδρομή προς το template και η δεύτερη είναι οι μεταβλητές. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // το email σας - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Το παραγόμενο HTML email θα το παραδώσουμε στη συνέχεια στη μέθοδο `setHtmlBody()` αντί της αρχικής `setBody()`. Επίσης, δεν χρειάζεται να αναφέρουμε το θέμα του email στο `setSubject()`, επειδή η βιβλιοθήκη θα το πάρει από το στοιχείο `` του template. - - -Διαμόρφωση ----------- - -Στον κώδικα της κλάσης `ContactFacade` είναι ακόμα σκληρά κωδικοποιημένο το διαχειριστικό μας email `admin@example.com`. Θα ήταν καλύτερο να το μεταφέρουμε στο αρχείο διαμόρφωσης. Πώς να το κάνουμε αυτό; - -Πρώτα θα τροποποιήσουμε την κλάση `ContactFacade` και θα αντικαταστήσουμε το string με το email με μια μεταβλητή που παραδίδεται μέσω του constructor: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -Και το δεύτερο βήμα είναι η αναφορά της τιμής αυτής της μεταβλητής στη διαμόρφωση. Στο αρχείο `app/config/services.neon` γράφουμε: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -Και αυτό είναι όλο. Αν τα στοιχεία στην ενότητα `services` ήταν πολλά και είχατε την αίσθηση ότι το email χάνεται ανάμεσά τους, μπορούμε να το κάνουμε μεταβλητή. Τροποποιούμε την καταχώρηση σε: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -Και στο αρχείο `app/config/common.neon` ορίζουμε αυτή τη μεταβλητή: - -```neon -parameters: - adminEmail: admin@example.com -``` - -Και τελειώσαμε! diff --git a/best-practices/el/microsites.texy b/best-practices/el/microsites.texy deleted file mode 100644 index 2224119362..0000000000 --- a/best-practices/el/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Πώς να γράφετε μικρο-ιστοσελίδες -******************************** - -Φανταστείτε ότι χρειάζεστε να δημιουργήσετε γρήγορα μια μικρή ιστοσελίδα για την επερχόμενη εκδήλωση της εταιρείας σας. Πρέπει να είναι απλό, γρήγορο και χωρίς περιττές πολυπλοκότητες. Ίσως σκέφτεστε ότι για ένα τόσο μικρό έργο δεν χρειάζεστε ένα στιβαρό framework. Αλλά τι γίνεται αν η χρήση του Nette framework μπορεί να απλοποιήσει και να επιταχύνει θεμελιωδώς αυτή τη διαδικασία; - -Ακόμα και κατά τη δημιουργία απλών ιστοσελίδων, δεν θέλετε να εγκαταλείψετε την άνεση. Δεν θέλετε να εφευρίσκετε αυτό που έχει ήδη λυθεί μία φορά. Μείνετε ήσυχα τεμπέλης και αφήστε τον εαυτό σας να κακομάθει. Το Nette Framework μπορεί να χρησιμοποιηθεί εξαιρετικά και ως micro framework. - -Πώς μπορεί να μοιάζει ένα τέτοιο microsite; Για παράδειγμα, έτσι ώστε ολόκληρος ο κώδικας της ιστοσελίδας να τοποθετηθεί σε ένα μόνο αρχείο `index.php` στον δημόσιο φάκελο: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// δημιουργία DI container βάσει της διαμόρφωσης στο config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// ορίζουμε το routing -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// route για το URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // ανιχνεύουμε τη γλώσσα του browser και ανακατευθύνουμε στο URL /en ή /de κ.λπ. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// route για το URL https://example.com/cs ή https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // εμφανίζουμε το αντίστοιχο template, για παράδειγμα ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// εκκίνηση της εφαρμογής! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Όλα τα υπόλοιπα θα είναι templates αποθηκευμένα στον γονικό φάκελο `/templates`. - -Ο κώδικας PHP στο `index.php` πρώτα [προετοιμάζει το περιβάλλον |bootstrap:], στη συνέχεια ορίζει τις [routes |application:routing#Δυναμική δρομολόγηση με callbacks] και τέλος εκκινεί την εφαρμογή. Το πλεονέκτημα είναι ότι η δεύτερη παράμετρος της συνάρτησης `addRoute()` μπορεί να είναι ένα callable, το οποίο εκτελείται μετά το άνοιγμα της αντίστοιχης σελίδας. - - -Γιατί να χρησιμοποιήσετε το Nette για microsite; ------------------------------------------------- - -- Οι προγραμματιστές που έχουν δοκιμάσει ποτέ το [Tracy |tracy:] δεν μπορούν σήμερα να φανταστούν ότι θα προγραμματίσουν κάτι χωρίς αυτό. -- Πάνω απ' όλα, όμως, θα χρησιμοποιήσετε το σύστημα προτύπων [Latte |latte:], επειδή ήδη από 2 σελίδες θα θέλετε να έχετε ξεχωριστή [διάταξη και περιεχόμενο |latte:template-inheritance]. -- Και σίγουρα θέλετε να βασιστείτε στο [αυτόματο escaping |latte:safety-first], ώστε να μην προκύψει ευπάθεια XSS -- Το Nette επίσης εξασφαλίζει ότι σε περίπτωση σφάλματος δεν θα εμφανιστούν ποτέ τα μηνύματα σφαλμάτων PHP για προγραμματιστές, αλλά μια κατανοητή σελίδα για τον χρήστη. -- Αν θέλετε να λαμβάνετε ανατροφοδότηση από τους χρήστες, για παράδειγμα με τη μορφή μιας φόρμας επικοινωνίας, τότε θα προσθέσετε επίσης [φόρμες |forms:] και [βάση δεδομένων |database:]. -- Μπορείτε επίσης εύκολα να [στείλετε μέσω email |mail:] τις συμπληρωμένες φόρμες. -- Μερικές φορές μπορεί να σας φανεί χρήσιμο το [caching |caching:], για παράδειγμα αν κατεβάζετε και εμφανίζετε feeds. - -Στη σημερινή εποχή, όπου η ταχύτητα και η αποτελεσματικότητα είναι καθοριστικής σημασίας, είναι σημαντικό να έχετε εργαλεία που σας επιτρέπουν να επιτύχετε αποτελέσματα χωρίς περιττές καθυστερήσεις. Το Nette framework σας προσφέρει ακριβώς αυτό - γρήγορη ανάπτυξη, ασφάλεια και ένα ευρύ φάσμα εργαλείων, όπως το Tracy και το Latte, που απλοποιούν τη διαδικασία. Αρκεί να εγκαταστήσετε μερικά πακέτα Nette και η κατασκευή ενός τέτοιου microsite γίνεται ξαφνικά παιχνιδάκι. Και ξέρετε ότι πουθενά δεν κρύβεται καμία τρύπα ασφαλείας. diff --git a/best-practices/el/pagination.texy b/best-practices/el/pagination.texy deleted file mode 100644 index cdefb763e4..0000000000 --- a/best-practices/el/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Σελίδωση αποτελεσμάτων βάσης δεδομένων -************************************** - -.[perex] -Κατά τη δημιουργία web εφαρμογών, πολύ συχνά θα συναντήσετε την απαίτηση για περιορισμό του αριθμού των εμφανιζόμενων στοιχείων ανά σελίδα. - -Θα ξεκινήσουμε από την κατάσταση όπου εμφανίζουμε όλα τα δεδομένα χωρίς σελίδωση. Για την επιλογή δεδομένων από τη βάση δεδομένων έχουμε την κλάση ArticleRepository, η οποία εκτός από τον constructor περιέχει τη μέθοδο `findPublishedArticles`, η οποία επιστρέφει όλα τα δημοσιευμένα άρθρα ταξινομημένα φθίνοντα κατά ημερομηνία δημοσίευσης. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -Στον presenter, στη συνέχεια, κάνουμε inject την κλάση του μοντέλου και στη μέθοδο render ζητάμε τα δημοσιευμένα άρθρα, τα οποία περνάμε στο template: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -Στο template `default.latte` φροντίζουμε στη συνέχεια για την εμφάνιση των άρθρων: - -```latte -{block content} -<h1>Άρθρα</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Με αυτόν τον τρόπο μπορούμε να εμφανίσουμε όλα τα άρθρα, πράγμα που όμως αρχίζει να δημιουργεί προβλήματα τη στιγμή που ο αριθμός των άρθρων αυξάνεται. Σε εκείνη τη στιγμή, έρχεται βολική η υλοποίηση ενός μηχανισμού σελίδωσης. - -Αυτός εξασφαλίζει ότι όλα τα άρθρα θα χωριστούν σε αρκετές σελίδες και εμείς θα εμφανίσουμε μόνο τα άρθρα μιας τρέχουσας σελίδας. Τον συνολικό αριθμό σελίδων και τη διαίρεση των άρθρων θα τον υπολογίσει ο [Paginator |utils:Paginator] μόνος του ανάλογα με το πόσα άρθρα έχουμε συνολικά και πόσα άρθρα ανά σελίδα θέλουμε να εμφανίσουμε. - -Στο πρώτο βήμα, θα τροποποιήσουμε τη μέθοδο για την απόκτηση άρθρων στην κλάση του repository έτσι ώστε να μπορεί να μας επιστρέφει μόνο άρθρα για μία σελίδα. Θα προσθέσουμε επίσης μια μέθοδο για τη διαπίστωση του συνολικού αριθμού άρθρων στη βάση δεδομένων, την οποία θα χρειαστούμε για τη ρύθμιση του Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $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, - ); - } - - /** - * Επιστρέφει τον συνολικό αριθμό δημοσιευμένων άρθρων - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Στη συνέχεια, θα προχωρήσουμε στις τροποποιήσεις του presenter. Στη μέθοδο render θα περνάμε τον αριθμό της τρέχουσας εμφανιζόμενης σελίδας. Για την περίπτωση που αυτός ο αριθμός δεν θα είναι μέρος του URL, θα ορίσουμε την προεπιλεγμένη τιμή της πρώτης σελίδας. - -Επίσης, θα επεκτείνουμε τη μέθοδο render με την απόκτηση της παρουσίας του Paginator, τη ρύθμισή του και την επιλογή των σωστών άρθρων για εμφάνιση στο template. Ο HomePresenter μετά τις τροποποιήσεις θα μοιάζει ως εξής: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Θα διαπιστώσουμε τον συνολικό αριθμό δημοσιευμένων άρθρων - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Θα δημιουργήσουμε μια παρουσία του Paginator και θα τον ρυθμίσουμε - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // συνολικός αριθμός άρθρων - $paginator->setItemsPerPage(10); // αριθμός στοιχείων ανά σελίδα - $paginator->setPage($page); // αριθμός τρέχουσας σελίδας - - // Από τη βάση δεδομένων θα τραβήξουμε ένα περιορισμένο σύνολο άρθρων σύμφωνα με τον υπολογισμό του Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // το οποίο θα περάσουμε στο template - $this->template->articles = $articles; - // και επίσης τον ίδιο τον Paginator για την εμφάνιση των επιλογών σελίδωσης - $this->template->paginator = $paginator; - } -} -``` - -Το template μας ήδη τώρα επαναλαμβάνεται μόνο πάνω στα άρθρα μιας σελίδας, αρκεί να προσθέσουμε τους συνδέσμους σελίδωσης: - -```latte -{block content} -<h1>Άρθρα</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Πρώτη</a> -  |  - <a n:href="default, $paginator->page-1">Προηγούμενη</a> -  |  - {/if} - - Σελίδα {$paginator->getPage()} από {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Επόμενη</a> -  |  - <a n:href="default, $paginator->getPageCount()">Τελευταία</a> - {/if} -</div> -``` - - -Έτσι συμπληρώσαμε τη σελίδα με τη δυνατότητα σελίδωσης χρησιμοποιώντας τον Paginator. Στην περίπτωση που αντί του [Nette Database Core |database:sql-way] ως επίπεδο βάσης δεδομένων χρησιμοποιήσουμε το [Nette Database Explorer |database:explorer], είμαστε σε θέση να υλοποιήσουμε τη σελίδωση και χωρίς τη χρήση του Paginator. Η κλάση `Nette\Database\Table\Selection` περιέχει τη μέθοδο [page |api:Nette\Database\Table\Selection::_page] με τη λογική σελίδωσης που έχει ληφθεί από τον Paginator. - -Το repository με αυτόν τον τρόπο υλοποίησης θα μοιάζει ως εξής: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -Στον presenter δεν χρειάζεται να δημιουργήσουμε Paginator, θα χρησιμοποιήσουμε αντί γι' αυτόν τη μέθοδο της κλάσης `Selection`, την οποία μας επιστρέφει το repository: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Θα τραβήξουμε τα δημοσιευμένα άρθρα - $articles = $this->articleRepository->findPublishedArticles(); - - // και στο template θα στείλουμε μόνο το μέρος τους που περιορίζεται σύμφωνα με τον υπολογισμό της μεθόδου page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // και επίσης τα απαραίτητα δεδομένα για την εμφάνιση των επιλογών σελίδωσης - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Επειδή στο template τώρα δεν στέλνουμε τον Paginator, θα τροποποιήσουμε το μέρος που εμφανίζει τους συνδέσμους σελίδωσης: - -```latte -{block content} -<h1>Άρθρα</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Πρώτη</a> -  |  - <a n:href="default, $page - 1">Προηγούμενη</a> -  |  - {/if} - - Σελίδα {$page} από {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Επόμενη</a> -  |  - <a n:href="default, $lastPage">Τελευταία</a> - {/if} -</div> -``` - -Με αυτόν τον τρόπο υλοποιήσαμε τον μηχανισμό σελίδωσης χωρίς τη χρήση του Paginator. - -{{priority: -1}} diff --git a/best-practices/el/passing-settings-to-presenters.texy b/best-practices/el/passing-settings-to-presenters.texy deleted file mode 100644 index f600bbb498..0000000000 --- a/best-practices/el/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Πέρασμα ρυθμίσεων στους presenters -********************************** - -.[perex] -Χρειάζεστε να περάσετε ορίσματα στους presenters που δεν είναι αντικείμενα (π.χ. πληροφορία αν τρέχουν σε debug mode, διαδρομές προς καταλόγους κ.λπ.), και επομένως δεν μπορούν να περαστούν αυτόματα μέσω autowiring; Η λύση είναι να τα ενσωματώσετε σε ένα αντικείμενο `Settings`. - -Η υπηρεσία `Settings` αποτελεί έναν πολύ εύκολο και ταυτόχρονα χρήσιμο τρόπο παροχής πληροφοριών σχετικά με την τρέχουσα εφαρμογή στους presenters. Η συγκεκριμένη της μορφή εξαρτάται αποκλειστικά από τις δικές σας συγκεκριμένες ανάγκες. Παράδειγμα: - -```php -namespace App; - -class Settings -{ - public function __construct( - // από την PHP 8.1 είναι δυνατό να δηλωθεί readonly - public bool $debugMode, - public string $appDir, - // και ούτω καθεξής - ) {} -} -``` - -Παράδειγμα καταχώρησης στη διαμόρφωση: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Όταν ο presenter χρειαστεί τις πληροφορίες που παρέχονται από αυτή την υπηρεσία, απλά θα τη ζητήσει στον constructor: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/el/post-links.texy b/best-practices/el/post-links.texy deleted file mode 100644 index a6147e263a..0000000000 --- a/best-practices/el/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Πώς να χρησιμοποιείτε σωστά τους συνδέσμους POST -************************************************ - -.[perex] -Σε web εφαρμογές, ειδικά σε διαχειριστικά interfaces, θα έπρεπε να είναι βασικός κανόνας ότι οι ενέργειες που αλλάζουν την κατάσταση του server δεν θα έπρεπε να εκτελούνται μέσω της μεθόδου HTTP GET. Όπως υποδηλώνει το όνομα της μεθόδου, η GET θα έπρεπε να χρησιμεύει μόνο για τη λήψη δεδομένων, όχι για την αλλαγή τους. Για ενέργειες όπως η διαγραφή εγγραφών, είναι προτιμότερη η χρήση της μεθόδου POST. Αν και η ιδανική θα ήταν η μέθοδος DELETE, αλλά αυτή δεν μπορεί να κληθεί χωρίς JavaScript, γι' αυτό ιστορικά χρησιμοποιείται η POST. - -Πώς να το κάνετε στην πράξη; Χρησιμοποιήστε αυτό το απλό κόλπο. Στην αρχή του template, δημιουργήστε μια βοηθητική φόρμα με το αναγνωριστικό `postForm`, την οποία στη συνέχεια θα χρησιμοποιήσετε για τα κουμπιά διαγραφής: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Χάρη σε αυτή τη φόρμα, μπορείτε αντί για τον κλασικό σύνδεσμο `<a>` να χρησιμοποιήσετε ένα κουμπί `<button>`, το οποίο μπορεί να διαμορφωθεί οπτικά ώστε να μοιάζει με συνηθισμένο σύνδεσμο. Για παράδειγμα, το CSS framework Bootstrap προσφέρει τις κλάσεις `btn btn-link` με τις οποίες επιτυγχάνετε το κουμπί να μην διαφέρει οπτικά από τους άλλους συνδέσμους. Με το attribute `form="postForm"` το συνδέουμε με την προετοιμασμένη φόρμα: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">delete</button> - <!-- instead of <a n:href="delete $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -Κατά το κλικ στον σύνδεσμο, καλείται τώρα η ενέργεια `delete`. Για να διασφαλίσετε ότι τα αιτήματα θα γίνονται δεκτά μόνο μέσω της μεθόδου POST και από τον ίδιο τομέα (που είναι μια αποτελεσματική άμυνα κατά των επιθέσεων CSRF), χρησιμοποιήστε το attribute `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // υποθετικός κώδικας που διαγράφει την εγγραφή - $this->redirect('default'); - } -} -``` - -Το attribute υπάρχει από το Nette Application 3.2 και περισσότερα για τις δυνατότητές του θα μάθετε στη σελίδα [Πώς να χρησιμοποιήσετε το attribute #Requires |attribute-requires]. - -Αν αντί για την ενέργεια `actionDelete()` χρησιμοποιούσατε το σήμα `handleDelete()`, δεν είναι απαραίτητο να αναφέρετε `sameOrigin: true`, επειδή τα σήματα έχουν αυτή την προστασία ρυθμισμένη από προεπιλογή: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Αυτή η προσέγγιση όχι μόνο βελτιώνει την ασφάλεια της εφαρμογής σας, αλλά συμβάλλει επίσης στην τήρηση των σωστών web προτύπων και πρακτικών. Χρησιμοποιώντας τις μεθόδους POST για ενέργειες που αλλάζουν την κατάσταση, επιτυγχάνετε μια πιο στιβαρή και ασφαλή εφαρμογή. diff --git a/best-practices/el/presenter-traits.texy b/best-practices/el/presenter-traits.texy deleted file mode 100644 index b5c9e24f28..0000000000 --- a/best-practices/el/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Σύνθεση presenters από traits -***************************** - -.[perex] -Αν χρειαζόμαστε να υλοποιήσουμε τον ίδιο κώδικα σε περισσότερους presenters (π.χ. έλεγχος ότι ο χρήστης είναι συνδεδεμένος), προσφέρεται η τοποθέτηση του κώδικα σε έναν κοινό πρόγονο. Η δεύτερη δυνατότητα είναι η δημιουργία μονοσκοπικών [traits |nette:introduction-to-object-oriented-programming#Traits]. - -Το πλεονέκτημα αυτής της λύσης είναι ότι καθένας από τους presenters μπορεί να χρησιμοποιήσει ακριβώς τα traits που πραγματικά χρειάζεται, ενώ η πολλαπλή κληρονομικότητα δεν είναι δυνατή στην PHP. - -Αυτά τα traits μπορούν να εκμεταλλευτούν το γεγονός ότι κατά τη δημιουργία του presenter καλούνται διαδοχικά όλες οι [μέθοδοι inject |inject-method-attribute#Μέθοδοι inject]. Απλά πρέπει να διασφαλιστεί ότι το όνομα κάθε μεθόδου inject είναι μοναδικό. - -Τα traits μπορούν να επισυνάψουν κώδικα αρχικοποίησης στα events [onStartup ή onRender |application:presenters#Γεγονότα]. - -Παραδείγματα: - -```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 στη συνέχεια χρησιμοποιεί απλά αυτά τα traits: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/el/restore-request.texy b/best-practices/el/restore-request.texy deleted file mode 100644 index 631a0c432b..0000000000 --- a/best-practices/el/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Πώς να επιστρέψετε σε προηγούμενη σελίδα; -***************************************** - -.[perex] -Τι γίνεται αν ο χρήστης συμπληρώνει μια φόρμα και η σύνδεσή του λήξει; Για να μην χάσει τα δεδομένα, πριν την ανακατεύθυνση στη σελίδα σύνδεσης, αποθηκεύουμε τα δεδομένα στο session. Στο Nette αυτό είναι παιχνιδάκι. - -Το τρέχον αίτημα μπορεί να αποθηκευτεί στο session χρησιμοποιώντας τη μέθοδο `storeRequest()`, η οποία επιστρέφει το αναγνωριστικό του με τη μορφή ενός σύντομου string. Η μέθοδος αποθηκεύει το όνομα του τρέχοντος presenter, την προβολή και τις παραμέτρους του. Σε περίπτωση που έχει υποβληθεί και φόρμα, αποθηκεύεται επίσης το περιεχόμενο των πεδίων (με εξαίρεση τα ανεβασμένα αρχεία). - -Η επαναφορά του αιτήματος γίνεται με τη μέθοδο `restoreRequest($key)`, στην οποία περνάμε το ληφθέν αναγνωριστικό. Αυτή ανακατευθύνει στον αρχικό presenter και προβολή. Αν όμως το αποθηκευμένο αίτημα περιέχει υποβολή φόρμας, μεταβαίνει στον αρχικό presenter με τη μέθοδο `forward()`, παραδίδει στη φόρμα τις προηγουμένως συμπληρωμένες τιμές και την αφήνει να αποδοθεί ξανά. Ο χρήστης έτσι έχει τη δυνατότητα να υποβάλει ξανά τη φόρμα και κανένα δεδομένο δεν χάνεται. - -Σημαντικό είναι ότι το `restoreRequest()` ελέγχει αν ο νέος συνδεδεμένος χρήστης είναι ο ίδιος που συμπλήρωσε αρχικά τη φόρμα. Αν όχι, απορρίπτει το αίτημα και δεν κάνει τίποτα. - -Θα δείξουμε τα πάντα με ένα παράδειγμα. Έστω ότι έχουμε έναν presenter `AdminPresenter`, στον οποίο επεξεργαζόμαστε δεδομένα και στη μέθοδο `startup()` του οποίου ελέγχουμε αν ο χρήστης είναι συνδεδεμένος. Αν δεν είναι, τον ανακατευθύνουμε στον `SignPresenter`. Ταυτόχρονα αποθηκεύουμε το τρέχον αίτημα και στέλνουμε το κλειδί του στον `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` θα περιέχει εκτός από τη φόρμα σύνδεσης και μια persistent παράμετρο `$backlink`, στην οποία θα γραφτεί το κλειδί. Επειδή η παράμετρος είναι persistent, θα μεταφέρεται και μετά την υποβολή της φόρμας σύνδεσης. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... προσθέτουμε πεδία φόρμας ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... εδώ συνδέουμε τον χρήστη ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Στη μέθοδο `restoreRequest()` περνάμε το κλειδί του αποθηκευμένου αιτήματος και αυτή ανακατευθύνει (ή μεταβαίνει) στον αρχικό presenter. - -Αν όμως το κλειδί είναι άκυρο (για παράδειγμα δεν υπάρχει πλέον στο session), η μέθοδος δεν κάνει τίποτα. Ακολουθεί επομένως η κλήση `$this->redirect('Admin:')`, η οποία ανακατευθύνει στον `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/hu/@home.texy b/best-practices/hu/@home.texy deleted file mode 100644 index e1ab1cabd8..0000000000 --- a/best-practices/hu/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Útmutatók és eljárások -********************** - -.[perex] -Útmutatók, gyakori feladatok megoldásai és *best practices* a Nette-hez. - - -<div class=documentation> -<div> - - -Nette Alkalmazások ------------------- -- [Inject metódusok és attribútumok |inject-method-attribute] -- [Presenterek összeállítása trait-ekből |presenter-traits] -- [Beállítások átadása presentereknek |passing-settings-to-presenters] -- [Hogyan térjünk vissza egy korábbi oldalra |restore-request] -- [Adatbázis eredmények lapozása |pagination] -- [Dinamikus snippettek |dynamic-snippets] -- [Hogyan használjuk a #Requires attribútumot |attribute-requires] -- [Hogyan használjuk helyesen a POST linkeket |post-links] - -</div> -<div> - - -Űrlapok -------- -- [Űrlapok újrafelhasználása |form-reuse] -- [Űrlap rekord létrehozásához és szerkesztéséhez |creating-editing-form] -- [Készítsünk kapcsolatfelvételi űrlapot |lets-create-contact-form] -- [Függő selectboxok |https://blog.nette.org/hu/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Általános ---------- -- [Hogyan töltsünk be egy konfigurációs fájlt |bootstrap:] -- [Hogyan írjunk mikro-weboldalakat |microsites] -- [Miért használja a Nette a PascalCase konstans jelölést? |https://blog.nette.org/hu/for-less-screaming-in-the-code] -- [Miért nem használja a Nette az Interface utótagot? |https://blog.nette.org/hu/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: használati tippek |composer] -- [Tippek szerkesztőkhöz & eszközökhöz |editors-and-tools] -- [Bevezetés az objektumorientált programozásba |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Példa megoldások ----------------- -- [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:] - -</div> -<div> - - -Videók ------- -Több száz felvétel a Poslední sobota eseményekről és Nette videók egy helyen a "Nette Framework Youtube csatornáján":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/hu/@meta.texy b/best-practices/hu/@meta.texy deleted file mode 100644 index 9a70856e97..0000000000 --- a/best-practices/hu/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Útmutatók és eljárások}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/hu/attribute-requires.texy b/best-practices/hu/attribute-requires.texy deleted file mode 100644 index 819f823943..0000000000 --- a/best-practices/hu/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Hogyan használjuk a `#[Requires]` attribútumot -********************************************** - -.[perex] -Amikor webalkalmazást ír, gyakran találkozik azzal az igénnyel, hogy korlátozza a hozzáférést az alkalmazás bizonyos részeihez. Talán azt szeretné, hogy bizonyos kérések csak űrlapon keresztül küldhessenek adatokat (azaz POST metódussal), vagy hogy csak AJAX hívások számára legyenek elérhetők. A Nette Framework 3.2-ben megjelent egy új eszköz, amely lehetővé teszi az ilyen korlátozások nagyon elegáns és áttekinthető beállítását: a `#[Requires]` attribútum. - -Az attribútum egy speciális jelölés a PHP-ban, amelyet az osztály vagy metódus definíciója elé adunk hozzá. Mivel valójában egy osztályról van szó, ahhoz, hogy a következő példák működjenek, meg kell adni a use klauzult: - -```php -use Nette\Application\Attributes\Requires; -``` - -A `#[Requires]` attribútumot használhatja magánál a presenter osztálynál és ezeknél a metódusoknál is: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Az utolsó két metódus a komponensekre is vonatkozik, tehát az attribútumot náluk is használhatja. - -Ha az attribútum által megadott feltételek nem teljesülnek, HTTP 4xx hiba váltódik ki. - - -HTTP metódusok --------------- - -Megadhatja, hogy mely HTTP metódusok (mint GET, POST stb.) engedélyezettek a hozzáféréshez. Például, ha csak űrlapküldéssel szeretné engedélyezni a hozzáférést, állítsa be: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Miért kellene POST-ot használnia GET helyett az állapotot megváltoztató akciókhoz, és hogyan tegye ezt? [Olvassa el az útmutatót |post-links]. - -Megadhat egy metódust vagy metódusok tömbjét. Speciális eset a `'*'` érték, amely minden metódust engedélyez, amit a presenterek [biztonsági okokból |application:presenters#HTTP metódus ellenőrzése] alapértelmezés szerint nem engednek meg. - - -AJAX hívás ----------- - -Ha azt szeretné, hogy a presenter vagy metódus csak AJAX kérések számára legyen elérhető, használja: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Azonos eredet -------------- - -A biztonság növelése érdekében megkövetelheti, hogy a kérés ugyanarról a domainről érkezzen. Ezzel megakadályozhatja a [CSRF sebezhetőséget |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -A `handle<Signal>()` metódusoknál az azonos domainről való hozzáférés automatikusan megkövetelt. Tehát ha fordítva, bármely domainről szeretné engedélyezni a hozzáférést, adja meg: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Hozzáférés forwardon keresztül ------------------------------- - -Néha hasznos korlátozni a presenterhez való hozzáférést úgy, hogy csak közvetve legyen elérhető, például a `forward()` vagy `switch()` metódus használatával egy másik presenterből. Így védik például az error-presentereket, hogy ne lehessen őket URL-ből meghívni: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -A gyakorlatban gyakran szükség van bizonyos view-k megjelölésére, amelyekhez csak a presenter logikája alapján lehet eljutni. Tehát ismét, hogy ne lehessen őket közvetlenül megnyitni: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Konkrét akciók --------------- - -Korlátozhatja azt is, hogy egy bizonyos kód, például egy komponens létrehozása, csak specifikus akciókhoz legyen elérhető a presenterben: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -Egyetlen akció esetén nem szükséges tömböt írni: `#[Requires(actions: 'default')]` - - -Saját attribútumok ------------------- - -Ha a `#[Requires]` attribútumot ismételten ugyanazzal a beállítással szeretné használni, létrehozhat saját attribútumot, amely örökli a `#[Requires]`-t, és az igényeknek megfelelően állítja be. - -Például a `#[SingleAction]` csak a `default` akción keresztül engedélyezi a hozzáférést: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Vagy a `#[RestMethods]` engedélyezi a hozzáférést az összes REST API-hoz használt HTTP metóduson keresztül: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Következtetés -------------- - -A `#[Requires]` attribútum nagy rugalmasságot és kontrollt ad Önnek afölött, hogyan érhetők el a weboldalai. Egyszerű, de erőteljes szabályok segítségével növelheti alkalmazása biztonságát és helyes működését. Mint láthatja, az attribútumok használata a Nette-ben nemcsak megkönnyítheti a munkáját, hanem biztonságosabbá is teheti. diff --git a/best-practices/hu/composer.texy b/best-practices/hu/composer.texy deleted file mode 100644 index 97d7faa75c..0000000000 --- a/best-practices/hu/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: tippek a használathoz -******************************* - -<div class=perex> - -A Composer egy eszköz a PHP függőségek kezelésére. Lehetővé teszi számunkra, hogy felsoroljuk azokat a könyvtárakat, amelyektől a projektünk függ, és telepíti és frissíti őket helyettünk. Megmutatjuk: - -- hogyan telepítsük a Composert -- használatát új vagy meglévő projektben - -</div> - - -Telepítés -========= - -A Composer egy futtatható `.phar` fájl, amelyet a következő módon tölthet le és telepíthet: - - -Windows -------- - -Használja a hivatalos telepítőt [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Csak 4 parancsra van szükség, amelyeket másoljon le [erről az oldalról |https://getcomposer.org/download/]. - -Továbbá, ha egy olyan mappába helyezi, amely a rendszer `PATH`-jában van, a Composer globálisan elérhetővé válik: - -```shell -$ mv ./composer.phar ~/bin/composer # vagy /usr/local/bin/composer -``` - - -Használat a projektben -====================== - -Ahhoz, hogy a projektünkben elkezdhessük használni a Composert, csak egy `composer.json` fájlra van szükségünk. Ez leírja a projektünk függőségeit, és tartalmazhat további metaadatokat is. Egy alap `composer.json` tehát így nézhet ki: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Itt azt mondjuk, hogy az alkalmazásunk (vagy könyvtárunk) megköveteli a `nette/database` csomagot (a csomag neve a szervezet nevéből és a projekt nevéből áll), és olyan verziót szeretne, amely megfelel a `^3.0` feltételnek (azaz a legújabb 3-as verziót). - -Tehát a projekt gyökerében van egy `composer.json` fájlunk, és elindítjuk a telepítést: - -```shell -composer update -``` - -A Composer letölti a Nette Database-t a `vendor/` mappába. Továbbá létrehoz egy `composer.lock` fájlt, amely információkat tartalmaz arról, hogy pontosan melyik verziójú könyvtárakat telepítette. - -A Composer generál egy `vendor/autoload.php` fájlt, amelyet egyszerűen includálhatunk, és elkezdhetjük használni a könyvtárakat bármilyen további munka nélkül: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Csomagok frissítése a legújabb verziókra -======================================== - -A használt könyvtárak frissítését a `composer.json`-ban definiált feltételek szerinti legújabb verziókra a `composer update` parancs végzi. Pl. a `"nette/database": "^3.0"` függőségnél a legújabb 3.x.x verziót telepíti, de a 4-es verziót már nem. - -A `composer.json` fájlban lévő feltételek frissítéséhez, például `"nette/database": "^4.1"`-re, hogy telepíthető legyen a legújabb verzió, használja a `composer require nette/database` parancsot. - -Az összes használt Nette csomag frissítéséhez mindet fel kellene sorolni a parancssorban, pl.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Ami nem praktikus. Használja ezért az egyszerű "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff szkriptet, amely ezt megteszi Ön helyett: - -```shell -php composer-frontline.php -``` - - -Új projekt létrehozása -====================== - -Új Nette projektet egyetlen paranccsal hozhat létre: - -```shell -composer create-project nette/web-project projekt-neve -``` - -A `projekt-neve` helyére illessze be a projekt könyvtárának nevét, és erősítse meg. A Composer letölti a `nette/web-project` repository-t a GitHubról, amely már tartalmazza a `composer.json` fájlt, és rögtön utána a Nette Frameworköt. Már csak a [jogosultságokat kell beállítani |nette:troubleshooting#Könyvtárjogosultságok beállítása] a `temp/` és `log/` mappákra való íráshoz, és a projektnek életre kell kelnie. - -Ha tudja, milyen PHP verzióval fog futni a projekt a hostingen, ne felejtse el [beállítani |#PHP verzió]. - - -PHP verzió -========== - -A Composer mindig azokat a csomagverziókat telepíti, amelyek kompatibilisek az Ön által éppen használt PHP verzióval (pontosabban a parancssorban a Composer futtatásakor használt PHP verzióval). Ami azonban valószínűleg nem ugyanaz a verzió, mint amit a hostingja használ. Ezért nagyon fontos, hogy a `composer.json` fájlba hozzáadja az információt a hostingen lévő PHP verzióról. Ezután csak a hostinggal kompatibilis csomagverziók kerülnek telepítésre. - -Azt, hogy a projekt például PHP 8.2.3-on fog futni, a következő paranccsal állítjuk be: - -```shell -composer config platform.php 8.2.3 -``` - -Így a verzió beíródik a `composer.json` fájlba: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Azonban a PHP verziószám a fájl egy másik helyén is szerepel, mégpedig a `require` szekcióban. Míg az első szám azt határozza meg, hogy melyik verzióhoz települjenek a csomagok, a második szám azt mondja meg, hogy melyik verzióhoz íródott maga az alkalmazás. És például a PhpStorm ez alapján állítja be a *PHP language level*-t. (Természetesen nincs értelme, hogy ezek a verziók eltérjenek, tehát a kettős beírás egy átgondolatlanság.) Ezt a verziót a következő paranccsal állíthatja be: - -```shell -composer require php 8.2.3 --no-update -``` - -Vagy közvetlenül a `composer.json` fájlban: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -PHP verzió figyelmen kívül hagyása -================================== - -A csomagok általában megadják mind a legalacsonyabb PHP verziót, amellyel kompatibilisek, mind a legmagasabbat, amellyel tesztelve vannak. Ha még újabb PHP verziót tervez használni, például tesztelés céljából, a Composer megtagadja az ilyen csomag telepítését. A megoldás az `--ignore-platform-req=php+` opció, amely miatt a Composer figyelmen kívül hagyja a megkövetelt PHP verzió felső határait. - - -Hamis jelentések -================ - -Csomagok frissítésekor vagy verziószámok változásakor előfordul, hogy konfliktus lép fel. Egy csomag olyan követelményekkel rendelkezik, amelyek ellentmondanak egy másiknak, és így tovább. A Composer azonban néha hamis jelentést ad. Olyan konfliktust jelez, amely valójában nem létezik. Ilyen esetben segít a `composer.lock` fájl törlése és az újrapróbálkozás. - -Ha a hibaüzenet továbbra is fennáll, akkor komolyan kell venni, és ki kell olvasni belőle, mit és hogyan kell módosítani. - - -Packagist.org - központi repository -=================================== - -A [Packagist |https://packagist.org] a fő repository, amelyben a Composer megpróbálja megkeresni a csomagokat, hacsak nem mondjuk neki másképp. Itt publikálhatunk saját csomagokat is. - - -Mi van, ha nem akarjuk használni a központi repository-t? ---------------------------------------------------------- - -Ha belső vállalati alkalmazásaink vannak, amelyeket egyszerűen nem hostolhatunk nyilvánosan, akkor létrehozunk hozzájuk egy vállalati repository-t. - -Több információ a repository-król [a hivatalos dokumentációban |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -A Composer alapvető tulajdonsága, hogy autoloadingot biztosít az összes általa telepített osztályhoz, amelyet a `vendor/autoload.php` fájl includálásával indíthat el. - -Azonban a Composert lehet használni további osztályok betöltésére is a `vendor` mappán kívül. Az első lehetőség az, hogy hagyjuk a Composert átkutatni a definiált mappákat és almappákat, megtalálni az összes osztályt, és bevenni őket az autoloaderbe. Ezt a `composer.json` `autoload > classmap` beállításával érhetjük el: - -```js -{ - "autoload": { - "classmap": [ - "src/", # beleveszi a src/ mappát és annak almappáit - ] - } -} -``` - -Ezután minden változáskor futtatni kell a `composer dumpautoload` parancsot, és hagyni kell az autoloading táblák újragenerálását. Ez rendkívül kényelmetlen, és sokkal jobb ezt a feladatot a [RobotLoaderra|robot-loader:] bízni, amely ugyanazt a tevékenységet automatikusan a háttérben és sokkal gyorsabban végzi. - -A második lehetőség a [PSR-4|https://www.php-fig.org/psr/psr-4/] betartása. Egyszerűsítve ez egy olyan rendszer, ahol a névterek és osztálynevek megfelelnek a könyvtárstruktúrának és a fájlneveknek, tehát pl. az `App\Core\RouterFactory` az `/path/to/App/Core/RouterFactory.php` fájlban lesz. Példa konfiguráció: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # az App\ névtér az app/ könyvtárban van - } - } -} -``` - -Hogyan konfigurálja pontosan a viselkedést, megtudhatja a [Composer dokumentációjában|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Új verziók tesztelése -===================== - -Szeretné tesztelni egy csomag új fejlesztői verzióját. Hogyan tegye? Először adja hozzá ezt a két opciót a `composer.json` fájlhoz, amely lehetővé teszi a fejlesztői verziójú csomagok telepítését, de csak akkor folyamodik ehhez, ha nincs olyan stabil verziókombináció, amely megfelelne a követelményeknek: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Továbbá javasoljuk a `composer.lock` fájl törlését, néha ugyanis a Composer érthetetlen módon megtagadja a telepítést, és ez megoldja a problémát. - -Tegyük fel, hogy a `nette/utils` csomagról van szó, és az új verzió száma 4.0. Telepítse a következő paranccsal: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Vagy telepíthet konkrét verziót is, például 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Ha azonban a könyvtártól egy másik csomag függ, amely egy régebbi verzióra van zárolva (pl. `^3.1`), akkor ideális a csomagot frissíteni, hogy az új verzióval működjön. Ha azonban csak meg akarja kerülni a korlátozást, és rávenni a Composert, hogy telepítse a fejlesztői verziót, és úgy tegyen, mintha egy régebbi verzió lenne (pl. 3.1.6), használhatja az `as` kulcsszót: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Parancsok hívása -================ - -A Composer segítségével saját előre elkészített parancsokat és szkripteket hívhat meg, mintha natív Composer parancsok lennének. A `vendor/bin` mappában található szkriptek esetében nem kell ezt a mappát megadni. - -Példaként definiálunk a `composer.json` fájlban egy szkriptet, amely a [Nette Testerrel|tester:] futtatja a teszteket: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -A teszteket ezután a `composer tester` segítségével futtatjuk. A parancsot akkor is meghívhatjuk, ha nem a projekt gyökérkönyvtárában vagyunk, hanem valamelyik alkönyvtárban. - - -Küldjön köszönetet -================== - -Mutatunk egy trükköt, amellyel örömet szerezhet az open source szerzőknek. Egyszerű módon adhat csillagot a GitHubon azoknak a könyvtáraknak, amelyeket a projektje használ. Csak telepíteni kell a `symfony/thanks` könyvtárat: - -```shell -composer global require symfony/thanks -``` - -Majd futtatni: - -```shell -composer thanks -``` - -Próbálja ki! - - -Konfiguráció -============ - -A Composer szorosan kapcsolódik a [Git |https://git-scm.com] verziókezelő eszközhöz. Ha nincs telepítve, szólni kell a Composernek, hogy ne használja: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/hu/creating-editing-form.texy b/best-practices/hu/creating-editing-form.texy deleted file mode 100644 index 959ed13a7b..0000000000 --- a/best-practices/hu/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Űrlap rekord létrehozásához és szerkesztéséhez -********************************************** - -.[perex] -Hogyan implementáljuk helyesen a Nette-ben egy rekord hozzáadását és szerkesztését úgy, hogy mindkettőhöz ugyanazt az űrlapot használjuk? - -Sok esetben a rekord hozzáadására és szerkesztésére szolgáló űrlapok ugyanazok, legfeljebb a gomb felirata különbözik. Egyszerű presenterek példáit mutatjuk be, ahol az űrlapot először rekord hozzáadására, majd szerkesztésére használjuk, végül pedig egyesítjük a két megoldást. - - -Rekord hozzáadása ------------------ - -Példa egy presenter-re, amely rekord hozzáadására szolgál. Magát az adatbázis-kezelést a `Facade` osztályra bízzuk, amelynek kódja a példa szempontjából nem lényeges. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... hozzáadjuk az űrlap mezőit ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // rekord hozzáadása az adatbázishoz - $this->flashMessage('Sikeresen hozzáadva'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Rekord szerkesztése -------------------- - -Most megmutatjuk, hogyan nézne ki egy presenter, amely rekord szerkesztésére szolgál: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // rekord létezésének ellenőrzése - || !$this->facade->isEditAllowed(/*...*/) // jogosultság ellenőrzése - ) { - $this->error(); // 404 hiba - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // ellenőrizzük, hogy az akció 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... hozzáadjuk az űrlap mezőit ... - - $form->setDefaults($this->record); // alapértelmezett értékek beállítása - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // rekord frissítése - $this->flashMessage('Sikeresen frissítve'); - $this->redirect('...'); - } -} -``` - -Az *action* metódusban, amely rögtön a [presenter életciklusának |application:presenters#Presenter életciklusa] elején fut le, ellenőrizzük a rekord létezését és a felhasználó jogosultságát annak szerkesztésére. - -A rekordot a `$record` property-be mentjük, hogy elérhető legyen a `createComponentRecordForm()` metódusban az alapértelmezett értékek beállításához, és a `recordFormSucceeded()` metódusban az ID miatt. Alternatív megoldásként beállíthatnánk az alapértelmezett értékeket közvetlenül az `actionEdit()` metódusban, és az URL részét képező ID értékét a `getParameter('id')` segítségével szerezhetnénk meg: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // létezés ellenőrzése és jogosultság ellenőrzése - ) { - $this->error(); - } - - // űrlap alapértelmezett értékeinek beállítása - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Azonban, és ez kellene, hogy **az egész kód legfontosabb tanulsága** legyen, az űrlap létrehozásakor meg kell győződnünk arról, hogy az akció valóban `edit`. Mert különben az `actionEdit()` metódusban lévő ellenőrzés egyáltalán nem futna le! - - -Ugyanaz az űrlap hozzáadáshoz és szerkesztéshez ------------------------------------------------ - -És most egyesítjük a két presentert egybe. Vagy megkülönböztethetnénk a `createComponentRecordForm()` metódusban, hogy melyik akcióról van szó, és ennek megfelelően konfigurálhatnánk az űrlapot, vagy ezt közvetlenül az action-metódusokra bízhatnánk, és megszabadulhatnánk a feltételtől: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private 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 // rekord létezésének ellenőrzése - || !$this->facade->isEditAllowed(/*...*/) // jogosultság ellenőrzése - ) { - $this->error(); // 404 hiba - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // alapértelmezett értékek beállítása - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // ellenőrizzük, hogy az akció 'add' vagy 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... hozzáadjuk az űrlap mezőit ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // rekord hozzáadása az adatbázishoz - $this->flashMessage('Sikeresen hozzáadva'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // rekord frissítése - $this->flashMessage('Sikeresen frissítve'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/hu/dynamic-snippets.texy b/best-practices/hu/dynamic-snippets.texy deleted file mode 100644 index b6df024d32..0000000000 --- a/best-practices/hu/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Dinamikus Snippetek -******************* - -Az alkalmazásfejlesztés során meglehetősen gyakran felmerül az igény AJAX műveletek végrehajtására, például táblázatok egyes sorain vagy listaelemeken. Példaként választhatjuk a cikkek listázását, ahol minden cikknél lehetővé tesszük a bejelentkezett felhasználó számára, hogy "tetszik/nem tetszik" értékelést adjon. A presenter és a hozzá tartozó sablon kódja AJAX nélkül körülbelül így fog kinézni (a legfontosabb részeket mutatom be, a kód számol az értékelések jelölésére szolgáló szolgáltatás létezésével és a cikkek gyűjteményének megszerzésével - a konkrét implementáció nem fontos ennek az útmutatónak a céljaihoz): - -```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'); -} -``` - -Sablon: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>tetszik</a> - {else} - <a n:href="unlike! $article->id" class=ajax>már nem tetszik</a> - {/if} -</article> -``` - - -Ajaxizálás -========== - -Most lássuk el ezt az egyszerű alkalmazást AJAX-szal. A cikk értékelésének megváltoztatása nem annyira fontos, hogy átirányításra legyen szükség, ezért ideális esetben AJAX-szal kellene történnie a háttérben. Használjuk [a kiegészítők kiszolgáló szkriptjét |application:ajax#Naja] a szokásos konvencióval, miszerint az AJAX linkeknek `ajax` CSS osztályuk van. - -De hogyan is csináljuk ezt konkrétan? A Nette 2 utat kínál: az ún. dinamikus snippetek útját és a komponensek útját. Mindkettőnek megvannak az előnyei és hátrányai, ezért egyenként bemutatjuk őket. - - -A dinamikus snippetek útja -========================== - -A dinamikus snippet a Latte terminológiájában a `{snippet}` tag egy speciális használati esetét jelenti, amikor a snippet nevében egy változó szerepel. Egy ilyen snippet nem lehet bárhol a sablonban - egy statikus snippetbe, azaz egy közönséges snippetbe vagy egy `{snippetArea}`-ba kell csomagolni. A sablonunkat a következőképpen módosíthatnánk. - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>tetszik</a> - {else} - <a n:href="unlike! $article->id" class=ajax>már nem tetszik</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Most minden cikk definiál egy snippetet, amelynek nevében a cikk ID-ja szerepel. Mindezeket a snippeket aztán egyetlen, `articlesContainer` nevű snippetbe csomagoljuk. Ha ezt a csomagoló snippetet kihagynánk, a Latte kivétellel figyelmeztetne minket. - -Már csak a presenterben kell kiegészítenünk az újrarajzolást - elég a statikus burkolót újrarajzolni. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- nem szükséges - } else { - $this->redirect('this'); - } -} -``` - -Hasonlóképpen módosítjuk a testvér `handleUnlike()` metódust is, és az AJAX működik! - -A megoldásnak azonban van egy árnyoldala. Ha jobban megvizsgálnánk, hogyan zajlik az AJAX kérés, rájönnénk, hogy bár kifelé az alkalmazás takarékosnak tűnik (csak egyetlen snippetet ad vissza az adott cikkhez), valójában a szerveren az összes snippetet kirajzolta. A kívánt snippetet a payloadba helyezte, a többit pedig eldobta (tehát teljesen feleslegesen szerezte be őket az adatbázisból is). - -Ahhoz, hogy ezt a folyamatot optimalizáljuk, ott kell beavatkoznunk, ahol a `$articles` gyűjteményt átadjuk a sablonnak (mondjuk a `renderDefault()` metódusban). Kihasználjuk azt a tényt, hogy a signálok feldolgozása a `render<Something>` metódusok előtt történik: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Most a signál feldolgozásakor a sablonba az összes cikket tartalmazó gyűjtemény helyett csak egy tömb kerül átadásra egyetlen cikkel - azzal, amelyet ki akarunk rajzolni és a payloadban elküldeni a böngészőnek. A `{foreach}` tehát csak egyszer fut le, és nem rajzolódnak ki felesleges snippettek. - - -A komponensek útja -================== - -Egy teljesen más megoldási mód elkerüli a dinamikus snippetteket. A trükk abban rejlik, hogy az egész logikát egy külön komponensbe helyezzük át - az értékelések megadásától kezdve nem a presenter fog gondoskodni, hanem egy dedikált `LikeControl`. Az osztály a következőképpen fog kinézni (ezen kívül tartalmazni fogja a `render`, `handleUnlike` stb. metódusokat is): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private 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'); - } - } -} -``` - -A komponens sablonja: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>tetszik</a> - {else} - <a n:href="unlike!" class=ajax>már nem tetszik</a> - {/if} -{/snippet} -``` - -Természetesen megváltozik a view sablonja, és a presenterbe be kell illesztenünk egy factory-t. Mivel a komponenst annyiszor hozzuk létre, ahány cikket lekérünk az adatbázisból, a "sokszorosításához" a [Multiplier |application:Multiplier] osztályt használjuk. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -A view sablonja a szükséges minimumra csökken (és teljesen mentes a snippettektől!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Majdnem készen vagyunk: az alkalmazás mostantól AJAX-osan fog működni. Itt is optimalizálnunk kell az alkalmazást, mert a Nette Database használata miatt a signál feldolgozásakor feleslegesen betöltődik az összes cikk az adatbázisból egy helyett. Előnye azonban, hogy nem kerülnek kirajzolásra, mert valóban csak a mi komponensünk renderelődik. - -{{priority: -1}} diff --git a/best-practices/hu/editors-and-tools.texy b/best-practices/hu/editors-and-tools.texy deleted file mode 100644 index 7104666da7..0000000000 --- a/best-practices/hu/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Szerkesztők és eszközök -*********************** - -.[perex] -Lehetsz ügyes programozó, de csak jó eszközökkel válsz mesterré. Ebben a fejezetben tippeket találsz fontos eszközökhöz, szerkesztőkhöz és bővítményekhez. - - -IDE szerkesztő -============== - -Határozottan javasoljuk, hogy a fejlesztéshez teljes értékű IDE-t használj, mint például a PhpStorm, NetBeans, VS Code, és ne csak egy PHP támogatással rendelkező szövegszerkesztőt. A különbség valóban alapvető. Nincs ok megelégedni egy egyszerű szerkesztővel, amely ugyan tudja színezni a szintaxist, de nem éri el egy csúcskategóriás IDE képességeit, amely pontosan súg, figyeli a hibákat, képes refaktorálni a kódot és sok minden mást. Néhány IDE fizetős, mások pedig ingyenesek. - -A **NetBeans IDE** beépített támogatással rendelkezik a Nette, Latte és NEON számára. - -**PhpStorm**: telepítsd ezeket a bővítményeket a `Settings > Plugins > Marketplace` menüpontban: -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: keresd meg a marketplace-en a "Nette Latte + Neon" bővítményt. - -Kapcsold össze a Tracy-t is a szerkesztővel. Amikor egy hibaoldal jelenik meg, rákattinthatsz a fájlnevekre, és azok megnyílnak a szerkesztőben a megfelelő sorra állított kurzorral. Olvasd el, [hogyan konfiguráld a rendszert |tracy:open-files-in-ide]. - - -PHPStan -======= - -A PHPStan egy eszköz, amely logikai hibákat tár fel a kódban, mielőtt futtatnád azt. - -Telepítsük a Composer segítségével: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Hozzunk létre egy konfigurációs fájlt a projektben `phpstan.neon` néven: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Majd futtassuk az elemzést az `app/` mappában lévő osztályokon: - -```shell -vendor/bin/phpstan analyse app -``` - -Kimerítő dokumentációt találsz közvetlenül a [PHPStan oldalán |https://phpstan.org]. - - -Code Checker -============ - -A [Code Checker|code-checker:] ellenőrzi és szükség esetén kijavítja a forráskódok néhány formai hibáját: - -- eltávolítja a [BOM |nette:glossary#BOM]-ot -- ellenőrzi a [Latte |latte:] sablonok érvényességét -- ellenőrzi a `.neon`, `.php` és `.json` fájlok érvényességét -- ellenőrzi a [vezérlőkarakterek |nette:glossary#Vezérlő karakterek] előfordulását -- ellenőrzi, hogy a fájl UTF-8 kódolású-e -- ellenőrzi a hibásan írt `/* @anotace */` (hiányzik a csillag) -- eltávolítja a záró `?>` PHP fájlokból -- eltávolítja a jobb oldali szóközöket és a felesleges sorokat a fájl végéről -- normalizálja a sorelválasztókat a rendszer alapértelmezettjére (ha megadja a `-l` opciót) - - -Composer -======== - -A [Composer |best-practices:composer] egy függőségkezelő eszköz PHP-hez. Lehetővé teszi számunkra, hogy tetszőlegesen összetett függőségeket deklaráljunk az egyes könyvtárakhoz, majd telepíti őket a projektünkbe. - - -Requirements Checker -==================== - -Ez egy eszköz volt, amely tesztelte a szerver futási környezetét, és tájékoztatott arról, hogy (és milyen mértékben) lehet használni a keretrendszert. Jelenleg a Nette minden olyan szerveren használható, amely rendelkezik a minimálisan szükséges PHP verzióval. diff --git a/best-practices/hu/form-reuse.texy b/best-practices/hu/form-reuse.texy deleted file mode 100644 index cbda52b931..0000000000 --- a/best-practices/hu/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Űrlapok újrafelhasználása több helyen -************************************* - -.[perex] -A Nette-ben több lehetőség is rendelkezésre áll ugyanazon űrlap több helyen történő használatára a kód duplikálása nélkül. Ebben a cikkben különböző megoldásokat mutatunk be, beleértve azokat is, amelyeket érdemes elkerülni. - - -Űrlap Factory -============= - -Az egyik alapvető megközelítés ugyanazon komponens több helyen történő használatára egy olyan metódus vagy osztály létrehozása, amely ezt a komponenst generálja, majd ennek a metódusnak a meghívása az alkalmazás különböző pontjain. Egy ilyen metódust vagy osztályt *factory*-nak nevezünk. Kérjük, ne keverje össze a *factory method* tervezési mintával, amely a factory-k specifikus felhasználási módját írja le, és nem kapcsolódik ehhez a témához. - -Példaként létrehozunk egy factory-t, amely egy szerkesztő űrlapot fog összeállítani: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - return $form; - } -} -``` - -Most már használhatja ezt a factory-t az alkalmazás különböző pontjain, például presenterekben vagy komponensekben. Ezt úgy teheti meg, hogy [függőségként kérjük |dependency-injection:passing-dependencies]. Először tehát regisztráljuk az osztályt a konfigurációs fájlban: - -```neon -services: - - FormFactory -``` - -Majd használjuk a presenterben: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // beküldött adatok feldolgozása - }; - return $form; - } -} -``` - -Az űrlap factory-t kibővítheti további metódusokkal más típusú űrlapok létrehozásához az alkalmazás igényei szerint. És természetesen hozzáadhatunk egy metódust is, amely létrehoz egy alap űrlapot elemek nélkül, és ezt a többi metódus fogja használni: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - return $form; - } -} -``` - -A `createForm()` metódus egyelőre nem csinál semmi hasznosat, de ez hamarosan megváltozik. - - -A Factory függőségei -==================== - -Idővel kiderül, hogy szükségünk van arra, hogy az űrlapok többnyelvűek legyenek. Ez azt jelenti, hogy minden űrlaphoz be kell állítanunk az úgynevezett [translator |forms:rendering#Fordítás]-t. Ebből a célból módosítjuk a `FormFactory` osztályt úgy, hogy a konstruktorban függőségként fogadja el a `Translator` objektumot, és átadjuk azt az űrlapnak: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Mivel a `createForm()` metódust a többi, specifikus űrlapokat létrehozó metódus is meghívja, elegendő a translatort csak ebben beállítani. És készen is vagyunk. Nincs szükség egyetlen presenter vagy komponens kódjának módosítására sem, ami nagyszerű. - - -Több Factory osztály -==================== - -Alternatív megoldásként létrehozhat több osztályt minden egyes űrlaphoz, amelyet használni szeretne az alkalmazásában. Ez a megközelítés növelheti a kód olvashatóságát és megkönnyítheti az űrlapok kezelését. Az eredeti `FormFactory`-t csak egy tiszta űrlap létrehozására hagyjuk meg alapkonfigurációval (például fordítási támogatással), és a szerkesztő űrlaphoz létrehozunk egy új `EditFormFactory` factory-t. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ kompozíció használata -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - return $form; - } -} -``` - -Nagyon fontos, hogy a `FormFactory` és az `EditFormFactory` osztályok közötti kapcsolat [kompozícióval |nette:introduction-to-object-oriented-programming#Kompozíció] valósuljon meg, nem pedig [objektum öröklődéssel |nette:introduction-to-object-oriented-programming#Öröklődés]: - -```php -// ⛔ ÍGY NE! IDE NEM VALÓ AZ ÖRÖKLŐDÉS -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - return $form; - } -} -``` - -Az öröklődés használata ebben az esetben teljesen kontraproduktív lenne. Nagyon gyorsan problémákba ütköznél. Például abban a pillanatban, amikor paramétereket szeretnél hozzáadni a `create()` metódushoz; a PHP hibát jelezne, hogy a szignatúrája eltér a szülőétől. Vagy amikor függőséget adnál át az `EditFormFactory` osztálynak a konstruktoron keresztül. Olyan helyzet állna elő, amelyet [constructor hell |dependency-injection:passing-dependencies#Constructor hell]-nek nevezünk. - -Általában jobb előnyben részesíteni a [kompozíciót az öröklődéssel szemben |dependency-injection:faq#Miért részesítjük előnyben a kompozíciót az öröklődéssel szemben]. - - -Űrlapkezelés -============ - -Az űrlapkezelő, amely a sikeres beküldés után hívódik meg, szintén lehet a factory osztály része. Úgy fog működni, hogy a beküldött adatokat átadja a modellnek feldolgozásra. Az esetleges hibákat [visszaadja |forms:validation#Hibák a feldolgozás során] az űrlapnak. A modellt a következő példában a `Facade` osztály képviseli: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // beküldött adatok feldolgozása - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Magát az átirányítást azonban a presenterre bízzuk. Az `onSuccess` eseményhez hozzáad egy további handlert, amely végrehajtja az átirányítást. Ennek köszönhetően az űrlapot különböző presenterekben lehet majd használni, és mindegyikben máshová lehet átirányítani. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('A rekord mentésre került'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Ez a megoldás kihasználja az űrlapok azon tulajdonságát, hogy ha az űrlapon vagy annak egy elemén meghívják az `addError()` metódust, akkor a további `onSuccess` handler már nem hívódik meg. - - -Öröklődés a Form osztályból -=========================== - -Az összeállított űrlapnak nem szabad az űrlap leszármazottjának lennie. Más szavakkal, ne használja ezt a megoldást: - -```php -// ⛔ ÍGY NE! IDE NEM VALÓ AZ ÖRÖKLŐDÉS -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $this->addSubmit('send', 'Küldés'); - $this->setTranslator($translator); - } -} -``` - -Az űrlap konstruktorban történő összeállítása helyett használjon factory-t. - -Fontos megérteni, hogy a `Form` osztály elsősorban egy eszköz az űrlap összeállítására, tehát egy *form builder*. Az összeállított űrlap pedig tekinthető annak termékének. Azonban a termék nem a builder specifikus esete, nincs közöttük *is a* kapcsolat, amely az öröklődés alapját képezi. - - -Komponens űrlappal -================== - -Egy teljesen más megközelítés egy olyan [komponens |application:components] létrehozását jelenti, amelynek része egy űrlap. Ez új lehetőségeket kínál, például az űrlap specifikus módon történő renderelését, mivel a komponensnek része egy sablon is. Vagy használhatunk signálokat AJAX kommunikációhoz és információk betöltéséhez az űrlapba, például súgáshoz stb. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Cím:'); - // itt adjuk hozzá a további űrlapmezőket - $form->addSubmit('send', 'Küldés'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // beküldött adatok feldolgozása - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // esemény kiváltása - $this->onSave($this, $data); - } -} -``` - -Még létrehozunk egy factory-t, amely ezt a komponenst fogja gyártani. Elég [felírni az interfészét |application:components#Komponensek függőségekkel]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -És hozzáadjuk a konfigurációs fájlhoz: - -```neon -services: - - EditControlFactory -``` - -És most már kérhetjük a factory-t és használhatjuk a presenterben: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // vagy átirányítunk a szerkesztés eredményére, pl.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/hu/inject-method-attribute.texy b/best-practices/hu/inject-method-attribute.texy deleted file mode 100644 index f36ead2d52..0000000000 --- a/best-practices/hu/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Inject metódusok és attribútumok -******************************** - -.[perex] -Ebben a cikkben a függőségek Nette keretrendszerbeli presenterekbe történő átadásának különböző módjaira összpontosítunk. Összehasonlítjuk az előnyben részesített módszert, amely a konstruktor, más lehetőségekkel, mint például az `inject` metódusok és attribútumok. - -A presenterekre is igaz, hogy a függőségek [konstruktoron |dependency-injection:passing-dependencies#Konstruktoron keresztüli átadás] keresztüli átadása az előnyben részesített út. Ha azonban létrehozol egy közös őst, amelyből a többi presenter öröklődik (pl. `BasePresenter`), és ennek az ősnek is vannak függőségei, akkor egy problémába ütközünk, amelyet [constructor hell |dependency-injection:passing-dependencies#Constructor hell]-nek nevezünk. Ezt meg lehet kerülni alternatív utakkal, amelyeket az `inject` metódusok és attribútumok (korábban annotációk) jelentenek. - - -`inject*()` metódusok -===================== - -Ez a függőségátadás [setterrel |dependency-injection:passing-dependencies#Setteren keresztüli átadás] történő formája. Ezeknek a settereknek a neve `inject` előtaggal kezdődik. A Nette DI az így elnevezett metódusokat automatikusan meghívja rögtön a presenter példányának létrehozása után, és átadja nekik az összes szükséges függőséget. Ezért public-ként kell deklarálni őket. - -Az `inject*()` metódusok tekinthetők a konstruktor egyfajta kiterjesztésének több metódusba. Ennek köszönhetően a `BasePresenter` más metóduson keresztül veheti át a függőségeket, és a konstruktort szabadon hagyhatja a leszármazottai számára: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -A presenter tetszőleges számú `inject*()` metódust tartalmazhat, és mindegyiknek tetszőleges számú paramétere lehet. Kiválóan alkalmasak olyan esetekben is, amikor a presenter [traitekből |presenter-traits] áll össze, és mindegyik saját függőséget igényel. - - -`Inject` attribútumok -===================== - -Ez a [property-be történő injektálás |dependency-injection:passing-dependencies#Property beállításával] formája. Elég megjelölni, hogy mely változókba kell injektálni, és a Nette DI automatikusan átadja a függőségeket rögtön a presenter példányának létrehozása után. Ahhoz, hogy be tudja illeszteni őket, public-ként kell deklarálni őket. - -A property-ket attribútummal jelöljük meg: (korábban a `/** @inject */` annotációt használták) - -```php -use Nette\DI\Attributes\Inject; // ez a sor fontos - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Ennek a függőségátadási módnak az előnye a nagyon tömör írásmód volt. Azonban a [constructor property promotion |https://blog.nette.org/hu/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] megjelenésével egyszerűbbnek tűnik a konstruktor használata. - -Másrészt ez a módszer ugyanazoktól a hiányosságoktól szenved, mint a függőségek általános property-kbe történő átadása: nincs ellenőrzésünk a változóban bekövetkező változások felett, és ugyanakkor a változó az osztály nyilvános interfészének részévé válik, ami nem kívánatos. diff --git a/best-practices/hu/lets-create-contact-form.texy b/best-practices/hu/lets-create-contact-form.texy deleted file mode 100644 index 3ceafb1f03..0000000000 --- a/best-practices/hu/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Kapcsolatfelvételi űrlap létrehozása -************************************ - -.[perex] -Megnézzük, hogyan hozzunk létre egy kapcsolatfelvételi űrlapot a Nette-ben, beleértve az e-mail küldést is. Vágjunk bele! - -Először létre kell hoznunk egy új projektet. Hogy hogyan, azt az [Első lépések |nette:installation] oldal magyarázza el. Ezután elkezdhetjük az űrlap létrehozását. - -A legegyszerűbb módja az [űrlap létrehozása közvetlenül a presenterben |forms:in-presenter]. Használhatjuk az előkészített `HomePresenter`-t. Hozzáadjuk a `contactForm` komponenst, amely az űrlapot képviseli. Ezt úgy tesszük, hogy a kódba beírjuk a `createComponentContactForm()` factory metódust, amely létrehozza a komponenst: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Név:') - ->setRequired('Adja meg a nevét'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Adja meg az e-mail címét'); - $form->addTextarea('message', 'Üzenet:') - ->setRequired('Adja meg az üzenetet'); - $form->addSubmit('send', 'Küldés'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, $data): void - { - // e-mail küldése - } -} -``` - -Amint látja, két metódust hoztunk létre. Az első, `createComponentContactForm()` metódus létrehoz egy új űrlapot. Ennek vannak mezői a név, e-mail és üzenet számára, amelyeket az `addText()`, `addEmail()` és `addTextArea()` metódusokkal adunk hozzá. Hozzáadtunk egy gombot is az űrlap elküldéséhez. De mi van, ha a felhasználó nem tölt ki valamelyik mezőt? Ebben az esetben tudatnunk kell vele, hogy ez egy kötelező mező. Ezt a `setRequired()` metódussal értük el. Végül hozzáadtuk az [onSuccess |nette:glossary#Eventek események] eseményt is, amely akkor fut le, ha az űrlapot sikeresen elküldték. Esetünkben a `contactFormSucceeded` metódust hívja meg, amely gondoskodik az elküldött űrlap feldolgozásáról. Ezt hamarosan kiegészítjük a kódban. - -A `contactForm` komponenst a `Home/default.latte` sablonban rajzoltatjuk ki: - -```latte -{block content} -<h1>Kapcsolatfelvételi űrlap</h1> -{control contactForm} -``` - -Magához az e-mail küldéshez létrehozunk egy új osztályt, amelyet `ContactFacade`-nek nevezünk el, és az `app/Model/ContactFacade.php` fájlba helyezzük: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // az Ön e-mail címe - ->setFrom($email, $name) - ->setSubject('Üzenet a kapcsolatfelvételi űrlapról') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -A `sendMessage()` metódus létrehozza és elküldi az e-mailt. Ehhez az úgynevezett mailert használja, amelyet függőségként kap meg a konstruktoron keresztül. Olvasson többet az [e-mailek küldéséről |mail:]. - -Most visszatérünk a presenterhez, és befejezzük a `contactFormSucceeded()` metódust. Ez meghívja a `ContactFacade` osztály `sendMessage()` metódusát, és átadja neki az űrlap adatait. És hogyan szerezzük meg a `ContactFacade` objektumot? Megkapjuk a konstruktoron keresztül: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Az üzenet elküldve'); - $this->redirect('this'); - } -} -``` - -Miután az e-mail elküldésre került, még megjelenítünk a felhasználónak egy úgynevezett [flash üzenetet |application:components#Flash üzenetek], amely megerősíti, hogy az üzenet elküldésre került, majd átirányítjuk egy másik oldalra, hogy ne lehessen az űrlapot ismételten elküldeni a böngésző *frissítésével*. - - -Nos, ha minden működik, képesnek kell lennie e-mailt küldeni a kapcsolatfelvételi űrlapjáról. Gratulálok! - - -HTML e-mail sablon ------------------- - -Eddig egy egyszerű szöveges e-mail került elküldésre, amely csak az űrlapon elküldött üzenetet tartalmazta. Az e-mailben azonban használhatunk HTML-t, és vonzóbbá tehetjük a megjelenését. Létrehozunk hozzá egy Latte sablont, amelyet az `app/Model/contactEmail.latte` fájlba írunk: - -```latte -<html> - <title>Üzenet a kapcsolatfelvételi űrlapról - - -

    Név: {$name}

    -

    E-mail: {$email}

    -

    Üzenet: {$message}

    - - -``` - -Már csak a `ContactFacade`-et kell módosítani, hogy ezt a sablont használja. A konstruktorban kérjük a `LatteFactory` osztályt, amely képes létrehozni egy `Latte\Engine` objektumot, azaz egy [Latte sablon renderelőt |latte:develop#Hogyan rendereljünk sablont]. A `renderToString()` metódussal rendereljük a sablont egy fájlba, az első paraméter a sablon elérési útja, a második pedig a változók. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // az Ön e-mail címe - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -A generált HTML e-mailt ezután a `setHtmlBody()` metódusnak adjuk át az eredeti `setBody()` helyett. Szintén nem kell megadnunk az e-mail tárgyát a `setSubject()`-ben, mert a könyvtár azt a sablon `` eleméből veszi át. - - -Konfiguráció ------------- - -A `ContactFacade` osztály kódjában még mindig fixen be van írva az adminisztrátori e-mail címünk, az `admin@example.com`. Jobb lenne ezt a konfigurációs fájlba helyezni. Hogyan tegyük ezt? - -Először módosítjuk a `ContactFacade` osztályt, és az e-mail címet tartalmazó stringet egy konstruktoron keresztül átadott változóval helyettesítjük: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -A második lépés ennek a változónak az értékének megadása a konfigurációban. Az `app/config/services.neon` fájlba írjuk: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -És kész is. Ha a `services` szekcióban sok elem lenne, és úgy éreznénk, hogy az e-mail elveszik közöttük, akkor változóvá tehetjük. Módosítjuk a bejegyzést erre: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -És az `app/config/common.neon` fájlban definiáljuk ezt a változót: - -```neon -parameters: - adminEmail: admin@example.com -``` - -És kész is vagyunk! diff --git a/best-practices/hu/microsites.texy b/best-practices/hu/microsites.texy deleted file mode 100644 index feb5f639bb..0000000000 --- a/best-practices/hu/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Hogyan írjunk mikro-weboldalakat -******************************** - -Képzelje el, hogy gyorsan létre kell hoznia egy kis weboldalt a cége közelgő eseményére. Egyszerűnek, gyorsnak és felesleges bonyodalmaktól mentesnek kell lennie. Talán úgy gondolja, hogy egy ilyen kis projekthez nincs szüksége egy robusztus keretrendszerre. De mi van, ha a Nette keretrendszer használata alapvetően leegyszerűsítheti és felgyorsíthatja ezt a folyamatot? - -Hiszen még egyszerű weboldalak készítésekor sem akar lemondani a kényelemről. Nem akarja újra feltalálni azt, amit már egyszer megoldottak. Legyen nyugodtan lusta, és hagyja magát kényeztetni. A Nette Framework kiválóan használható mikro keretrendszerként is. - -Hogyan nézhet ki egy ilyen microsite? Például úgy, hogy a weboldal teljes kódját egyetlen `index.php` fájlba helyezzük a nyilvános mappában: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// hozzon létre DI konténert a config.neon konfiguráció alapján -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// beállítjuk a routingot -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// route a https://example.com/ URL-hez -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // érzékeljük a böngésző nyelvét és átirányítunk az /en vagy /de stb. URL-re - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// route a https://example.com/cs vagy https://example.com/en URL-hez -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // megjelenítjük a megfelelő sablont, például ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// indítsa el az alkalmazást! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Minden más sablon lesz, amelyek a szülő `/templates` mappában vannak tárolva. - -Az `index.php` PHP kódja először [előkészíti a környezetet |bootstrap:], majd definiálja a [route-okat |application:routing#Dinamikus routing callbackekkel], és végül elindítja az alkalmazást. Az előnye, hogy a `addRoute()` függvény második paramétere lehet egy callable, amely a megfelelő oldal megnyitása után végrehajtódik. - - -Miért használjunk Nette-t microsite-hoz? ----------------------------------------- - -- Azok a programozók, akik valaha kipróbálták a [Tracy |tracy:]-t, ma már el sem tudják képzelni, hogy nélküle programozzanak valamit. -- Mindenekelőtt azonban a [Latte |latte:] sablonrendszert fogja használni, mert már 2 oldaltól kezdve külön szeretné választani az [elrendezést és a tartalmat |latte:template-inheritance]. -- És határozottan szeretne támaszkodni az [automatikus escapelésre |latte:safety-first], hogy ne keletkezzen XSS sebezhetőség. -- A Nette azt is biztosítja, hogy hiba esetén soha ne jelenjenek meg a programozói PHP hibaüzenetek, hanem egy felhasználóbarát oldal. -- Ha visszajelzést szeretne kapni a felhasználóktól, például egy kapcsolatfelvételi űrlap formájában, akkor még hozzáadja az [űrlapokat |forms:] és az [adatbázist |database:]. -- A kitöltött űrlapokat szintén könnyedén [elküldheti e-mailben |mail:]. -- Néha hasznos lehet a [gyorsítótárazás |caching:], például ha feedeket tölt le és jelenít meg. - -Napjainkban, amikor a sebesség és a hatékonyság kulcsfontosságú, fontos, hogy olyan eszközök álljanak rendelkezésre, amelyek lehetővé teszik az eredmények elérését felesleges késedelem nélkül. A Nette keretrendszer pontosan ezt kínálja - gyors fejlesztést, biztonságot és széles körű eszközöket, mint például a Tracy és a Latte, amelyek egyszerűsítik a folyamatot. Elég telepíteni néhány Nette csomagot, és egy ilyen microsite létrehozása hirtelen gyerekjáték. És tudja, hogy sehol sem rejtőzik biztonsági rés. diff --git a/best-practices/hu/pagination.texy b/best-practices/hu/pagination.texy deleted file mode 100644 index 286a369a9b..0000000000 --- a/best-practices/hu/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Adatbázis eredmények lapozása -***************************** - -.[perex] -Webalkalmazások fejlesztése során nagyon gyakran találkozhat azzal a követelménnyel, hogy korlátozni kell az oldalon megjelenített elemek számát. - -Kezdjük azzal az állapottal, amikor minden adatot lapozás nélkül listázunk ki. Az adatok adatbázisból történő kiválasztásához van egy ArticleRepository osztályunk, amely a konstruktoron kívül tartalmaz egy `findPublishedArticles` metódust, amely visszaadja az összes publikált cikket a publikálás dátuma szerint csökkenő sorrendben. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -A presenterben ezután injectáljuk a modell osztályt, és a render metódusban lekérjük a publikált cikkeket, amelyeket átadunk a sablonnak: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -A `default.latte` sablonban pedig gondoskodunk a cikkek kiírásáról: - -```latte -{block content} -<h1>Cikkek</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Ezzel a módszerrel ki tudjuk listázni az összes cikket, ami azonban problémákat kezd okozni, amint a cikkek száma megnő. Ebben a pillanatban válik hasznossá egy lapozó mechanizmus implementálása. - -Ez biztosítja, hogy az összes cikk több oldalra legyen osztva, és mi csak az aktuális oldal cikkeit jelenítjük meg. Az oldalak teljes számát és a cikkek elosztását a [Paginator |utils:Paginator] maga számítja ki attól függően, hogy összesen hány cikkünk van, és hány cikket szeretnénk megjeleníteni egy oldalon. - -Az első lépésben módosítjuk a cikkek lekérésére szolgáló metódust a repository osztályban úgy, hogy csak egy oldal cikkeit tudja visszaadni. Hozzáadunk egy metódust is az adatbázisban lévő cikkek teljes számának lekérdezésére, amelyre szükségünk lesz a Paginator beállításához: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $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, - ); - } - - /** - * Visszaadja a publikált cikkek teljes számát - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Ezután nekilátunk a presenter módosításának. A render metódusba átadjuk az aktuálisan megjelenített oldal számát. Arra az esetre, ha ez a szám nem lenne része az URL-nek, beállítjuk az első oldal alapértelmezett értékét. - -Továbbá kibővítjük a render metódust a Paginator példányának megszerzésével, beállításával és a sablonban megjelenítendő megfelelő cikkek kiválasztásával. A HomePresenter a módosítások után így fog kinézni: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Lekérdezzük a publikált cikkek teljes számát - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Létrehozunk egy Paginator példányt és beállítjuk - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // cikkek teljes száma - $paginator->setItemsPerPage(10); // elemek száma oldalanként - $paginator->setPage($page); // aktuális oldal száma - - // Az adatbázisból lekérünk egy korlátozott cikkhalmazt a Paginator számítása szerint - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // amelyet átadunk a sablonnak - $this->template->articles = $articles; - // és magát a Paginatort is a lapozási lehetőségek megjelenítéséhez - $this->template->paginator = $paginator; - } -} -``` - -A sablonunk most már csak egy oldal cikkein iterál, elég hozzáadnunk a lapozó linkeket: - -```latte -{block content} -<h1>Cikkek</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Első</a> -  |  - <a n:href="default, $paginator->page-1">Előző</a> -  |  - {/if} - - Oldal {$paginator->getPage()} / {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Következő</a> -  |  - <a n:href="default, $paginator->getPageCount()">Utolsó</a> - {/if} -</div> -``` - - -Így egészítettük ki az oldalt a Paginator segítségével történő lapozás lehetőségével. Abban az esetben, ha a [Nette Database Core |database:sql-way] helyett adatbázisrétegként a [Nette Database Explorer |database:explorer]-t használjuk, képesek vagyunk implementálni a lapozást Paginator használata nélkül is. A `Nette\Database\Table\Selection` osztály ugyanis tartalmaz egy [page |api:Nette\Database\Table\Selection::_page] metódust a Paginatorból átvett lapozási logikával. - -A repository ebben az implementációs módban így fog kinézni: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -A presenterben nem kell Paginatort létrehoznunk, helyette a `Selection` osztály metódusát használjuk, amelyet a repository ad vissza: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Lekérjük a publikált cikkeket - $articles = $this->articleRepository->findPublishedArticles(); - - // és a sablonba csak azok egy részét küldjük el, amelyet a page metódus számítása korlátoz - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // és a szükséges adatokat is a lapozási lehetőségek megjelenítéséhez - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Mivel most nem küldünk Paginatort a sablonba, módosítjuk a lapozó linkeket megjelenítő részt: - -```latte -{block content} -<h1>Cikkek</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Első</a> -  |  - <a n:href="default, $page - 1">Előző</a> -  |  - {/if} - - Oldal {$page} / {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Következő</a> -  |  - <a n:href="default, $lastPage">Utolsó</a> - {/if} -</div> -``` - -Ezzel a módszerrel implementáltuk a lapozó mechanizmust Paginator használata nélkül. - -{{priority: -1}} diff --git a/best-practices/hu/passing-settings-to-presenters.texy b/best-practices/hu/passing-settings-to-presenters.texy deleted file mode 100644 index 077d860afd..0000000000 --- a/best-practices/hu/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Beállítások átadása presentereknek -********************************** - -.[perex] -Szüksége van arra, hogy olyan argumentumokat adjon át a presentereknek, amelyek nem objektumok (pl. információ arról, hogy debug módban fut-e, könyvtárak elérési útjai stb.), és ezért nem adhatók át automatikusan autowiring segítségével? A megoldás az, hogy becsomagolja őket egy `Settings` objektumba. - -A `Settings` szolgáltatás egy nagyon egyszerű, mégis hasznos módja annak, hogy információkat szolgáltassunk a futó alkalmazásról a presentereknek. Konkrét formája kizárólag az Ön igényeitől függ. Példa: - -```php -namespace App; - -class Settings -{ - public function __construct( - // PHP 8.1-től kezdve megadható a readonly - public bool $debugMode, - public string $appDir, - // és így tovább - ) {} -} -``` - -Példa a konfigurációba történő regisztrálásra: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Amikor egy presenternek szüksége van az e szolgáltatás által nyújtott információkra, egyszerűen elkéri a konstruktorban: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/hu/post-links.texy b/best-practices/hu/post-links.texy deleted file mode 100644 index 8faa625780..0000000000 --- a/best-practices/hu/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Hogyan használjuk helyesen a POST linkeket -****************************************** - -.[perex] -Webalkalmazásokban, különösen adminisztrációs felületeken, alapvető szabálynak kellene lennie, hogy a szerver állapotát megváltoztató műveleteket ne a GET HTTP metódussal végezzük. Ahogy a metódus neve is sugallja, a GET csak adatok lekérésére szolgál, nem pedig azok megváltoztatására. Olyan műveletekhez, mint például a rekordok törlése, célszerűbb a POST metódust használni. Bár ideális a DELETE metódus lenne, de azt JavaScript nélkül nem lehet meghívni, ezért történelmileg a POST-ot használják. - -Hogyan tegyük ezt a gyakorlatban? Használja ezt az egyszerű trükköt. A sablon elején hozzon létre egy segédűrlapot `postForm` azonosítóval, amelyet aztán a törlő gombokhoz használ: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Ennek az űrlapnak köszönhetően a klasszikus `<a>` link helyett használhat egy `<button>` gombot, amelyet vizuálisan úgy lehet módosítani, hogy úgy nézzen ki, mint egy normál link. Például a Bootstrap CSS keretrendszer `btn btn-link` osztályokat kínál, amelyekkel elérheti, hogy a gomb vizuálisan ne különbözzön a többi linktől. A `form="postForm"` attribútummal összekapcsoljuk az előkészített űrlappal: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">törlés</button> - <!-- <a n:href="delete $post->id">törlés</a> helyett --> - </td> - </tr> -</table> -``` - -A linkre kattintva most a `delete` akció hívódik meg. Annak biztosítására, hogy a kérések csak a POST metóduson keresztül és ugyanarról a domainről érkezzenek (ami hatékony védelem a CSRF támadások ellen), használja a `#[Requires]` attribútumot: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // hipotetikus kód a rekord törlésére - $this->redirect('default'); - } -} -``` - -Az attribútum a Nette Application 3.2 óta létezik, és további lehetőségeiről a [Hogyan használjuk a #Requires attribútumot |attribute-requires] oldalon olvashat többet. - -Ha az `actionDelete()` akció helyett a `handleDelete()` signált használná, nem szükséges megadni a `sameOrigin: true`-t, mert a signáloknak ez a védelme alapértelmezetten be van állítva: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Ez a megközelítés nemcsak javítja az alkalmazás biztonságát, hanem hozzájárul a helyes webes szabványok és gyakorlatok betartásához is. A POST metódusok használatával az állapotot megváltoztató műveletekhez robusztusabb és biztonságosabb alkalmazást érhet el. diff --git a/best-practices/hu/presenter-traits.texy b/best-practices/hu/presenter-traits.texy deleted file mode 100644 index a8ec3c6482..0000000000 --- a/best-practices/hu/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Presenterek összeállítása traitekkel -************************************ - -.[perex] -Ha több presenterben ugyanazt a kódot kell implementálnunk (pl. annak ellenőrzése, hogy a felhasználó be van-e jelentkezve), kézenfekvő a kódot egy közös ősbe helyezni. A második lehetőség egycélú [traitek |nette:introduction-to-object-oriented-programming#Traitek] létrehozása. - -Ennek a megoldásnak az az előnye, hogy minden presenter pontosan azokat a traiteket használhatja, amelyekre valóban szüksége van, míg a többszörös öröklődés PHP-ban nem lehetséges. - -Ezek a traitek kihasználhatják azt a tényt, hogy a presenter létrehozásakor sorban meghívódnak az összes [inject metódus |inject-method-attribute#inject metódusok]. Csak arra kell ügyelni, hogy minden inject metódus neve egyedi legyen. - -A traitek inicializáló kódot csatolhatnak az [onStartup vagy onRender |application:presenters#Események] eseményekhez. - -Példák: - -```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); - }; - } -} -``` - -A presenter ezután egyszerűen használja ezeket a traiteket: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/hu/restore-request.texy b/best-practices/hu/restore-request.texy deleted file mode 100644 index 9de22622f4..0000000000 --- a/best-practices/hu/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Hogyan térjünk vissza egy korábbi oldalra? -****************************************** - -.[perex] -Mi van, ha a felhasználó egy űrlapot tölt ki, és lejár a bejelentkezése? Hogy ne vesszenek el az adatok, a bejelentkezési oldalra történő átirányítás előtt az adatokat a sessionbe mentjük. A Nette-ben ez gyerekjáték. - -Az aktuális kérést a `storeRequest()` metódussal lehet a sessionbe menteni, amely visszaadja annak azonosítóját egy rövid string formájában. A metódus elmenti az aktuális presenter nevét, a view-t és annak paramétereit. Abban az esetben, ha egy űrlap is elküldésre került, a mezők tartalma is elmentésre kerül (a feltöltött fájlok kivételével). - -A kérés visszaállítását a `restoreRequest($key)` metódus végzi, amelynek átadjuk a kapott azonosítót. Ez átirányít az eredeti presenterhez és view-hoz. Ha azonban a mentett kérés egy űrlap elküldését tartalmazza, akkor az eredeti presenterhez a `forward()` metódussal lép át, átadja az űrlapnak a korábban kitöltött értékeket, és újra kirajzoltatja azt. Így a felhasználónak lehetősége van újra elküldeni az űrlapot, és nem vesznek el adatok. - -Fontos, hogy a `restoreRequest()` ellenőrzi, hogy az újonnan bejelentkezett felhasználó ugyanaz-e, aki eredetileg kitöltötte az űrlapot. Ha nem, eldobja a kérést, és nem tesz semmit. - -Mutassuk be mindezt egy példán. Legyen egy `AdminPresenter` presenterünk, amelyben adatokat szerkesztünk, és amelynek `startup()` metódusában ellenőrizzük, hogy a felhasználó be van-e jelentkezve. Ha nincs, átirányítjuk a `SignPresenter`-re. Ezzel egyidejűleg elmentjük az aktuális kérést, és annak kulcsát elküldjük a `SignPresenter`-nek. - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - protected function startup() - { - parent::startup(); - - if (!$this->user->isLoggedIn()) { - $this->redirect('Sign:in', ['backlink' => $this->storeRequest()]); - } - } -} -``` - -A `SignPresenter` a bejelentkezési űrlapon kívül tartalmazni fog egy `$backlink` perzisztens paramétert is, amelybe a kulcs beíródik. Mivel a paraméter perzisztens, a bejelentkezési űrlap elküldése után is átadásra kerül. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... hozzáadjuk az űrlap mezőit ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... itt bejelentkeztetjük a felhasználót ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -A `restoreRequest()` metódusnak átadjuk a mentett kérés kulcsát, és az átirányít (vagy átlép) az eredeti presenterhez. - -Ha azonban a kulcs érvénytelen (például már nem létezik a sessionben), a metódus nem tesz semmit. Ezt követi a `$this->redirect('Admin:')` hívása, amely átirányít az `AdminPresenter`-re. - -{{priority: -1}} diff --git a/best-practices/pt/@home.texy b/best-practices/pt/@home.texy deleted file mode 100644 index ff77ce2b7f..0000000000 --- a/best-practices/pt/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Guias e melhores práticas -************************* - -.[perex] -Guias, soluções para tarefas comuns e *melhores práticas* para Nette. - - -<div class=documentation> -<div> - - -Aplicação Nette ---------------- -- [Métodos e atributos inject |inject-method-attribute] -- [Composição de presenters a partir de traits |presenter-traits] -- [Passando configurações para presenters |passing-settings-to-presenters] -- [Como retornar a uma página anterior |restore-request] -- [Paginação de resultados do banco de dados |pagination] -- [Snippets dinâmicos |dynamic-snippets] -- [Como usar o atributo #Requires |attribute-requires] -- [Como usar corretamente links POST |post-links] - -</div> -<div> - - -Formulários ------------ -- [Reutilização de formulários |form-reuse] -- [Formulário para criar e editar registros |creating-editing-form] -- [Criando um formulário de contato |lets-create-contact-form] -- [Selectboxes dependentes |https://blog.nette.org/pt/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Geral ------ -- [Como carregar um arquivo de configuração |bootstrap:] -- [Como escrever microsites |microsites] -- [Por que o Nette usa a notação PascalCase para constantes? |https://blog.nette.org/pt/for-less-screaming-in-the-code] -- [Por que o Nette não usa o sufixo Interface? |https://blog.nette.org/pt/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: dicas de uso |composer] -- [Dicas sobre editores & ferramentas |editors-and-tools] -- [Introdução à programação orientada a objetos |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Solução de exemplo ------------------- -- [Exemplos Nette |https://github.com/nette-examples] -- [Doctrine & Nette |https://contributte.org/nettrine/] -- [Exemplos Contributte |https://contributte.org/examples.html] -- [Site Doctrine ORM |https://github.com/MinecordNetwork/Website] -- [Início rápido |quickstart:] - -</div> -<div> - - -Vídeos ------- -Centenas de gravações dos Últimos Sábados e vídeos sobre Nette podem ser encontrados sob um mesmo teto no "Canal do Youtube Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/pt/@meta.texy b/best-practices/pt/@meta.texy deleted file mode 100644 index 1bf3200c6f..0000000000 --- a/best-practices/pt/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Guias e melhores práticas}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/pt/attribute-requires.texy b/best-practices/pt/attribute-requires.texy deleted file mode 100644 index e128fa61b9..0000000000 --- a/best-practices/pt/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Como usar o atributo `#[Requires]` -********************************** - -.[perex] -Ao escrever uma aplicação web, você frequentemente encontrará a necessidade de restringir o acesso a certas partes da sua aplicação. Talvez você queira que algumas requisições possam enviar dados apenas através de um formulário (ou seja, pelo método POST), ou que sejam acessíveis apenas para chamadas AJAX. No Nette Framework 3.2, surgiu uma nova ferramenta que permite definir tais restrições de forma muito elegante e clara: o atributo `#[Requires]`. - -Um atributo é uma marca especial em PHP que você adiciona antes da definição de uma classe ou método. Como na verdade é uma classe, para que os exemplos a seguir funcionem, é necessário incluir a cláusula use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Você pode usar o atributo `#[Requires]` na própria classe do presenter e também nestes métodos: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Os dois últimos métodos também se aplicam a componentes, ou seja, você também pode usar o atributo neles. - -Se as condições especificadas pelo atributo não forem atendidas, um erro HTTP 4xx será lançado. - - -Métodos HTTP ------------- - -Você pode especificar quais métodos HTTP (como GET, POST, etc.) são permitidos para acesso. Por exemplo, se você quiser permitir o acesso apenas enviando um formulário, defina: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Por que você deve usar POST em vez de GET para ações que alteram o estado e como fazer isso? [Leia o tutorial |post-links]. - -Você pode especificar um método ou um array de métodos. Um caso especial é o valor `'*'`, que permite todos os métodos, o que os presenters normalmente não permitem por [razões de segurança |application:presenters#Verificação do método HTTP]. - - -Chamada AJAX ------------- - -Se você quiser que o presenter ou método esteja disponível apenas para requisições AJAX, use: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Mesma origem ------------- - -Para aumentar a segurança, você pode exigir que a requisição seja feita do mesmo domínio. Isso evita a [vulnerabilidade CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Para os métodos `handle<Signal>()`, o acesso do mesmo domínio é exigido automaticamente. Portanto, se, pelo contrário, você quiser permitir o acesso de qualquer domínio, especifique: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Acesso via forward ------------------- - -Às vezes, é útil restringir o acesso a um presenter para que ele esteja disponível apenas indiretamente, por exemplo, usando o método `forward()` ou `switch()` de outro presenter. Assim, por exemplo, protegem-se os error-presenters para que não possam ser chamados a partir da URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Na prática, muitas vezes é necessário marcar certas views às quais só se pode chegar com base na lógica do presenter. Ou seja, novamente, para que não possam ser abertas diretamente: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Ações específicas ------------------ - -Você também pode restringir que um determinado código, como a criação de um componente, esteja disponível apenas para ações específicas no presenter: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -No caso de uma única ação, não é necessário escrever um array: `#[Requires(actions: 'default')]` - - -Atributos personalizados ------------------------- - -Se você quiser usar o atributo `#[Requires]` repetidamente com as mesmas configurações, pode criar seu próprio atributo que herdará `#[Requires]` e o configurará de acordo com as necessidades. - -Por exemplo, `#[SingleAction]` permitirá o acesso apenas através da ação `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Ou `#[RestMethods]` permitirá o acesso através de todos os métodos HTTP usados para APIs REST: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Conclusão ---------- - -O atributo `#[Requires]` oferece grande flexibilidade e controle sobre como suas páginas web são acessíveis. Usando regras simples, mas poderosas, você pode aumentar a segurança e o funcionamento correto da sua aplicação. Como você pode ver, o uso de atributos no Nette pode não apenas facilitar seu trabalho, mas também torná-lo mais seguro. diff --git a/best-practices/pt/composer.texy b/best-practices/pt/composer.texy deleted file mode 100644 index 0bbb245278..0000000000 --- a/best-practices/pt/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: dicas de uso -********************** - -<div class=perex> - -O Composer é uma ferramenta para gerenciamento de dependências em PHP. Ele nos permite listar as bibliotecas das quais nosso projeto depende e as instalará e atualizará para nós. Vamos mostrar: - -- como instalar o Composer -- seu uso em um projeto novo ou existente - -</div> - - -Instalação -========== - -O Composer é um arquivo `.phar` executável que você baixa e instala da seguinte maneira: - - -Windows -------- - -Use o instalador oficial [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Basta seguir 4 comandos que você pode copiar [desta página |https://getcomposer.org/download/]. - -Além disso, colocando-o em uma pasta que esteja no `PATH` do sistema, o Composer se torna acessível globalmente: - -```shell -$ mv ./composer.phar ~/bin/composer # ou /usr/local/bin/composer -``` - - -Uso no projeto -============== - -Para começar a usar o Composer em seu projeto, você só precisa do arquivo `composer.json`. Ele descreve as dependências do nosso projeto e também pode conter outros metadados. Um `composer.json` básico pode, portanto, parecer assim: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Aqui dizemos que nossa aplicação (ou biblioteca) requer o pacote `nette/database` (o nome do pacote consiste no nome da organização e no nome do projeto) e quer a versão que corresponde à condição `^3.0` (ou seja, a versão 3 mais recente). - -Temos, portanto, o arquivo `composer.json` na raiz do projeto e executamos a instalação: - -```shell -composer update -``` - -O Composer baixará o Nette Database para a pasta `vendor/`. Além disso, criará o arquivo `composer.lock`, que contém informações sobre quais versões exatas das bibliotecas ele instalou. - -O Composer gera o arquivo `vendor/autoload.php`, que podemos simplesmente incluir e começar a usar as bibliotecas sem qualquer trabalho adicional: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Atualização de pacotes para as versões mais recentes -==================================================== - -A atualização das bibliotecas usadas para as versões mais recentes, de acordo com as condições definidas em `composer.json`, é responsabilidade do comando `composer update`. Por exemplo, para a dependência `"nette/database": "^3.0"`, ele instalará a versão 3.x.x mais recente, mas não a versão 4. - -Para atualizar as condições no arquivo `composer.json`, por exemplo, para `"nette/database": "^4.1"`, para que seja possível instalar a versão mais recente, use o comando `composer require nette/database`. - -Para atualizar todos os pacotes Nette usados, seria necessário listá-los todos na linha de comando, por exemplo: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -O que é impraticável. Use, portanto, o script simples "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, que fará isso por você: - -```shell -php composer-frontline.php -``` - - -Criação de um novo projeto -========================== - -Você pode criar um novo projeto Nette com um único comando: - -```shell -composer create-project nette/web-project nome-do-projeto -``` - -Como `nome-do-projeto`, insira o nome do diretório para o seu projeto e confirme. O Composer baixará o repositório `nette/web-project` do GitHub, que já contém o arquivo `composer.json`, e logo depois o Nette Framework. Deve bastar apenas [definir as permissões |nette:troubleshooting#Configurando Permissões de Diretório] de escrita nas pastas `temp/` e `log/` e o projeto deve ganhar vida. - -Se você sabe em qual versão do PHP o projeto será hospedado, não se esqueça de [configurá-la |#Versão do PHP]. - - -Versão do PHP -============= - -O Composer sempre instala as versões dos pacotes que são compatíveis com a versão do PHP que você está usando atualmente (mais precisamente, com a versão do PHP usada na linha de comando ao executar o Composer). O que, no entanto, provavelmente não é a mesma versão que sua hospedagem usa. Por isso, é muito importante adicionar ao arquivo `composer.json` a informação sobre a versão do PHP na hospedagem. Depois disso, apenas as versões dos pacotes compatíveis com a hospedagem serão instaladas. - -Para definir que o projeto será executado, por exemplo, no PHP 8.2.3, usamos o comando: - -```shell -composer config platform.php 8.2.3 -``` - -Assim, a versão será escrita no arquivo `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -No entanto, o número da versão do PHP é especificado em outro local do arquivo, na seção `require`. Enquanto o primeiro número determina para qual versão os pacotes serão instalados, o segundo número diz para qual versão a própria aplicação foi escrita. E de acordo com ele, por exemplo, o PhpStorm define o *PHP language level*. (Claro, não faz sentido que essas versões sejam diferentes, então a dupla escrita é uma falha de design.) Você define esta versão com o comando: - -```shell -composer require php 8.2.3 --no-update -``` - -Ou diretamente no arquivo `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignorar versão do PHP -===================== - -Os pacotes geralmente especificam tanto a versão mais baixa do PHP com a qual são compatíveis quanto a mais alta com a qual foram testados. Se você planeja usar uma versão do PHP ainda mais recente, talvez para fins de teste, o Composer se recusará a instalar tal pacote. A solução é a opção `--ignore-platform-req=php+`, que faz com que o Composer ignore os limites superiores da versão do PHP exigida. - - -Mensagens falsas -================ - -Ao atualizar pacotes ou alterar números de versão, pode ocorrer um conflito. Um pacote tem requisitos que estão em conflito com outro e assim por diante. Mas o Composer às vezes exibe uma mensagem falsa. Ele relata um conflito que realmente não existe. Nesse caso, ajuda excluir o arquivo `composer.lock` e tentar novamente. - -Se a mensagem de erro persistir, então ela é séria e é necessário ler nela o que e como ajustar. - - -Packagist.org - repositório central -=================================== - -[Packagist |https://packagist.org] é o repositório principal no qual o Composer tenta procurar pacotes, a menos que lhe digamos o contrário. Também podemos publicar nossos próprios pacotes aqui. - - -E se não quisermos usar o repositório central? ----------------------------------------------- - -Se tivermos aplicações internas da empresa que simplesmente não podemos hospedar publicamente, criaremos um repositório corporativo para elas. - -Mais sobre o tema de repositórios [na documentação oficial |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -Uma característica fundamental do Composer é que ele fornece autoloading para todas as classes instaladas por ele, que você inicia incluindo o arquivo `vendor/autoload.php`. - -No entanto, é possível usar o Composer também para carregar outras classes fora da pasta `vendor`. A primeira opção é deixar o Composer pesquisar pastas e subpastas definidas, encontrar todas as classes e incluí-las no autoloader. Isso é alcançado definindo `autoload > classmap` em `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/" # inclui a pasta src/ e suas subpastas - ] - } -} -``` - -Posteriormente, é necessário executar o comando `composer dumpautoload` a cada alteração e deixar as tabelas de autoloading serem regeneradas. Isso é extremamente inconveniente e é muito melhor confiar esta tarefa ao [RobotLoader|robot-loader:], que realiza a mesma atividade automaticamente em segundo plano e muito mais rapidamente. - -A segunda opção é seguir o [PSR-4|https://www.php-fig.org/psr/psr-4/]. Simplificadamente, é um sistema onde namespaces e nomes de classes correspondem à estrutura de diretórios e nomes de arquivos, ou seja, por exemplo, `App\Core\RouterFactory` estará no arquivo `/path/to/App/Core/RouterFactory.php`. Exemplo de configuração: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # o namespace App\ está no diretório app/ - } - } -} -``` - -Como configurar exatamente o comportamento pode ser encontrado na [documentação do Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Testando novas versões -====================== - -Você quer testar uma nova versão de desenvolvimento de um pacote. Como fazer isso? Primeiro, adicione este par de opções ao arquivo `composer.json`, que permite instalar versões de desenvolvimento de pacotes, mas recorrerá a isso apenas se não houver nenhuma combinação de versões estáveis que atenda aos requisitos: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true -} -``` - -Além disso, recomendamos excluir o arquivo `composer.lock`, às vezes o Composer inexplicavelmente se recusa a instalar e isso resolve o problema. - -Digamos que seja o pacote `nette/utils` e a nova versão tenha o número 4.0. Você a instala com o comando: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Ou você pode instalar uma versão específica, por exemplo, 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Mas se outro pacote depender da biblioteca, que está bloqueada em uma versão mais antiga (por exemplo, `^3.1`), então o ideal é atualizar o pacote para que funcione com a nova versão. No entanto, se você quiser apenas contornar a restrição e forçar o Composer a instalar a versão de desenvolvimento e fingir que é uma versão mais antiga (por exemplo, 3.1.6), pode usar a palavra-chave `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Chamada de comandos -=================== - -Através do Composer, é possível chamar comandos e scripts próprios pré-preparados, como se fossem comandos nativos do Composer. Para scripts localizados na pasta `vendor/bin`, não é necessário especificar esta pasta. - -Como exemplo, definimos no arquivo `composer.json` um script que, usando o [Nette Tester|tester:], executa os testes: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Os testes são então executados usando `composer tester`. O comando pode ser chamado mesmo que não estejamos na pasta raiz do projeto, mas em algum subdiretório. - - -Envie agradecimentos -==================== - -Mostraremos um truque que agradará os autores de open source. De maneira simples, você pode dar uma estrela no GitHub às bibliotecas que seu projeto usa. Basta instalar a biblioteca `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -E depois executar: - -```shell -composer thanks -``` - -Experimente! - - -Configuração -============ - -O Composer está intimamente ligado à ferramenta de versionamento [Git |https://git-scm.com]. Se você não o tiver instalado, é necessário dizer ao Composer para não usá-lo: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/pt/creating-editing-form.texy b/best-practices/pt/creating-editing-form.texy deleted file mode 100644 index c08c92a039..0000000000 --- a/best-practices/pt/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Formulário para criar e editar um registro -****************************************** - -.[perex] -Como implementar corretamente a adição e edição de um registro no Nette, usando o mesmo formulário para ambos? - -Em muitos casos, os formulários para adicionar e editar um registro são os mesmos, diferindo talvez apenas no rótulo do botão. Mostraremos exemplos de presenters simples onde usaremos o formulário primeiro para adicionar um registro, depois para editar e, finalmente, combinaremos ambas as soluções. - - -Adicionar um registro ---------------------- - -Exemplo de um presenter usado para adicionar um registro. Deixaremos o trabalho real com o banco de dados para a classe `Facade`, cujo código não é essencial para a demonstração. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... adicionamos os campos do formulário ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // adiciona o registro ao banco de dados - $this->flashMessage('Adicionado com sucesso'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Editar um registro ------------------- - -Agora mostraremos como seria um presenter usado para editar um registro: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // verifica a existência do registro - || !$this->facade->isEditAllowed(/*...*/) // verifica a permissão - ) { - $this->error(); // erro 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // verificamos se a ação é 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... adicionamos os campos do formulário ... - - $form->setDefaults($this->record); // define os valores padrão - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // atualiza o registro - $this->flashMessage('Atualizado com sucesso'); - $this->redirect('...'); - } -} -``` - -No método `actionEdit`, que é executado logo no início do [ciclo de vida do presenter |application:presenters#Ciclo de vida do presenter], verificamos a existência do registro e a permissão do usuário para editá-lo. - -Armazenamos o registro na propriedade `$record` para tê-lo disponível no método `createComponentRecordForm()` para definir os valores padrão e em `recordFormSucceeded()` para o ID. Uma solução alternativa seria definir os valores padrão diretamente em `actionEdit()` e obter o valor do ID, que faz parte da URL, usando `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // verifica a existência e a permissão - ) { - $this->error(); - } - - // define os valores padrão do formulário - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -No entanto, e isso deve ser **o ponto mais importante de todo o código**, devemos garantir ao criar o formulário que a ação seja realmente `edit`. Caso contrário, a verificação no método `actionEdit()` não ocorreria de forma alguma! - - -O mesmo formulário para adicionar e editar ------------------------------------------- - -E agora combinamos ambos os presenters em um só. Poderíamos distinguir qual ação está sendo realizada no método `createComponentRecordForm()` e configurar o formulário de acordo, ou podemos deixar isso diretamente para os métodos de ação e nos livrar da condição: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private 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 // verifica a existência do registro - || !$this->facade->isEditAllowed(/*...*/) // verifica a permissão - ) { - $this->error(); // erro 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // define os valores padrão - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // verificamos se a ação é 'add' ou 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... adicionamos os campos do formulário ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // adiciona o registro ao banco de dados - $this->flashMessage('Adicionado com sucesso'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // atualiza o registro - $this->flashMessage('Atualizado com sucesso'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/pt/dynamic-snippets.texy b/best-practices/pt/dynamic-snippets.texy deleted file mode 100644 index a77d207b02..0000000000 --- a/best-practices/pt/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Snippets dinâmicos -****************** - -Com bastante frequência, durante o desenvolvimento de aplicações, surge a necessidade de realizar operações AJAX, por exemplo, em linhas individuais de uma tabela ou itens de uma lista. Como exemplo, podemos escolher a exibição de artigos, onde permitimos que um usuário logado escolha a avaliação "gosto/não gosto" para cada um deles. O código do presenter e do template correspondente sem AJAX será aproximadamente o seguinte (apresento os trechos mais importantes, o código assume a existência de um serviço para marcar a avaliação e obter a coleção de artigos - a implementação específica não é importante para os fins deste 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 -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>Gosto disto</a> - {else} - <a n:href="unlike! $article->id" class=ajax>Já não gosto disto</a> - {/if} -</article> -``` - - -Ajaxificação -============ - -Vamos agora equipar esta aplicação simples com AJAX. A alteração da avaliação de um artigo não é tão importante a ponto de exigir um redirecionamento, e, portanto, idealmente, deveria ocorrer via AJAX em segundo plano. Usaremos o [script auxiliar dos add-ons |application:ajax#Naja] com a convenção usual de que os links AJAX têm a classe CSS `ajax`. - -Mas como fazer isso especificamente? O Nette oferece 2 caminhos: o caminho dos chamados snippets dinâmicos e o caminho dos componentes. Ambos têm seus prós e contras, e por isso vamos mostrá-los um por um. - - -Caminho dos snippets dinâmicos -============================== - -Um snippet dinâmico, na terminologia Latte, significa um caso específico de uso da tag `{snippet}`, onde uma variável é usada no nome do snippet. Tal snippet não pode estar em qualquer lugar no template - deve ser envolvido por um snippet estático, ou seja, um comum, ou dentro de `{snippetArea}`. Poderíamos modificar nosso template da seguinte forma. - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>Gosto disto</a> - {else} - <a n:href="unlike! $article->id" class=ajax>Já não gosto disto</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Cada artigo agora define um snippet que tem o ID do artigo em seu nome. Todos esses snippets são então agrupados em um único snippet chamado `articlesContainer`. Se omitíssemos este snippet envolvente, o Latte nos alertaria com uma exceção. - -Resta-nos adicionar o redesenho ao presenter - basta redesenhar o invólucro estático. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- não é necessário - } else { - $this->redirect('this'); - } -} -``` - -Modificamos de forma semelhante o método irmão `handleUnlike()`, e o AJAX está funcional! - -A solução, no entanto, tem um lado sombrio. Se investigássemos mais a fundo como a requisição AJAX ocorre, descobriríamos que, embora externamente a aplicação pareça econômica (retorna apenas um único snippet para o artigo em questão), na realidade, no servidor, ela renderizou todos os snippets. Ela colocou o snippet desejado no payload e descartou os outros (obtendo-os desnecessariamente do banco de dados também). - -Para otimizar este processo, teremos que intervir onde passamos a coleção `$articles` para o template (digamos, no método `renderDefault()`). Aproveitaremos o fato de que o processamento de sinais ocorre antes dos métodos `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Agora, ao processar o sinal, em vez de uma coleção com todos os artigos, apenas um array com um único artigo é passado para o template - aquele que queremos renderizar e enviar no payload para o navegador. O `{foreach}` então ocorrerá apenas uma vez e nenhum snippet extra será renderizado. - - -Caminho dos componentes -======================= - -Uma forma completamente diferente de solução evita os snippets dinâmicos. O truque consiste em transferir toda a lógica para um componente separado - a partir de agora, o presenter não será responsável pela inserção da avaliação, mas sim um `LikeControl` dedicado. A classe ficará assim (além disso, conterá também os métodos `render`, `handleUnlike`, etc.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private 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 do componente: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>Gosto disto</a> - {else} - <a n:href="unlike!" class=ajax>Já não gosto disto</a> - {/if} -{/snippet} -``` - -Claro, o template da view mudará e teremos que adicionar uma fábrica ao presenter. Como criaremos o componente tantas vezes quantos artigos obtivermos do banco de dados, usaremos a classe [application:Multiplier] para sua "multiplicação". - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -O template da view será reduzido ao mínimo necessário (e completamente livre de snippets!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Estamos quase lá: a aplicação agora funcionará com AJAX. Aqui também teremos que otimizar a aplicação, porque devido ao uso do Nette Database, ao processar o sinal, todos os artigos são carregados desnecessariamente do banco de dados em vez de apenas um. A vantagem, no entanto, é que eles não serão renderizados, pois apenas nosso componente será renderizado. - -{{priority: -1}} diff --git a/best-practices/pt/editors-and-tools.texy b/best-practices/pt/editors-and-tools.texy deleted file mode 100644 index 6d841136f7..0000000000 --- a/best-practices/pt/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Editores & Ferramentas -********************** - -.[perex] -Você pode ser um programador habilidoso, mas é com boas ferramentas que você se torna um mestre. Neste capítulo, você encontrará dicas sobre ferramentas importantes, editores e plugins. - - -Editor IDE -========== - -Recomendamos fortemente o uso de um IDE completo para desenvolvimento, como PhpStorm, NetBeans, VS Code, e não apenas um editor de texto com suporte a PHP. A diferença é realmente fundamental. Não há razão para se contentar com um simples editor que, embora possa colorir a sintaxe, não atinge as capacidades de um IDE de ponta, que sugere com precisão, monitora erros, pode refatorar código e muito mais. Alguns IDEs são pagos, outros são até gratuitos. - -**NetBeans IDE** já vem com suporte integrado para Nette, Latte e NEON. - -**PhpStorm**: instale estes plugins em `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: encontre o plugin "Nette Latte + Neon" no marketplace. - -Conecte também o Tracy ao seu editor. Ao exibir uma página de erro, você poderá clicar nos nomes dos arquivos e eles serão abertos no editor com o cursor na linha correspondente. Leia [como configurar o sistema|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan é uma ferramenta que detecta erros lógicos no código antes mesmo de você executá-lo. - -Instalamos usando o Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Criamos um arquivo de configuração `phpstan.neon` no projeto: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -E, em seguida, deixamos que ele analise as classes na pasta `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Você encontrará documentação completa diretamente no [site do PHPStan |https://phpstan.org]. - - -Code Checker -============ - -O [Code Checker|code-checker:] verifica e, opcionalmente, corrige alguns erros formais em seus códigos-fonte: - -- remove [BOM |nette:glossary#BOM] -- verifica a validade dos templates [Latte |latte:] -- verifica a validade dos arquivos `.neon`, `.php` e `.json` -- verifica a ocorrência de [caracteres de controle |nette:glossary#Caracteres de controle] -- verifica se o arquivo está codificado em UTF-8 -- verifica `/* @anotações */` escritas incorretamente (falta um asterisco) -- remove `?>` de fechamento em arquivos PHP -- remove espaços em branco à direita e linhas desnecessárias no final do arquivo -- normaliza os separadores de linha para os do sistema (se você usar a opção `-l`) - - -Composer -======== - -[Composer|best-practices:composer] é uma ferramenta para gerenciamento de dependências em PHP. Permite declarar dependências arbitrariamente complexas de bibliotecas individuais e, em seguida, as instala para nós em nosso projeto. - - -Requirements Checker -==================== - -Era uma ferramenta que testava o ambiente de execução do servidor e informava se (e em que medida) o framework poderia ser usado. Atualmente, o Nette pode ser usado em qualquer servidor que tenha a versão mínima exigida do PHP. diff --git a/best-practices/pt/form-reuse.texy b/best-practices/pt/form-reuse.texy deleted file mode 100644 index a1c88ae4c5..0000000000 --- a/best-practices/pt/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Reutilização de formulários em vários lugares -********************************************* - -.[perex] -No Nette, você tem várias opções para usar o mesmo formulário em vários lugares sem duplicar o código. Neste artigo, mostraremos diferentes soluções, incluindo aquelas que você deve evitar. - - -Fábrica de formulários -====================== - -Uma das abordagens básicas para usar o mesmo componente em vários lugares é criar um método ou classe que gera esse componente e, em seguida, chamar esse método em diferentes lugares da aplicação. Tal método ou classe é chamado de *fábrica*. Por favor, não confunda com o padrão de projeto *factory method*, que descreve uma forma específica de usar fábricas e não está relacionado a este tópico. - -Como exemplo, criaremos uma fábrica que construirá um formulário de edição: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - return $form; - } -} -``` - -Agora você pode usar esta fábrica em diferentes lugares da sua aplicação, por exemplo, em presenters ou componentes. E isso é feito [solicitando-a como dependência|dependency-injection:passing-dependencies]. Primeiro, registramos a classe no arquivo de configuração: - -```neon -services: - - FormFactory -``` - -E depois a usamos no presenter: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // processamento dos dados enviados - }; - return $form; - } -} -``` - -Você pode estender a fábrica de formulários com outros métodos para criar outros tipos de formulários de acordo com as necessidades da sua aplicação. E, claro, também podemos adicionar um método que cria um formulário básico sem elementos, e os outros métodos o utilizarão: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - return $form; - } -} -``` - -O método `createForm()` ainda não faz nada útil, mas isso mudará rapidamente. - - -Dependências da fábrica -======================= - -Com o tempo, percebe-se que precisamos que os formulários sejam multilíngues. Isso significa que precisamos definir o chamado [tradutor |forms:rendering#Tradução] para todos os formulários. Para isso, modificaremos a classe `FormFactory` para que ela aceite o objeto `Translator` como dependência no construtor e o passe para o formulário: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Como o método `createForm()` também é chamado por outros métodos que criam formulários específicos, basta definir o tradutor apenas nele. E está feito. Não é necessário alterar o código de nenhum presenter ou componente, o que é ótimo. - - -Múltiplas classes de fábrica -============================ - -Alternativamente, você pode criar várias classes para cada formulário que deseja usar em sua aplicação. Essa abordagem pode aumentar a legibilidade do código e facilitar o gerenciamento dos formulários. Deixaremos a `FormFactory` original criar apenas um formulário limpo com configuração básica (por exemplo, com suporte a traduções) e criaremos uma nova fábrica `EditFormFactory` para o formulário de edição. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ uso de composição -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - return $form; - } -} -``` - -É muito importante que a relação entre as classes `FormFactory` e `EditFormFactory` seja realizada por [composição |nette:introduction-to-object-oriented-programming#Composição], e não por [herança de objetos |nette:introduction-to-object-oriented-programming#Herança]: - -```php -// ⛔ ASSIM NÃO! A HERANÇA NÃO PERTENCE AQUI -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - return $form; - } -} -``` - -O uso de herança seria completamente contraproducente neste caso. Você encontraria problemas muito rapidamente. Por exemplo, no momento em que quisesse adicionar parâmetros ao método `create()`; o PHP relataria um erro de que sua assinatura difere da do pai. Ou ao passar dependências para a classe `EditFormFactory` através do construtor. Ocorreria uma situação que chamamos de [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. - -Em geral, é melhor [preferir composição em vez de herança |dependency-injection:faq#Por que a composição é preferida em relação à herança]. - - -Manipulação do formulário -========================= - -O manipulador do formulário, que é chamado após o envio bem-sucedido, também pode fazer parte da classe de fábrica. Ele funcionará passando os dados enviados para o modelo para processamento. Eventuais erros são [passados de volta |forms:validation#Erros durante o processamento] para o formulário. O modelo no exemplo a seguir é representado pela classe `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // processamento dos dados enviados - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -No entanto, deixaremos o redirecionamento em si para o presenter. Ele adicionará outro manipulador ao evento `onSuccess`, que realizará o redirecionamento. Graças a isso, será possível usar o formulário em diferentes presenters e redirecionar para um local diferente em cada um deles. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('O registro foi salvo'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Esta solução utiliza a propriedade dos formulários de que, quando `addError()` é chamado no formulário ou em seu elemento, o próximo manipulador `onSuccess` não é chamado. - - -Herança da classe Form -====================== - -Um formulário construído não deve ser um descendente da classe `Form`. Em outras palavras, não use esta solução: - -```php -// ⛔ ASSIM NÃO! A HERANÇA NÃO PERTENCE AQUI -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $this->addSubmit('send', 'Enviar'); - $this->setTranslator($translator); - } -} -``` - -Em vez de construir o formulário no construtor, use uma fábrica. - -É preciso perceber que a classe `Form` é, antes de tudo, uma ferramenta para construir um formulário, ou seja, um *form builder*. E o formulário construído pode ser entendido como seu produto. Mas o produto não é um caso específico do builder, não há entre eles uma relação *is a* que forma a base da herança. - - -Componente com formulário -========================= - -Uma abordagem completamente diferente é a criação de [componentes|application:components] que incluem um formulário. Isso oferece novas possibilidades, como renderizar o formulário de uma maneira específica, já que o componente também inclui um template. Ou é possível usar sinais para comunicação AJAX e carregamento de informações no formulário, por exemplo, para sugestões, etc. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Título:'); - // aqui são adicionados outros campos do formulário - $form->addSubmit('send', 'Enviar'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // processamento dos dados enviados - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // dispara o evento - $this->onSave($this, $data); - } -} -``` - -Criaremos também uma fábrica que produzirá este componente. Basta [registrar sua interface |application:components#Componentes com dependências]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -E adicionar ao arquivo de configuração: - -```neon -services: - - EditControlFactory -``` - -E agora podemos solicitar a fábrica e usá-la no presenter: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // ou redirecionamos para o resultado da edição, por exemplo: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/pt/inject-method-attribute.texy b/best-practices/pt/inject-method-attribute.texy deleted file mode 100644 index c5b05410c2..0000000000 --- a/best-practices/pt/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Métodos e atributos inject -************************** - -.[perex] -Neste artigo, focaremos nas diferentes maneiras de passar dependências para presenters no framework Nette. Compararemos a forma preferida, que é o construtor, com outras opções, como métodos e atributos `inject`. - -Também para presenters, passar dependências usando o [construtor |dependency-injection:passing-dependencies#Passagem pelo construtor] é o caminho preferido. No entanto, se você criar um ancestral comum do qual outros presenters herdam (por exemplo, `BasePresenter`), e este ancestral também tiver dependências, ocorrerá um problema que chamamos de [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Isso pode ser contornado usando caminhos alternativos, que são os métodos e atributos (anotações) `inject`. - - -Métodos `inject*()` -=================== - -É uma forma de passar dependências por [setter |dependency-injection:passing-dependencies#Passagem por setter]. O nome desses setters começa com o prefixo `inject`. O Nette DI chama automaticamente métodos com esse nome logo após a criação da instância do presenter e passa a eles todas as dependências necessárias. Portanto, eles devem ser declarados como `public`. - -Os métodos `inject*()` podem ser considerados como uma extensão do construtor em vários métodos. Graças a isso, o `BasePresenter` pode receber dependências através de outro método e deixar o construtor livre para seus descendentes: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Um presenter pode conter qualquer número de métodos `inject*()` e cada um pode ter qualquer número de parâmetros. Eles também são ótimos em casos onde o presenter é [composto por traits |presenter-traits] e cada um deles requer sua própria dependência. - - -Atributos `Inject` -================== - -É uma forma de [injeção na propriedade |dependency-injection:passing-dependencies#Configuração de propriedade]. Basta marcar em quais propriedades injetar, e o Nette DI passa automaticamente as dependências logo após a criação da instância do presenter. Para poder inseri-las, é necessário declará-las como `public`. - -Marcamos as propriedades com um atributo: (anteriormente, usava-se a anotação `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // esta linha é importante - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -A vantagem dessa forma de passar dependências era a forma de escrita muito concisa. No entanto, com a chegada da [promoção de propriedades do construtor |https://blog.nette.org/pt/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], parece mais fácil usar o construtor. - -Por outro lado, essa forma sofre das mesmas desvantagens que a passagem de dependências para propriedades em geral: não temos controle sobre as alterações na variável e, ao mesmo tempo, a variável se torna parte da interface pública da classe, o que é indesejável. diff --git a/best-practices/pt/lets-create-contact-form.texy b/best-practices/pt/lets-create-contact-form.texy deleted file mode 100644 index f75fa749a8..0000000000 --- a/best-practices/pt/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Criando um formulário de contato -******************************** - -.[perex] -Vamos ver como criar um formulário de contato no Nette, incluindo o envio para e-mail. Então, vamos lá! - -Primeiro, precisamos criar um novo projeto. Como fazer isso é explicado na página [Começando |nette:installation]. E então podemos começar a criar o formulário. - -A maneira mais simples é criar o [formulário diretamente no presenter |forms:in-presenter]. Podemos usar o `HomePresenter` pré-preparado. Nele, adicionaremos o componente `contactForm` que representa o formulário. Faremos isso escrevendo o método de fábrica `createComponentContactForm()` no código, que produzirá o componente: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Nome:') - ->setRequired('Por favor, digite seu nome.'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Por favor, digite seu e-mail.'); - $form->addTextarea('message', 'Mensagem:') - ->setRequired('Por favor, digite sua mensagem.'); - $form->addSubmit('send', 'Enviar'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, $data): void - { - // envio de e-mail - } -} -``` - -Como você pode ver, criamos dois métodos. O primeiro método `createComponentContactForm()` cria um novo formulário. Ele tem campos para nome, e-mail e mensagem, que adicionamos com os métodos `addText()`, `addEmail()` e `addTextArea()`. Também adicionamos um botão para enviar o formulário. Mas e se o usuário não preencher algum campo? Nesse caso, devemos informá-lo de que é um campo obrigatório. Conseguimos isso com o método `setRequired()`. Finalmente, adicionamos também o [evento |nette:glossary#Eventos] `onSuccess`, que é acionado se o formulário for enviado com sucesso. No nosso caso, ele chama o método `contactFormSucceeded`, que cuidará do processamento do formulário enviado. Adicionaremos isso ao código em um momento. - -Deixaremos o componente `contactForm` ser renderizado no template `Home/default.latte`: - -```latte -{block content} -<h1>Formulário de Contato</h1> -{control contactForm} -``` - -Para o envio do e-mail em si, criaremos uma nova classe, que chamaremos de `ContactFacade` e a colocaremos no arquivo `app/Model/ContactFacade.php`: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // seu e-mail - ->setFrom($email, $name) - ->setSubject('Mensagem do formulário de contato') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -O método `sendMessage()` cria e envia o e-mail. Ele usa o chamado mailer para isso, que ele recebe como dependência através do construtor. Leia mais sobre [envio de e-mails |mail:]. - -Agora voltaremos ao presenter e finalizaremos o método `contactFormSucceeded()`. Ele chamará o método `sendMessage()` da classe `ContactFacade` e passará os dados do formulário para ele. E como obtemos o objeto `ContactFacade`? Vamos recebê-lo através do construtor: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('A mensagem foi enviada'); - $this->redirect('this'); - } -} -``` - -Depois que o e-mail for enviado, ainda exibiremos ao usuário a chamada [flash message |application:components#Mensagens Flash], confirmando que a mensagem foi enviada, e depois redirecionaremos para a mesma página (usando `this`), para que não seja possível reenviar o formulário usando *refresh* no navegador. - - -Então, se tudo funcionar, você deve ser capaz de enviar um e-mail do seu formulário de contato. Parabéns! - - -Template HTML do e-mail ------------------------ - -Até agora, um e-mail de texto simples está sendo enviado, contendo apenas a mensagem enviada pelo formulário. Mas no e-mail, podemos usar HTML e tornar sua aparência mais atraente. Criaremos um template em Latte para ele, que escreveremos em `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Mensagem do formulário de contato - - -

    Nome: {$name}

    -

    E-mail: {$email}

    -

    Mensagem: {$message}

    - - -``` - -Resta modificar o `ContactFacade`, para usar este template. No construtor, solicitaremos a classe `LatteFactory`, que pode criar um objeto `Latte\Engine`, ou seja, o [renderizador de templates Latte |latte:develop#Como renderizar um template]. Usando o método `renderToString()`, renderizamos o template para uma string, o primeiro parâmetro é o caminho para o template e o segundo são as variáveis. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // seu e-mail - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -O e-mail HTML gerado é então passado para o método `setHtmlBody()` em vez do original `setBody()`. Da mesma forma, não precisamos especificar o assunto do e-mail em `setSubject()`, pois a biblioteca o pegará do elemento `` do template. - - -Configuração ------------- - -No código da classe `ContactFacade`, nosso e-mail de administrador `admin@example.com` ainda está codificado. Seria melhor movê-lo para o arquivo de configuração. Como fazer isso? - -Primeiro, modificamos a classe `ContactFacade` e substituímos a string com o e-mail por uma variável passada pelo construtor: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -E o segundo passo é especificar o valor desta variável na configuração. No arquivo `app/config/services.neon`, escrevemos: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -E está feito. Se houvesse muitos itens na seção `services` e você sentisse que o e-mail se perde entre eles, podemos transformá-lo em um parâmetro. Modificamos a entrada para: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -E no arquivo `app/config/common.neon`, definimos esta variável: - -```neon -parameters: - adminEmail: admin@example.com -``` - -E está pronto! diff --git a/best-practices/pt/microsites.texy b/best-practices/pt/microsites.texy deleted file mode 100644 index 92abd0db15..0000000000 --- a/best-practices/pt/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Como criar micro-sites -********************** - -Imagine que você precisa criar rapidamente um pequeno site para o próximo evento da sua empresa. Deve ser simples, rápido e sem complicações desnecessárias. Você pode pensar que para um projeto tão pequeno não precisa de um framework robusto. Mas e se o uso do framework Nette puder simplificar e acelerar fundamentalmente esse processo? - -Afinal, mesmo ao criar sites simples, você não quer abrir mão do conforto. Você não quer reinventar o que já foi resolvido uma vez. Sinta-se à vontade para ser preguiçoso e deixe-se mimar. O Nette Framework pode ser perfeitamente usado também como um micro framework. - -Como pode ser um microsite assim? Por exemplo, colocando todo o código do site em um único arquivo `index.php` na pasta pública: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// cria o contêiner de DI com base na configuração em config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// definimos o roteamento -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// rota para a URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // detectamos o idioma do navegador e redirecionamos para a URL /en ou /de etc. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// rota para a URL https://example.com/cs ou https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // exibimos o template correspondente, por exemplo ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// execute a aplicação! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Todo o resto serão templates armazenados na pasta pai `/templates`. - -O código PHP em `index.php` primeiro [prepara o ambiente |bootstrap:], depois define as [rotas |application:routing#Roteamento dinâmico com callbacks] e finalmente executa a aplicação. A vantagem é que o segundo parâmetro da função `addRoute()` pode ser um callable, que será executado após a abertura da página correspondente. - - -Por que usar Nette para microsites? ------------------------------------ - -- Programadores que já experimentaram o [Tracy|tracy:] hoje não conseguem imaginar programar algo sem ele. -- Acima de tudo, você usará o sistema de templates [Latte|latte:], porque a partir de 2 páginas você vai querer ter o [layout e conteúdo|latte:template-inheritance] separados. -- E você definitivamente quer confiar no [escaping automático |latte:safety-first] para evitar a vulnerabilidade XSS. -- O Nette também garante que, em caso de erro, nunca sejam exibidas mensagens de erro de programação PHP, mas sim uma página compreensível para o usuário. -- Se você quiser obter feedback dos usuários, por exemplo, na forma de um formulário de contato, você ainda adicionará [formulários|forms:] e [banco de dados|database:]. -- Você também pode facilmente [enviar por e-mail|mail:] os formulários preenchidos. -- Às vezes, pode ser útil usar [cache|caching:], por exemplo, se você baixa e exibe feeds. - -Nos dias de hoje, onde a velocidade e a eficiência são cruciais, é importante ter ferramentas que permitam alcançar resultados sem atrasos desnecessários. O framework Nette oferece exatamente isso - desenvolvimento rápido, segurança e uma ampla gama de ferramentas, como Tracy e Latte, que simplificam o processo. Basta instalar alguns pacotes Nette e construir tal microsite torna-se de repente uma brincadeira de criança. E você sabe que não há nenhuma falha de segurança escondida em lugar nenhum. diff --git a/best-practices/pt/pagination.texy b/best-practices/pt/pagination.texy deleted file mode 100644 index a1ec4b1351..0000000000 --- a/best-practices/pt/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Paginação de resultados do banco de dados -***************************************** - -.[perex] -Ao criar aplicações web, você frequentemente encontrará a exigência de limitar o número de itens exibidos por página, ou seja, implementar a paginação. - -Partiremos do estado em que exibimos todos os dados sem paginação. Para selecionar dados do banco de dados, temos a classe `ArticleRepository`, que, além do construtor, contém o método `findPublishedArticles`, que retorna todos os artigos publicados ordenados decrescentemente pela data de publicação. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -No presenter, injetamos a classe do modelo e no método render solicitamos os artigos publicados, que passamos para o template: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -No template `default.latte`, cuidamos da exibição dos artigos: - -```latte -{block content} -<h1>Artigos</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Desta forma, podemos exibir todos os artigos, o que, no entanto, começará a causar problemas quando o número de artigos aumentar. Nesse momento, a implementação de um mecanismo de paginação se torna útil. - -Ele garantirá que todos os artigos sejam divididos em várias páginas e exibiremos apenas os artigos da página atual. O número total de páginas e a divisão dos artigos serão calculados pelo [Paginator|utils:paginator] com base em quantos artigos temos no total e quantos artigos por página queremos exibir. - -No primeiro passo, modificamos o método para obter artigos na classe do repositório para que ele possa retornar apenas artigos para uma página. Também adicionamos um método para descobrir o número total de artigos no banco de dados, que precisaremos para configurar o Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $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, - ); - } - - /** - * Retorna o número total de artigos publicados - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Em seguida, começamos a modificar o presenter. Passaremos o número da página atualmente exibida para o método render. Caso este número não faça parte da URL, definiremos o valor padrão da primeira página (`1`). - -Também estenderemos o método render para obter a instância do Paginator, configurá-lo e selecionar os artigos corretos para exibição no template. O `HomePresenter` ficará assim após as modificações: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Descobrimos o número total de artigos publicados - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Criamos uma instância do Paginator e a configuramos - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // número total de artigos - $paginator->setItemsPerPage(10); // número de itens por página - $paginator->setPage($page); // número da página atual - - // Do banco de dados, extraímos um conjunto limitado de artigos de acordo com o cálculo do Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // que passamos para o template - $this->template->articles = $articles; - // e também o próprio Paginator para exibir as opções de paginação - $this->template->paginator = $paginator; - } -} -``` - -O template agora itera apenas sobre os artigos de uma página, basta adicionar os links de paginação: - -```latte -{block content} -<h1>Artigos</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Primeira</a> -  |  - <a n:href="default, $paginator->page-1">Anterior</a> -  |  - {/if} - - Página {$paginator->getPage()} de {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Próxima</a> -  |  - <a n:href="default, $paginator->getPageCount()">Última</a> - {/if} -</div> -``` - - -Assim, adicionamos a opção de paginação à página usando o Paginator. Caso, em vez do [Nette Database Core |database:sql-way], usemos o [Nette Database Explorer |database:explorer], somos capazes de implementar a paginação de forma ainda mais simples. A classe `Nette\Database\Table\Selection` contém o método [page() |api:Nette\Database\Table\Selection::page()] que encapsula a lógica de paginação. - -O repositório ficará assim com este método de implementação: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -No presenter, não precisamos criar o Paginator, usamos diretamente o método `page()` da `Selection` retornada pelo repositório: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Extraímos os artigos publicados - $articles = $this->articleRepository->findPublishedArticles(); - - // e para o template enviamos apenas sua parte limitada de acordo com o cálculo do método page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // e também os dados necessários para exibir as opções de paginação - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Como agora não enviamos o Paginator para o template, modificamos a parte que exibe os links de paginação: - -```latte -{block content} -<h1>Artigos</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Primeira</a> -  |  - <a n:href="default, $page - 1">Anterior</a> -  |  - {/if} - - Página {$page} de {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Próxima</a> -  |  - <a n:href="default, $lastPage">Última</a> - {/if} -</div> -``` - -Desta forma, implementamos o mecanismo de paginação usando o Nette Database Explorer sem a necessidade explícita do Paginator. - -{{priority: -1}} diff --git a/best-practices/pt/passing-settings-to-presenters.texy b/best-practices/pt/passing-settings-to-presenters.texy deleted file mode 100644 index a9f8a66798..0000000000 --- a/best-practices/pt/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Passando configurações para presenters -************************************** - -.[perex] -Você precisa passar argumentos para presenters que não são objetos (por exemplo, informação se está rodando em modo debug, caminhos para diretórios, etc.), e portanto não podem ser passados automaticamente via autowiring? A solução é encapsulá-los em um objeto `Settings`. - -O serviço `Settings` representa uma maneira muito fácil e útil de fornecer informações sobre a aplicação em execução aos presenters. Sua forma específica depende puramente de suas necessidades particulares. Exemplo: - -```php -namespace App; - -class Settings -{ - public function __construct( - // a partir do PHP 8.1 é possível usar readonly - public bool $debugMode, - public string $appDir, - // e assim por diante - ) {} -} -``` - -Exemplo de registro na configuração: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Quando um presenter precisar das informações fornecidas por este serviço, ele simplesmente as solicitará no construtor: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/pt/post-links.texy b/best-practices/pt/post-links.texy deleted file mode 100644 index b1ddb720ee..0000000000 --- a/best-practices/pt/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Como usar corretamente links POST -********************************* - -.[perex] -Em aplicações web, especialmente em interfaces administrativas, deve ser uma regra básica que ações que alteram o estado do servidor não devem ser realizadas através do método HTTP GET. Como o nome do método sugere, GET deve ser usado apenas para obter dados, não para alterá-los. Para ações como excluir registros, é mais apropriado usar o método POST. Embora o ideal fosse o método DELETE, ele não pode ser invocado sem JavaScript, por isso historicamente se usa POST. - -Como fazer isso na prática? Use este truque simples. No início do template, crie um formulário auxiliar com o identificador `postForm`, que você usará posteriormente para os botões de exclusão: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Graças a este formulário, você pode usar um botão `<button>` em vez de um link `<a>` clássico, que pode ser estilizado visualmente para parecer um link comum. Por exemplo, o framework CSS Bootstrap oferece as classes `btn btn-link` com as quais você pode garantir que o botão não seja visualmente diferente de outros links. Usando o atributo `form="postForm"`, nós o vinculamos ao formulário pré-preparado: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">delete</button> - <!-- em vez de <a n:href="delete $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -Ao clicar no link, a ação `delete` agora é invocada. Para garantir que as requisições sejam aceitas apenas através do método POST e do mesmo domínio (o que é uma defesa eficaz contra ataques CSRF), use o atributo `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // código hipotético que exclui o registro - $this->redirect('default'); - } -} -``` - -O atributo existe desde o Nette Application 3.2 e você pode aprender mais sobre suas possibilidades na página [Como usar o atributo #Requires |attribute-requires]. - -Se você estivesse usando o sinal `handleDelete()` em vez da ação `actionDelete()`, não seria necessário especificar `sameOrigin: true`, pois os sinais têm essa proteção definida implicitamente: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Esta abordagem não só melhora a segurança da sua aplicação, mas também contribui para a adesão aos padrões e práticas corretas da web. Ao utilizar métodos POST para ações que alteram o estado, você alcançará uma aplicação mais robusta e segura. diff --git a/best-practices/pt/presenter-traits.texy b/best-practices/pt/presenter-traits.texy deleted file mode 100644 index 0a6ad31278..0000000000 --- a/best-practices/pt/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Compondo presenters a partir de traits -************************************** - -.[perex] -Se precisarmos implementar o mesmo código em vários presenters (por exemplo, verificar se o usuário está logado), uma opção é colocar o código em um ancestral comum. A segunda opção é criar [traits |nette:introduction-to-object-oriented-programming#Traits] de propósito único. - -A vantagem desta solução é que cada presenter pode usar exatamente as traits que realmente precisa, enquanto a herança múltipla não é possível em PHP. - -Essas traits podem aproveitar o fato de que, ao criar um presenter, todos os [métodos inject |inject-method-attribute#Métodos inject] são chamados sequencialmente. É apenas necessário garantir que o nome de cada método inject seja único. - -As traits podem anexar código de inicialização aos eventos [onStartup ou onRender |application:presenters#Eventos]. - -Exemplos: - -```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); - }; - } -} -``` - -O presenter então simplesmente usa essas traits: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/pt/restore-request.texy b/best-practices/pt/restore-request.texy deleted file mode 100644 index c46f2ecb8d..0000000000 --- a/best-practices/pt/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Como retornar a uma página anterior? -************************************ - -.[perex] -E se o usuário estiver preenchendo um formulário e sua sessão expirar? Para que ele não perca os dados, antes de redirecionar para a página de login, salvamos a requisição atual na sessão. No Nette, isso é muito fácil. - -A requisição atual pode ser salva na sessão usando o método `storeRequest()`, que retorna seu identificador na forma de uma string curta. O método salva o nome do presenter atual, a view e seus parâmetros. Caso um formulário também tenha sido enviado, o conteúdo dos campos também é salvo (com exceção dos arquivos enviados por upload). - -A restauração da requisição é feita pelo método `restoreRequest($key)`, ao qual passamos o identificador obtido. Ele redireciona para o presenter e view originais. No entanto, se a requisição salva contiver o envio de um formulário, ele vai para o presenter original usando o método `forward()`, passa os valores preenchidos anteriormente para o formulário e o renderiza novamente. O usuário tem assim a possibilidade de reenviar o formulário e nenhum dado é perdido. - -Importante: `restoreRequest()` verifica se o usuário recém-logado é o mesmo que preencheu o formulário originalmente. Se não for, ele descarta a requisição e não faz nada para evitar vazamento de dados. - -Vamos mostrar tudo com um exemplo. Temos um presenter `AdminPresenter`, no qual os dados são editados e em cujo método `startup()` verificamos se o usuário está logado. Se não estiver, o redirecionamos para `SignPresenter`. Ao mesmo tempo, salvamos a requisição atual e enviamos sua chave para `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()]); - } - } -} -``` - -O presenter `SignPresenter` conterá, além do formulário de login, também um parâmetro persistente `$backlink`, no qual a chave será escrita. Como o parâmetro é persistente, ele será transmitido mesmo após o envio do formulário de login. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... adicionamos os campos do formulário ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... aqui fazemos o login do usuário ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Passamos a chave da requisição salva para o método `restoreRequest()` e ele redireciona (ou avança) para o presenter original. - -No entanto, se a chave for inválida (por exemplo, não existir mais na sessão), o método não faz nada. Segue-se então a chamada `$this->redirect('Admin:')`, que redireciona para `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/ro/@home.texy b/best-practices/ro/@home.texy deleted file mode 100644 index a742e5d015..0000000000 --- a/best-practices/ro/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Tutoriale și proceduri -********************** - -.[perex] -Tutoriale, soluții pentru sarcini frecvente și *best practices* pentru Nette. - - -<div class=documentation> -<div> - - -Aplicații Nette ---------------- -- [Metode și atribute inject |inject-method-attribute] -- [Compunerea presenterilor din trait-uri |presenter-traits] -- [Transmiterea setărilor către presenteri |passing-settings-to-presenters] -- [Cum să reveniți la pagina anterioară |restore-request] -- [Paginarea rezultatelor bazei de date |pagination] -- [Snippete dinamice |dynamic-snippets] -- [Cum să utilizați atributul #Requires |attribute-requires] -- [Cum să utilizați corect linkurile POST |post-links] - -</div> -<div> - - -Formulare ---------- -- [Reutilizarea formularelor |form-reuse] -- [Formular pentru crearea și editarea înregistrărilor |creating-editing-form] -- [Creăm un formular de contact |lets-create-contact-form] -- [Selectbox-uri dependente |https://blog.nette.org/ro/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Generale --------- -- [Cum să încărcați un fișier de configurare |bootstrap:] -- [Cum să scrieți micro-site-uri |microsites] -- [De ce Nette utilizează notația PascalCase pentru constante? |https://blog.nette.org/ro/for-less-screaming-in-the-code] -- [De ce Nette nu utilizează sufixul Interface? |https://blog.nette.org/ro/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: sfaturi de utilizare |composer] -- [Sfaturi pentru editori & instrumente |editors-and-tools] -- [Introducere în programarea orientată pe obiecte |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Soluții exemplu ---------------- -- [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:] - -</div> -<div> - - -Videoclipuri ------------- -Sute de înregistrări de la Ultimele Sâmbete și videoclipuri despre Nette pot fi găsite sub un singur acoperiș pe "Canalul Youtube Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/ro/@meta.texy b/best-practices/ro/@meta.texy deleted file mode 100644 index 738844dc28..0000000000 --- a/best-practices/ro/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Tutoriale și proceduri}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/ro/attribute-requires.texy b/best-practices/ro/attribute-requires.texy deleted file mode 100644 index 26bc6f5e2b..0000000000 --- a/best-practices/ro/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Cum se utilizează atributul `#[Requires]` -***************************************** - -.[perex] -Când scrieți o aplicație web, adesea vă confruntați cu nevoia de a restricționa accesul la anumite părți ale aplicației dvs. Poate doriți ca unele cereri să poată trimite date doar folosind un formular (adică prin metoda POST), sau să fie accesibile doar pentru apeluri AJAX. În Nette Framework 3.2 a apărut un nou instrument care vă permite să setați astfel de restricții foarte elegant și clar: atributul `#[Requires]`. - -Atributul este o marcă specială în PHP, pe care o adăugați înaintea definiției unei clase sau metode. Deoarece este de fapt o clasă, pentru ca următoarele exemple să funcționeze, este necesar să specificați clauza use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Atributul `#[Requires]` îl puteți utiliza la clasa presenterului însuși și, de asemenea, la aceste metode: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Ultimele două metode se referă și la componente, deci atributul îl puteți utiliza și la ele. - -Dacă nu sunt îndeplinite condițiile specificate de atribut, se va declanșa o eroare HTTP 4xx. - - -Metode HTTP ------------ - -Puteți specifica ce metode HTTP (cum ar fi GET, POST etc.) sunt permise pentru acces. De exemplu, dacă doriți să permiteți accesul doar prin trimiterea unui formular, setați: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -De ce ar trebui să utilizați POST în loc de GET pentru acțiunile care modifică starea și cum să faceți asta? [Citiți ghidul |post-links]. - -Puteți specifica o metodă sau un array de metode. Un caz special este valoarea `'*'`, care permite toate metodele, ceea ce presenterele standard nu permit din [motive de securitate |application:presenters#Verificarea metodei HTTP]. - - -Apel AJAX ---------- - -Dacă doriți ca presenterul sau metoda să fie disponibilă doar pentru cereri AJAX, utilizați: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Aceeași origine ---------------- - -Pentru a crește securitatea, puteți solicita ca cererea să fie făcută din același domeniu. Astfel preveniți [vulnerabilitatea CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Pentru metodele `handle<Signal>()`, accesul din același domeniu este solicitat automat. Deci, dacă, dimpotrivă, doriți să permiteți accesul din orice domeniu, specificați: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Acces prin forward ------------------- - -Uneori este util să restricționați accesul la presenter astfel încât să fie disponibil doar indirect, de exemplu folosind metoda `forward()` sau `switch()` dintr-un alt presenter. Astfel se protejează, de exemplu, error-presenterele, pentru a nu putea fi apelate din URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -În practică, este adesea necesar să se marcheze anumite view-uri, la care se poate ajunge doar pe baza logicii din presenter. Adică, din nou, pentru a nu putea fi deschise direct: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Acțiuni specifice ------------------ - -Puteți, de asemenea, să restricționați ca un anumit cod, de exemplu crearea unei componente, să fie disponibil doar pentru acțiuni specifice în presenter: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -În cazul unei singure acțiuni, nu este necesar să scrieți un array: `#[Requires(actions: 'default')]` - - -Atribute personalizate ----------------------- - -Dacă doriți să utilizați atributul `#[Requires]` în mod repetat cu aceleași setări, puteți crea propriul atribut, care va moșteni `#[Requires]` și îl va seta conform nevoilor. - -De exemplu, `#[SingleAction]` va permite accesul doar prin acțiunea `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Sau `#[RestMethods]` va permite accesul prin toate metodele HTTP utilizate pentru API-uri REST: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Concluzie ---------- - -Atributul `#[Requires]` vă oferă o mare flexibilitate și control asupra modului în care paginile dvs. web sunt accesibile. Folosind reguli simple, dar puternice, puteți crește securitatea și funcționarea corectă a aplicației dvs. După cum vedeți, utilizarea atributelor în Nette vă poate nu numai ușura munca, ci și securiza. diff --git a/best-practices/ro/composer.texy b/best-practices/ro/composer.texy deleted file mode 100644 index 2e63994768..0000000000 --- a/best-practices/ro/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: sfaturi de utilizare -****************************** - -<div class=perex> - -Composer este un instrument pentru gestionarea dependențelor în PHP. Ne permite să enumerăm bibliotecile de care depinde proiectul nostru și le va instala și actualiza pentru noi. Vom arăta: - -- cum se instalează Composer -- utilizarea sa într-un proiect nou sau existent - -</div> - - -Instalare -========= - -Composer este un fișier executabil `.phar`, pe care îl descărcați și instalați în felul următor: - - -Windows -------- - -Utilizați instalatorul oficial [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Sunt suficiente 4 comenzi, pe care le copiați de pe [această pagină |https://getcomposer.org/download/]. - -Apoi, prin plasarea în directorul care se află în `PATH`-ul sistemului, Composer devine accesibil global: - -```shell -$ mv ./composer.phar ~/bin/composer # sau /usr/local/bin/composer -``` - - -Utilizare în proiect -==================== - -Pentru a putea începe să utilizați Composer în proiectul dvs., aveți nevoie doar de fișierul `composer.json`. Acesta descrie dependențele proiectului nostru și poate conține și alte metadate. Un `composer.json` de bază poate arăta deci astfel: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Spunem aici că aplicația noastră (sau biblioteca) necesită pachetul `nette/database` (numele pachetului este format din numele organizației și numele proiectului) și dorește o versiune care corespunde condiției `^3.0` (adică cea mai recentă versiune 3). - -Avem deci în rădăcina proiectului fișierul `composer.json` și rulăm instalarea: - -```shell -composer update -``` - -Composer va descărca Nette Database în directorul `vendor/`. Apoi va crea fișierul `composer.lock`, care conține informații despre ce versiuni exacte ale bibliotecilor a instalat. - -Composer generează fișierul `vendor/autoload.php`, pe care îl putem include simplu și începe să folosim bibliotecile fără nicio altă muncă: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Actualizarea pachetelor la cele mai recente versiuni -==================================================== - -Actualizarea bibliotecilor utilizate la cele mai recente versiuni conform condițiilor definite în `composer.json` este responsabilitatea comenzii `composer update`. De ex., pentru dependența `"nette/database": "^3.0"`, va instala cea mai recentă versiune 3.x.x, dar nu și versiunea 4. - -Pentru a actualiza condițiile din fișierul `composer.json`, de exemplu la `"nette/database": "^4.1"`, pentru a putea instala cea mai recentă versiune, utilizați comanda `composer require nette/database`. - -Pentru a actualiza toate pachetele Nette utilizate, ar fi necesar să le enumerați pe toate în linia de comandă, de ex.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Ceea ce este nepractic. Utilizați, prin urmare, scriptul simplu "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, care va face asta pentru dvs.: - -```shell -php composer-frontline.php -``` - - -Crearea unui proiect nou -======================== - -Un proiect nou pe Nette îl creați folosind o singură comandă: - -```shell -composer create-project nette/web-project nume-proiect -``` - -Ca `nume-proiect` introduceți numele directorului pentru proiectul dvs. și confirmați. Composer va descărca repository-ul `nette/web-project` de pe GitHub, care conține deja fișierul `composer.json`, și imediat după aceea Nette Framework. Ar trebui să fie suficient doar să [setați permisiunile |nette:troubleshooting#Setarea permisiunilor pentru directoare] de scriere pentru directoarele `temp/` și `log/` și proiectul ar trebui să prindă viață. - -Dacă știți pe ce versiune de PHP va fi găzduit proiectul, nu uitați [să o setați |#Versiunea PHP]. - - -Versiunea PHP -============= - -Composer instalează întotdeauna acele versiuni de pachete care sunt compatibile cu versiunea de PHP pe care o utilizați în prezent (mai precis, cu versiunea de PHP utilizată în linia de comandă la rularea Composerului). Ceea ce, însă, probabil nu este aceeași versiune pe care o utilizează găzduirea dvs. De aceea, este foarte important să adăugați în fișierul `composer.json` informații despre versiunea PHP de pe găzduire. Apoi se vor instala doar versiuni de pachete compatibile cu găzduirea. - -Faptul că proiectul va rula, de exemplu, pe PHP 8.2.3, îl setăm cu comanda: - -```shell -composer config platform.php 8.2.3 -``` - -Astfel se va scrie versiunea în fișierul `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Cu toate acestea, numărul versiunii PHP se specifică și în alt loc al fișierului, și anume în secțiunea `require`. În timp ce primul număr specifică pentru ce versiune se vor instala pachetele, al doilea număr spune pentru ce versiune este scrisă aplicația însăși. Și conform acestuia, de exemplu, PhpStorm setează *PHP language level*. (Desigur, nu are sens ca aceste versiuni să difere, deci dubla scriere este o neglijență.) Această versiune o setați cu comanda: - -```shell -composer require php 8.2.3 --no-update -``` - -Sau direct în fișierul `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignorarea versiunii PHP -======================= - -Pachetele au de obicei specificată atât cea mai mică versiune de PHP cu care sunt compatibile, cât și cea mai mare cu care sunt testate. Dacă intenționați să utilizați o versiune de PHP și mai nouă, de exemplu din motive de testare, Composer va refuza să instaleze un astfel de pachet. Soluția este opțiunea `--ignore-platform-req=php+`, care face ca Composer să ignore limitele superioare ale versiunii PHP solicitate. - - -Mesaje false -============ - -La actualizarea pachetelor sau modificarea numerelor de versiuni, se întâmplă să apară conflicte. Un pachet are cerințe care sunt în contradicție cu altul și altele asemenea. Composer, însă, uneori afișează mesaje false. Raportează un conflict care în realitate nu există. În acest caz, ajută ștergerea fișierului `composer.lock` și încercarea din nou. - -Dacă mesajul de eroare persistă, atunci este serios și trebuie să citiți din el ce și cum să modificați. - - -Packagist.org - repository central -================================== - -[Packagist |https://packagist.org] este repository-ul principal în care Composer încearcă să caute pachete, dacă nu îi spunem altfel. Putem publica aici și propriile pachete. - - -Ce facem dacă nu vrem să folosim repository-ul central? -------------------------------------------------------- - -Dacă avem aplicații interne ale companiei, pe care pur și simplu nu le putem găzdui public, atunci ne creăm pentru ele un repository al companiei. - -Mai multe despre subiectul repository-urilor [în documentația oficială |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Autoloading -=========== - -O caracteristică esențială a Composerului este că oferă autoloading pentru toate clasele instalate de el, pe care îl porniți prin includerea fișierului `vendor/autoload.php`. - -Cu toate acestea, este posibil să utilizați Composer și pentru încărcarea altor clase și în afara directorului `vendor`. Prima opțiune este să lăsați Composer să caute în directoarele și subdirectoarele definite, să găsească toate clasele și să le includă în autoloader. Acest lucru se realizează prin setarea `autoload > classmap` în `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # include directorul src/ și subdirectoarele sale - ] - } -} -``` - -Ulterior, este necesar la fiecare modificare să rulați comanda `composer dumpautoload` și să lăsați tabelele de autoloading să se regenereze. Acest lucru este extrem de incomod și mult mai bine este să încredințați această sarcină [RobotLoaderului |robot-loader:], care efectuează aceeași activitate automat în fundal și mult mai rapid. - -A doua opțiune este să respectați [PSR-4 |https://www.php-fig.org/psr/psr-4/]. Simplificat spus, este vorba despre un sistem în care spațiile de nume și numele claselor corespund structurii directoarelor și numelor fișierelor, adică, de ex., `App\Core\RouterFactory` va fi în fișierul `/path/to/App/Core/RouterFactory.php`. Exemplu de configurare: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # spațiul de nume App\ este în directorul app/ - } - } -} -``` - -Cum să configurați exact comportamentul veți afla în [documentația Composerului |https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Testarea versiunilor noi -======================== - -Doriți să testați o nouă versiune de dezvoltare a unui pachet. Cum să faceți asta? Mai întâi, adăugați în fișierul `composer.json` această pereche de opțiuni, care permite instalarea versiunilor de dezvoltare ale pachetelor, însă recurge la aceasta doar în cazul în care nu există nicio combinație de versiuni stabile care să satisfacă cerințele: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Apoi, recomandăm ștergerea fișierului `composer.lock`, uneori Composer refuză inexplicabil instalarea și acest lucru rezolvă problema. - -Să presupunem că este vorba despre pachetul `nette/utils` și noua versiune are numărul 4.0. O instalați cu comanda: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Sau puteți instala o versiune specifică, de exemplu 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Dar dacă de bibliotecă depinde un alt pachet, care este blocat la o versiune mai veche (de ex. `^3.1`), atunci este ideal să actualizați pachetul, pentru a funcționa cu noua versiune. Dacă însă doriți doar să ocoliți restricția și să forțați Composer să instaleze versiunea de dezvoltare și să pretindă că este o versiune mai veche (de ex. 3.1.6), puteți utiliza cuvântul cheie `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Apelarea comenzilor -=================== - -Prin Composer se pot apela comenzi și scripturi proprii pre-pregătite, ca și cum ar fi comenzi native ale Composerului. Pentru scripturile care se află în directorul `vendor/bin`, nu este necesar să specificați acest director. - -Ca exemplu, definim în fișierul `composer.json` un script care, folosind [Nette Tester |tester:], rulează testele: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Testele le rulăm apoi folosind `composer tester`. Comanda o putem apela și în cazul în care nu ne aflăm în directorul rădăcină al proiectului, ci într-un subdirector. - - -Trimiteți mulțumiri -=================== - -Vă vom arăta un truc prin care veți bucura autorii de open source. Într-un mod simplu, dați o stea pe GitHub bibliotecilor pe care proiectul dvs. le utilizează. Este suficient să instalați biblioteca `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -Și apoi să rulați: - -```shell -composer thanks -``` - -Încercați! - - -Configurare -=========== - -Composer este strâns legat de instrumentul de versionare [Git |https://git-scm.com]. Dacă nu îl aveți instalat, trebuie să îi spuneți Composerului să nu îl utilizeze: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/ro/creating-editing-form.texy b/best-practices/ro/creating-editing-form.texy deleted file mode 100644 index 8c58fc221a..0000000000 --- a/best-practices/ro/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Formular pentru crearea și editarea înregistrărilor -*************************************************** - -.[perex] -Cum să implementăm corect adăugarea și editarea unei înregistrări în Nette, folosind același formular pentru ambele operațiuni? - -În multe cazuri, formularele pentru adăugarea și editarea înregistrărilor sunt identice, diferind poate doar prin eticheta butonului. Vom prezenta exemple de presenteri simpli, unde vom folosi formularul mai întâi pentru adăugarea unei înregistrări, apoi pentru editare și, în final, vom combina ambele soluții. - - -Adăugarea unei înregistrări ---------------------------- - -Exemplu de presenter utilizat pentru adăugarea unei înregistrări. Vom lăsa lucrul efectiv cu baza de date în seama clasei `Facade`, al cărei cod nu este esențial pentru exemplu. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... adăugăm câmpurile formularului ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // adăugarea înregistrării în baza de date - $this->flashMessage('Adăugat cu succes'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Editarea unei înregistrări --------------------------- - -Acum vom arăta cum ar arăta un presenter utilizat pentru editarea unei înregistrări: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // verificarea existenței înregistrării - || !$this->facade->isEditAllowed(/*...*/) // verificarea permisiunilor - ) { - $this->error(); // eroare 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // verificăm dacă acțiunea este 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... adăugăm câmpurile formularului ... - - $form->setDefaults($this->record); // setarea valorilor implicite - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // actualizarea înregistrării - $this->flashMessage('Actualizat cu succes'); - $this->redirect('...'); - } -} -``` - -În metoda *action*, care se execută chiar la începutul [ciclului de viață al presenterului |application:presenters#Ciclul de viață al presenterului], verificăm existența înregistrării și permisiunea utilizatorului de a o edita. - -Salvăm înregistrarea în proprietatea `$record`, pentru a o avea disponibilă în metoda `createComponentRecordForm()` pentru setarea valorilor implicite și în `recordFormSucceeded()` pentru ID. O soluție alternativă ar fi setarea valorilor implicite direct în `actionEdit()` și obținerea valorii ID-ului, care face parte din URL, folosind `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // verificarea existenței și controlul permisiunilor - ) { - $this->error(); - } - - // setarea valorilor implicite ale formularului - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Cu toate acestea, și aceasta ar trebui să fie **cea mai importantă concluzie a întregului cod**, trebuie să ne asigurăm la crearea formularului că acțiunea este într-adevăr `edit`. Altfel, verificarea din metoda `actionEdit()` nu ar avea loc deloc! - - -Același formular pentru adăugare și editare -------------------------------------------- - -Și acum vom combina ambii presenteri într-unul singur. Fie am putea distinge în metoda `createComponentRecordForm()` despre ce acțiune este vorba și să configurăm formularul în consecință, fie putem lăsa acest lucru direct pe seama metodelor action și să scăpăm de condiție: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private 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 // verificarea existenței înregistrării - || !$this->facade->isEditAllowed(/*...*/) // verificarea permisiunilor - ) { - $this->error(); // eroare 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // setarea valorilor implicite - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // verificăm dacă acțiunea este 'add' sau 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... adăugăm câmpurile formularului ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // adăugarea înregistrării în baza de date - $this->flashMessage('Adăugat cu succes'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // actualizarea înregistrării - $this->flashMessage('Actualizat cu succes'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/ro/dynamic-snippets.texy b/best-practices/ro/dynamic-snippets.texy deleted file mode 100644 index f6879f7488..0000000000 --- a/best-practices/ro/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Snippets dinamice -***************** - -Destul de des, în timpul dezvoltării aplicațiilor, apare nevoia de a efectua operațiuni AJAX, de exemplu, pe rândurile individuale ale unui tabel sau pe elementele unei liste. Ca exemplu, putem alege afișarea articolelor, permițând fiecărui utilizator autentificat să aleagă evaluarea "îmi place/nu-mi place". Codul presenterului și șablonul corespunzător fără AJAX vor arăta aproximativ astfel (prezint cele mai importante fragmente, codul presupune existența unui serviciu pentru marcarea evaluărilor și obținerea colecției de articole - implementarea specifică nu este importantă pentru scopul acestui ghid): - -```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'); -} -``` - -Șablon: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>îmi place</a> - {else} - <a n:href="unlike! $article->id" class=ajax>nu-mi mai place</a> - {/if} -</article> -``` - - -Ajaxizare -========= - -Să echipăm acum această aplicație simplă cu AJAX. Schimbarea evaluării unui articol nu este atât de importantă încât să necesite o redirecționare, așa că ideal ar fi să se desfășoare prin AJAX în fundal. Vom folosi [scriptul de ajutor din add-on-uri |application:ajax#Naja] cu convenția obișnuită că linkurile AJAX au clasa CSS `ajax`. - -Dar cum facem asta concret? Nette oferă 2 căi: calea așa-numitelor snippets dinamice și calea componentelor. Ambele au avantaje și dezavantaje, așa că le vom prezenta pe rând. - - -Calea snippetelor dinamice -========================== - -Un snippet dinamic înseamnă, în terminologia Latte, un caz specific de utilizare a tag-ului `{snippet}`, unde în numele snippetului este folosită o variabilă. Un astfel de snippet nu poate fi găsit oriunde în șablon - trebuie să fie încapsulat într-un snippet static, adică unul obișnuit, sau în interiorul `{snippetArea}`. Am putea modifica șablonul nostru astfel: - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>îmi place</a> - {else} - <a n:href="unlike! $article->id" class=ajax>nu-mi mai place</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Fiecare articol definește acum un snippet care are ID-ul articolului în nume. Toate aceste snippets sunt apoi împachetate împreună într-un singur snippet cu numele `articlesContainer`. Dacă am omite acest snippet încapsulator, Latte ne-ar avertiza cu o excepție. - -Ne rămâne să adăugăm redesenarea în presenter - este suficient să redesenăm învelișul static. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- nu este necesar - } else { - $this->redirect('this'); - } -} -``` - -Modificăm în mod similar și metoda soră `handleUnlike()`, iar AJAX-ul este funcțional! - -Soluția are însă un dezavantaj. Dacă am examina mai atent cum decurge cererea AJAX, am descoperi că, deși aplicația pare economică la exterior (returnează doar un singur snippet pentru articolul respectiv), în realitate, pe server, a redat toate snippet-urile. Snippet-ul dorit a fost plasat în payload, iar celelalte au fost aruncate (deci au fost obținute inutil din baza de date). - -Pentru a optimiza acest proces, va trebui să intervenim acolo unde transmitem colecția `$articles` către șablon (să zicem în metoda `renderDefault()`). Vom profita de faptul că procesarea semnalelor are loc înainte de metodele `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Acum, la procesarea semnalului, în loc de colecția cu toate articolele, se va transmite către șablon doar un array cu un singur articol - cel pe care dorim să-l redăm și să-l trimitem în payload către browser. `{foreach}` va rula deci o singură dată și nu se vor mai reda snippet-uri suplimentare. - - -Calea componentelor -=================== - -O modalitate complet diferită de rezolvare evită snippet-urile dinamice. Trucul constă în transferarea întregii logici într-o componentă separată - de acum înainte, introducerea evaluărilor nu va mai fi gestionată de presenter, ci de o `LikeControl` dedicată. Clasa va arăta astfel (în plus, va conține și metodele `render`, `handleUnlike` etc.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private 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'); - } - } -} -``` - -Șablonul componentei: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>îmi place</a> - {else} - <a n:href="unlike!" class=ajax>nu-mi mai place</a> - {/if} -{/snippet} -``` - -Desigur, șablonul view-ului se va schimba și va trebui să adăugăm o fabrică în presenter. Deoarece vom crea componenta de atâtea ori câte articole obținem din baza de date, vom folosi clasa [Multiplier |application:Multiplier] pentru a o "multiplica". - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Șablonul view-ului se reduce la minimul necesar (și complet lipsit de snippet-uri!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Aproape am terminat: aplicația va funcționa acum cu AJAX. Și aici va trebui să optimizăm aplicația, deoarece, datorită utilizării Nette Database, la procesarea semnalului se încarcă inutil toate articolele din baza de date în loc de unul singur. Avantajul este însă că acestea nu vor fi redate, deoarece se va reda efectiv doar componenta noastră. - -{{priority: -1}} diff --git a/best-practices/ro/editors-and-tools.texy b/best-practices/ro/editors-and-tools.texy deleted file mode 100644 index 7c44d258a6..0000000000 --- a/best-practices/ro/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Editoare & instrumente -********************** - -.[perex] -Poți fi un programator priceput, dar numai cu instrumentele potrivite devii un maestru. În acest capitol vei găsi sfaturi despre instrumente, editoare și plugin-uri importante. - - -Editor IDE -========== - -Recomandăm cu tărie utilizarea unui IDE complet pentru dezvoltare, cum ar fi PhpStorm, NetBeans, VS Code, și nu doar un editor de text cu suport pentru PHP. Diferența este cu adevărat fundamentală. Nu există niciun motiv să te mulțumești cu un simplu editor care colorează sintaxa, dar nu atinge capacitățile unui IDE de top, care oferă sugestii precise, verifică erorile, poate refactoriza codul și multe altele. Unele IDE-uri sunt plătite, altele sunt chiar gratuite. - -**NetBeans IDE** are suport încorporat pentru Nette, Latte și NEON. - -**PhpStorm**: instalează aceste plugin-uri în `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: găsește pluginul "Nette Latte + Neon" în marketplace. - -Conectează, de asemenea, Tracy la editor. Când se afișează pagina de eroare, vei putea da clic pe numele fișierelor și acestea se vor deschide în editor cu cursorul pe linia corespunzătoare. Citește [cum să configurezi sistemul |tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan este un instrument care detectează erorile logice din cod înainte de a-l rula. - -Îl instalăm folosind Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Creăm în proiect fișierul de configurare `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Și apoi îl lăsăm să analizeze clasele din directorul `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Documentația exhaustivă o găsiți direct pe [site-ul PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker |code-checker:] verifică și, eventual, corectează unele dintre erorile formale din codurile sursă: - -- elimină [BOM |nette:glossary#BOM] -- verifică validitatea șabloanelor [Latte |latte:] -- verifică validitatea fișierelor `.neon`, `.php` și `.json` -- verifică prezența [caracterelor de control |nette:glossary#Caractere de control] -- verifică dacă fișierul este codificat în UTF-8 -- verifică `/* @anotace */` scrise incorect (lipsește asteriscul) -- elimină `?>` de închidere din fișierele PHP -- elimină spațiile de la sfârșitul rândului și rândurile goale inutile de la sfârșitul fișierului -- normalizează delimitatorii de rând la cei de sistem (dacă specificați opțiunea `-l`) - - -Composer -======== - -[Composer |best-practices:composer] este un instrument pentru gestionarea dependențelor în PHP. Ne permite să declarăm dependențe oricât de complexe ale diferitelor biblioteci și apoi le instalează pentru noi în proiectul nostru. - - -Requirements Checker -==================== - -Acesta a fost un instrument care testa mediul de rulare al serverului și informa dacă (și în ce măsură) framework-ul poate fi utilizat. În prezent, Nette poate fi utilizat pe orice server care are versiunea minimă necesară de PHP. diff --git a/best-practices/ro/form-reuse.texy b/best-practices/ro/form-reuse.texy deleted file mode 100644 index 20b7c45804..0000000000 --- a/best-practices/ro/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Reutilizarea formularelor în mai multe locuri -********************************************* - -.[perex] -În Nette aveți la dispoziție mai multe opțiuni pentru a utiliza același formular în mai multe locuri și a nu duplica codul. În acest articol vom prezenta diverse soluții, inclusiv cele pe care ar trebui să le evitați. - - -Fabrica de formulare -==================== - -Una dintre abordările de bază pentru utilizarea aceleiași componente în mai multe locuri este crearea unei metode sau clase care generează această componentă și apoi apelarea acestei metode în diferite locuri ale aplicației. O astfel de metodă sau clasă se numește *fabrică*. Vă rugăm să nu confundați cu modelul de proiectare *factory method*, care descrie un mod specific de utilizare a fabricilor și nu are legătură cu acest subiect. - -Ca exemplu, vom crea o fabrică care va construi un formular de editare: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Titlu:'); - // aici se adaugă alte câmpuri de formular - $form->addSubmit('send', 'Trimite'); - return $form; - } -} -``` - -Acum puteți utiliza această fabrică în diferite locuri din aplicația dvs., de exemplu în presenteri sau componente. Și asta prin [solicitarea ei ca dependență |dependency-injection:passing-dependencies]. Mai întâi, vom înregistra clasa în fișierul de configurare: - -```neon -services: - - FormFactory -``` - -Și apoi o vom folosi într-un presenter: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // procesarea datelor trimise - }; - return $form; - } -} -``` - -Puteți extinde fabrica de formulare cu alte metode pentru crearea altor tipuri de formulare, în funcție de nevoile aplicației dvs. Și, desigur, putem adăuga și o metodă care creează un formular de bază fără elemente, pe care celelalte metode o vor utiliza: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Titlu:'); - // aici se adaugă alte câmpuri de formular - $form->addSubmit('send', 'Trimite'); - return $form; - } -} -``` - -Metoda `createForm()` nu face încă nimic util, dar acest lucru se va schimba rapid. - - -Dependențele fabricii -===================== - -Cu timpul, se va dovedi că avem nevoie ca formularele să fie multilingve. Acest lucru înseamnă că trebuie să setăm un așa-numit [translator |forms:rendering#Traducere] pentru toate formularele. În acest scop, vom modifica clasa `FormFactory` astfel încât să accepte obiectul `Translator` ca dependență în constructor și să-l transmitem formularului: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Deoarece metoda `createForm()` este apelată și de celelalte metode care creează formulare specifice, este suficient să setăm translatorul doar în ea. Și am terminat. Nu este nevoie să modificăm codul niciunui presenter sau componente, ceea ce este grozav. - - -Mai multe clase de fabrici -========================== - -Alternativ, puteți crea mai multe clase pentru fiecare formular pe care doriți să-l utilizați în aplicația dvs. Această abordare poate crește lizibilitatea codului și facilita gestionarea formularelor. Vom lăsa `FormFactory` originală să creeze doar un formular curat cu configurația de bază (de exemplu, cu suport pentru traduceri) și vom crea o nouă fabrică `EditFormFactory` pentru formularul de editare. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ utilizarea compoziției -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // aici se adaugă alte câmpuri de formular - $form->addSubmit('send', 'Trimite'); - return $form; - } -} -``` - -Este foarte important ca legătura dintre clasele `FormFactory` și `EditFormFactory` să fie realizată prin [compoziție |nette:introduction-to-object-oriented-programming#Compoziție], nu prin [moștenire de obiecte |nette:introduction-to-object-oriented-programming#Moștenire]: - -```php -// ⛔ NU AȘA! MOȘTENIREA NU APARȚINE AICI -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Titlu:'); - // aici se adaugă alte câmpuri de formular - $form->addSubmit('send', 'Trimite'); - return $form; - } -} -``` - -Utilizarea moștenirii ar fi complet contraproductivă în acest caz. Ați întâmpina probleme foarte rapid. De exemplu, în momentul în care ați dori să adăugați parametri metodei `create()`; PHP ar raporta o eroare că semnătura sa diferă de cea a părintelui. Sau la transmiterea dependențelor către clasa `EditFormFactory` prin constructor. Ar apărea o situație pe care o numim [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. - -În general, este mai bine să preferăm [compoziția în detrimentul moștenirii |dependency-injection:faq#De ce se preferă compoziția în locul moștenirii]. - - -Gestionarea formularului -======================== - -Gestionarea formularului, care este apelată după trimiterea cu succes, poate fi, de asemenea, parte a clasei fabricii. Va funcționa prin transmiterea datelor trimise către model pentru procesare. Eventualele erori le va [transmite înapoi |forms:validation#Erori în timpul procesării] formularului. Modelul din exemplul următor este reprezentat de clasa `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Titlu:'); - // aici se adaugă alte câmpuri de formular - $form->addSubmit('send', 'Trimite'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // procesarea datelor trimise - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Redirecționarea în sine o vom lăsa însă pe seama presenterului. Acesta va adăuga evenimentului `onSuccess` un alt handler care va efectua redirecționarea. Datorită acestui fapt, va fi posibilă utilizarea formularului în diferiți presenteri și redirecționarea către locuri diferite în fiecare. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Înregistrarea a fost salvată'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Această soluție utilizează proprietatea formularelor că, atunci când se apelează `addError()` pe formular sau pe elementele sale, următorul handler `onSuccess` nu mai este apelat. - - -Moștenirea de la clasa Form -=========================== - -Formularul construit nu trebuie să fie un descendent al formularului. Cu alte cuvinte, nu utilizați această soluție: - -```php -// ⛔ NU AȘA! MOȘTENIREA NU APARȚINE AICI -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Titlu:'); - // aici se adaugă alte câmpuri de formular - $this->addSubmit('send', 'Trimite'); - $this->setTranslator($translator); - } -} -``` - -În loc să construiți formularul în constructor, utilizați o fabrică. - -Este necesar să realizăm că clasa `Form` este în primul rând un instrument pentru construirea unui formular, adică un *form builder*. Iar formularul construit poate fi considerat produsul său. Însă produsul nu este un caz specific al builder-ului, nu există între ele o legătură *is a* care stă la baza moștenirii. - - -Componenta cu formular -====================== - -O abordare complet diferită este crearea unei [componente |application:components], care include un formular. Acest lucru oferă noi posibilități, de exemplu, redarea formularului într-un mod specific, deoarece componenta include și un șablon. Sau se pot utiliza semnale pentru comunicarea AJAX și încărcarea suplimentară a informațiilor în formular, de exemplu pentru sugestii, etc. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Titlu:'); - // aici se adaugă alte câmpuri de formular - $form->addSubmit('send', 'Trimite'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // procesarea datelor trimise - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // declanșarea evenimentului - $this->onSave($this, $data); - } -} -``` - -Vom crea și o fabrică care va produce această componentă. Este suficient să [înregistrăm interfața sa |application:components#Componente cu dependențe]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -Și să o adăugăm în fișierul de configurare: - -```neon -services: - - EditControlFactory -``` - -Și acum putem solicita fabrica și o putem utiliza în presenter: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // sau redirecționăm către rezultatul editării, de ex.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/ro/inject-method-attribute.texy b/best-practices/ro/inject-method-attribute.texy deleted file mode 100644 index 0479b38c22..0000000000 --- a/best-practices/ro/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Metode și atribute inject -************************* - -.[perex] -În acest articol ne vom concentra pe diferite modalități de a transmite dependențe către presenteri în framework-ul Nette. Vom compara metoda preferată, care este constructorul, cu alte opțiuni, cum ar fi metodele și atributele `inject`. - -Și pentru presenteri este valabil că transmiterea dependențelor prin [constructor |dependency-injection:passing-dependencies#Transmitere prin constructor] este calea preferată. Dacă însă creați un strămoș comun din care moștenesc alți presenteri (de ex. `BasePresenter`), și acest strămoș are de asemenea dependențe, apare o problemă pe care o numim [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Aceasta poate fi ocolită folosind căi alternative, reprezentate de metode și atribute (anterior adnotări) `inject`. - - -Metode `inject*()` -================== - -Este o formă de transmitere a dependenței prin [setter |dependency-injection:passing-dependencies#Transmitere prin setter]. Numele acestor setteri începe cu prefixul `inject`. Nette DI apelează automat metodele numite astfel imediat după crearea instanței presenterului și le transmite toate dependențele necesare. Prin urmare, trebuie declarate ca public. - -Metodele `inject*()` pot fi considerate un fel de extensie a constructorului în mai multe metode. Datorită acestui fapt, `BasePresenter` poate prelua dependențe printr-o altă metodă și lăsa constructorul liber pentru descendenții săi: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Un presenter poate conține un număr arbitrar de metode `inject*()` și fiecare poate avea un număr arbitrar de parametri. Se potrivesc excelent și în cazurile în care presenterul este [compus din trait-uri |presenter-traits] și fiecare dintre ele necesită propria dependență. - - -Atribute `Inject` -================= - -Este o formă de [injectare în proprietate |dependency-injection:passing-dependencies#Setarea proprietății]. Este suficient să marcați în ce variabile trebuie injectat, iar Nette DI transmite automat dependențele imediat după crearea instanței presenterului. Pentru a le putea insera, este necesar să le declarați ca public. - -Marcăm proprietățile cu atributul: (anterior se folosea adnotarea `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // această linie este importantă - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Avantajul acestei metode de transmitere a dependențelor a fost forma foarte concisă a scrierii. Cu toate acestea, odată cu apariția [constructor property promotion |https://blog.nette.org/ro/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], pare mai ușor să folosești constructorul. - -Pe de altă parte, această metodă suferă de aceleași neajunsuri ca și transmiterea dependențelor către proprietăți în general: nu avem control asupra modificărilor din variabilă și, în același timp, variabila devine parte a interfeței publice a clasei, ceea ce este nedorit. diff --git a/best-practices/ro/lets-create-contact-form.texy b/best-practices/ro/lets-create-contact-form.texy deleted file mode 100644 index 9ae4c05523..0000000000 --- a/best-practices/ro/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Creăm un formular de contact -**************************** - -.[perex] -Vom analiza cum să creăm un formular de contact în Nette, inclusiv trimiterea pe email. Să începem! - -Mai întâi trebuie să creăm un proiect nou. Cum se face acest lucru este explicat pe pagina [Începeți |nette:installation]. Apoi putem începe crearea formularului. - -Cel mai simplu este să creăm [formularul direct în presenter |forms:in-presenter]. Putem folosi `HomePresenter` pre-pregătit. În el vom adăuga componenta `contactForm` care reprezintă formularul. Vom face acest lucru scriind în cod metoda fabrică `createComponentContactForm()`, care va produce componenta: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Nume:') - ->setRequired('Introduceți numele'); - $form->addEmail('email', 'E-mail:') - ->setRequired('Introduceți e-mailul'); - $form->addTextarea('message', 'Mesaj:') - ->setRequired('Introduceți mesajul'); - $form->addSubmit('send', 'Trimite'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, $data): void - { - // trimiterea emailului - } -} -``` - -După cum vedeți, am creat două metode. Prima metodă `createComponentContactForm()` creează un nou formular. Acesta are câmpuri pentru nume, email și mesaj, pe care le adăugăm cu metodele `addText()`, `addEmail()` și `addTextArea()`. Am adăugat și un buton pentru trimiterea formularului. Dar ce se întâmplă dacă utilizatorul nu completează un câmp? În acest caz, ar trebui să-l informăm că este un câmp obligatoriu. Am realizat acest lucru cu metoda `setRequired()`. În final, am adăugat și [evenimentul |nette:glossary#Evenimente] `onSuccess`, care se declanșează dacă formularul este trimis cu succes. În cazul nostru, apelează metoda `contactFormSucceeded`, care se ocupă de procesarea formularului trimis. Vom completa codul pentru aceasta imediat. - -Vom lăsa componenta `contactForm` să fie redată în șablonul `Home/default.latte`: - -```latte -{block content} -<h1>Formular de contact</h1> -{control contactForm} -``` - -Pentru trimiterea efectivă a emailului, vom crea o nouă clasă, pe care o vom numi `ContactFacade` și o vom plasa în fișierul `app/Model/ContactFacade.php`: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // emailul dvs. - ->setFrom($email, $name) - ->setSubject('Mesaj din formularul de contact') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Metoda `sendMessage()` creează și trimite emailul. Utilizează pentru aceasta așa-numitul mailer, pe care îl primește ca dependență prin constructor. Citiți mai multe despre [trimiterea emailurilor |mail:]. - -Acum ne vom întoarce la presenter și vom finaliza metoda `contactFormSucceeded()`. Aceasta va apela metoda `sendMessage()` a clasei `ContactFacade` și îi va transmite datele din formular. Și cum obținem obiectul `ContactFacade`? Îl vom primi prin constructor: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Mesajul a fost trimis'); - $this->redirect('this'); - } -} -``` - -După ce emailul este trimis, vom afișa utilizatorului un așa-numit [flash message |application:components#Mesaje flash], confirmând că mesajul a fost trimis, și apoi vom redirecționa către aceeași pagină (pentru a curăța formularul), astfel încât să nu fie posibilă retrimiterea formularului prin *refresh* în browser. - - -Deci, dacă totul funcționează, ar trebui să puteți trimite un email din formularul dvs. de contact. Felicitări! - - -Șablon HTML pentru email ------------------------- - -Deocamdată se trimite un email text simplu care conține doar mesajul trimis prin formular. Dar în email putem folosi HTML și să-i facem aspectul mai atractiv. Vom crea un șablon pentru el în Latte, pe care îl vom scrie în `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Mesaj din formularul de contact - - -

    Nume: {$name}

    -

    E-mail: {$email}

    -

    Mesaj: {$message}

    - - -``` - -Rămâne să modificăm `ContactFacade` pentru a utiliza acest șablon. În constructor vom solicita clasa `LatteFactory`, care poate produce obiectul `Latte\Engine`, adică [motorul de redare a șabloanelor Latte |latte:develop#Cum se randează un șablon]. Folosind metoda `renderToString()`, vom reda șablonul într-un șir, primul parametru este calea către șablon și al doilea sunt variabilele. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // emailul dvs. - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Emailul HTML generat îl vom transmite apoi metodei `setHtmlBody()` în locul celei originale `setBody()`. De asemenea, nu trebuie să specificăm subiectul emailului în `setSubject()`, deoarece biblioteca îl va prelua din elementul `` al șablonului. - - -Configurare ------------ - -În codul clasei `ContactFacade` este încă hardcodat emailul nostru de administrator `admin@example.com`. Ar fi mai bine să-l mutăm în fișierul de configurare. Cum facem asta? - -Mai întâi modificăm clasa `ContactFacade` și înlocuim șirul cu emailul cu o variabilă transmisă prin constructor: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -Și al doilea pas este specificarea valorii acestei variabile în configurație. În fișierul `app/config/services.neon` scriem: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -Și gata. Dacă ar fi multe elemente în secțiunea `services` și ați avea senzația că emailul se pierde printre ele, îl putem transforma într-o variabilă. Modificăm înregistrarea la: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -Și în fișierul `app/config/common.neon` definim această variabilă: - -```neon -parameters: - adminEmail: admin@example.com -``` - -Și am terminat! diff --git a/best-practices/ro/microsites.texy b/best-practices/ro/microsites.texy deleted file mode 100644 index 6c4441df8b..0000000000 --- a/best-practices/ro/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Cum să scrii micro-site-uri -*************************** - -Imaginați-vă că trebuie să creați rapid un mic site web pentru un eveniment viitor al companiei dvs. Trebuie să fie simplu, rapid și fără complicații inutile. Poate credeți că pentru un proiect atât de mic nu aveți nevoie de un framework robust. Dar ce se întâmplă dacă utilizarea framework-ului Nette poate simplifica și accelera fundamental acest proces? - -Chiar și la crearea site-urilor web simple, nu doriți să renunțați la confort. Nu doriți să reinventați ceea ce a fost deja rezolvat. Fiți liniștit leneș și lăsați-vă răsfățat. Nette Framework poate fi utilizat excelent și ca micro framework. - -Cum poate arăta un astfel de microsite? De exemplu, astfel încât întregul cod al site-ului să fie plasat într-un singur fișier `index.php` în directorul public: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// creează containerul DI pe baza configurației din config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// setăm rutarea -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// rută pentru URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // detectăm limba browserului și redirecționăm către URL /en sau /de etc. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// rută pentru URL https://example.com/cs sau https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // afișăm șablonul corespunzător, de exemplu ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// pornește aplicația! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Restul vor fi șabloane stocate în directorul părinte `/templates`. - -Codul PHP din `index.php` mai întâi [pregătește mediul |bootstrap:], apoi definește [rutele |application:routing#Rutare dinamică cu callback-uri] și în final pornește aplicația. Avantajul este că al doilea parametru al funcției `addRoute()` poate fi un callable, care se execută după deschiderea paginii corespunzătoare. - - -De ce să folosiți Nette pentru microsite-uri? ---------------------------------------------- - -- Programatorii care au încercat vreodată [Tracy |tracy:] nu își pot imagina astăzi că ar programa ceva fără ea. -- În primul rând, veți utiliza sistemul de șabloane [Latte |latte:], deoarece de la 2 pagini veți dori să aveți [layout-ul și conținutul separate |latte:template-inheritance]. -- Și cu siguranță doriți să vă bazați pe [escaparea automată |latte:safety-first], pentru a nu crea o vulnerabilitate XSS. -- Nette asigură, de asemenea, că în caz de eroare nu se vor afișa niciodată mesaje de eroare PHP pentru programatori, ci o pagină inteligibilă pentru utilizator. -- Dacă doriți să obțineți feedback de la utilizatori, de exemplu sub forma unui formular de contact, atunci veți adăuga și [formulare |forms:] și [bază de date |database:]. -- Formularele completate le puteți, de asemenea, [trimite ușor prin email |mail:]. -- Uneori vă poate fi utilă [cache-uirea |caching:], de exemplu dacă descărcați și afișați feed-uri. - -În zilele noastre, când viteza și eficiența sunt esențiale, este important să aveți instrumente care vă permit să obțineți rezultate fără întârzieri inutile. Nette framework vă oferă exact asta - dezvoltare rapidă, securitate și o gamă largă de instrumente, cum ar fi Tracy și Latte, care simplifică procesul. Este suficient să instalați câteva pachete Nette și construirea unui astfel de microsite devine brusc o joacă de copii. Și știți că nu se ascunde nicio gaură de securitate nicăieri. diff --git a/best-practices/ro/pagination.texy b/best-practices/ro/pagination.texy deleted file mode 100644 index fe8b8e2114..0000000000 --- a/best-practices/ro/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Paginarea rezultatelor bazei de date -************************************ - -.[perex] -La crearea aplicațiilor web, vă veți întâlni foarte des cu cerința de a limita numărul de elemente afișate pe pagină. - -Pornim de la starea în care afișăm toate datele fără paginare. Pentru selectarea datelor din baza de date avem clasa `ArticleRepository`, care, pe lângă constructor, conține metoda `findPublishedArticles`, ce returnează toate articolele publicate sortate descrescător după data publicării. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -În presenter injectăm apoi clasa model și în metoda render solicităm articolele publicate, pe care le transmitem șablonului: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -În șablonul `default.latte` ne ocupăm apoi de afișarea articolelor: - -```latte -{block content} -<h1>Articole</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -În acest mod putem afișa toate articolele, ceea ce însă începe să cauzeze probleme în momentul în care numărul articolelor crește. În acel moment devine utilă implementarea unui mecanism de paginare. - -Acesta asigură că toate articolele sunt împărțite în mai multe pagini și noi afișăm doar articolele unei pagini curente. Numărul total de pagini și împărțirea articolelor sunt calculate de [Paginator |utils:Paginator] singur, în funcție de câte articole avem în total și câte articole dorim să afișăm pe pagină. - -În primul pas, vom folosi obiectul `Paginator` în presenter pentru a calcula limita și offset-ul necesare pentru interogarea bazei de date. Clasa `ArticleRepository` nu necesită modificări dacă folosim `Nette\Database\Explorer`, deoarece putem aplica paginarea direct pe obiectul `Selection`. - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $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, - ); - } - - /** - * Returnează numărul total de articole publicate - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Ulterior, ne apucăm de modificările presenterului. În metoda render vom transmite numărul paginii afișate curent. Pentru cazul în care acest număr nu va face parte din URL, setăm valoarea implicită a primei pagini. - -Extindem, de asemenea, metoda render cu obținerea instanței Paginatorului, setarea sa și selectarea articolelor corecte pentru afișare în șablon. HomePresenter va arăta astfel după modificări: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Aflăm numărul total de articole publicate - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Creăm o instanță a Paginatorului și o setăm - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // numărul total de articole - $paginator->setItemsPerPage(10); // numărul de elemente pe pagină - $paginator->setPage($page); // numărul paginii curente - - // Extragem din baza de date un set limitat de articole conform calculului Paginatorului - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // pe care îl transmitem șablonului - $this->template->articles = $articles; - // și, de asemenea, Paginatorul însuși pentru afișarea opțiunilor de paginare - $this->template->paginator = $paginator; - } -} -``` - -Șablonul nostru iterează acum doar peste articolele unei singure pagini, este suficient să adăugăm linkurile de paginare: - -```latte -{block content} -<h1>Articole</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Prima</a> -  |  - <a n:href="default, $paginator->page-1">Anterioara</a> -  |  - {/if} - - Pagina {$paginator->getPage()} din {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Următoarea</a> -  |  - <a n:href="default, $paginator->getPageCount()">Ultima</a> - {/if} -</div> -``` - - -Astfel am completat pagina cu posibilitatea de paginare folosind `Paginator`. În cazul în care folosim [Nette Database Explorer |database:explorer], suntem capabili să implementăm paginarea și **fără a utiliza explicit** obiectul `Paginator` în presenter, deoarece clasa `Nette\Database\Table\Selection` conține metoda `page()` care încapsulează logica paginatorului. - -Repository-ul rămâne același ca în exemplul cu Explorer: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -În presenter nu trebuie să creăm Paginator, folosim în locul său metoda clasei `Selection`, pe care ne-o returnează repository-ul: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Extragem articolele publicate - $articles = $this->articleRepository->findPublishedArticles(); - - // și trimitem către șablon doar o parte din ele, limitată conform calculului metodei page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // și, de asemenea, datele necesare pentru afișarea opțiunilor de paginare - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Deoarece acum nu trimitem `Paginator` către șablon, modificăm partea care afișează linkurile de paginare pentru a folosi variabilele `$page` și `$lastPage`: - -```latte -{block content} -<h1>Articole</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Prima</a> -  |  - <a n:href="default, $page - 1">Anterioara</a> -  |  - {/if} - - Pagina {$page} din {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Următoarea</a> -  |  - <a n:href="default, $lastPage">Ultima</a> - {/if} -</div> -``` - -În acest mod am implementat mecanismul de paginare fără utilizarea Paginatorului. - -{{priority: -1}} diff --git a/best-practices/ro/passing-settings-to-presenters.texy b/best-practices/ro/passing-settings-to-presenters.texy deleted file mode 100644 index 0b0e2883ee..0000000000 --- a/best-practices/ro/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Transmiterea setărilor către presenteri -*************************************** - -.[perex] -Aveți nevoie să transmiteți argumente către presenteri care nu sunt obiecte (de ex. informația dacă rulează în modul debug, căi către directoare etc.) și, prin urmare, nu pot fi transmise automat prin autowiring? Soluția este să le încapsulați într-un obiect `Settings`. - -Serviciul `Settings` reprezintă o modalitate foarte ușoară și totuși utilă de a furniza informații despre aplicația care rulează către presenteri. Forma sa specifică depinde exclusiv de nevoile dvs. concrete. Exemplu: - -```php -namespace App; - -class Settings -{ - public function __construct( - // de la PHP 8.1 este posibil să specificați readonly - public bool $debugMode, - public string $appDir, - // și așa mai departe - ) {} -} -``` - -Exemplu de înregistrare în configurație: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Când presenterul va avea nevoie de informațiile furnizate de acest serviciu, pur și simplu îl va solicita în constructor: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/ro/post-links.texy b/best-practices/ro/post-links.texy deleted file mode 100644 index 449a9de1c5..0000000000 --- a/best-practices/ro/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Cum să utilizați corect linkurile POST -************************************** - -.[perex] -În aplicațiile web, în special în interfețele administrative, ar trebui să fie o regulă de bază ca acțiunile care modifică starea serverului să nu fie efectuate prin metoda HTTP GET. După cum sugerează și numele metodei, GET ar trebui utilizat doar pentru obținerea datelor, nu pentru modificarea lor. Pentru acțiuni precum ștergerea înregistrărilor, este mai potrivită utilizarea metodei POST. Deși ideală ar fi metoda DELETE, aceasta nu poate fi invocată fără JavaScript, de aceea se folosește istoric POST. - -Cum se face acest lucru în practică? Utilizați acest truc simplu. La începutul șablonului, creați un formular auxiliar cu identificatorul `postForm`, pe care îl veți utiliza ulterior pentru butoanele de ștergere: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Datorită acestui formular, puteți utiliza un buton `<button>` în loc de linkul clasic `<a>`, care poate fi stilizat vizual pentru a arăta ca un link obișnuit. De exemplu, framework-ul CSS Bootstrap oferă clasele `btn btn-link` cu care puteți obține ca butonul să nu fie vizual diferit de alte linkuri. Folosind atributul `form="postForm"`, îl legați de formularul pre-pregătit: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">delete</button> - <!-- instead of <a n:href="delete $post->id">delete</a> --> - </td> - </tr> -</table> -``` - -La click pe buton, se va invoca acum acțiunea `delete` prin metoda POST. Pentru a asigura că cererile sunt acceptate doar prin metoda POST și de pe același domeniu (ceea ce este o apărare eficientă împotriva atacurilor CSRF), utilizați atributul `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // cod ipotetic care șterge înregistrarea - $this->redirect('default'); - } -} -``` - -Atributul există de la Nette Application 3.2 și mai multe despre posibilitățile sale puteți afla pe pagina [Cum să utilizați atributul #Requires |attribute-requires]. - -Dacă ați utiliza semnalul `handleDelete()` în loc de acțiunea `actionDelete()`, nu este necesar să specificați `sameOrigin: true`, deoarece semnalele au această protecție setată implicit: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Această abordare nu numai că îmbunătățește securitatea aplicației dvs., dar contribuie și la respectarea standardelor și practicilor web corecte. Prin utilizarea metodelor POST pentru acțiunile care modifică starea, veți obține o aplicație mai robustă și mai sigură. diff --git a/best-practices/ro/presenter-traits.texy b/best-practices/ro/presenter-traits.texy deleted file mode 100644 index a074c21292..0000000000 --- a/best-practices/ro/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Compunerea presenterilor din trait-uri -************************************** - -.[perex] -Dacă avem nevoie să implementăm același cod în mai mulți presenteri (de ex. verificarea că utilizatorul este autentificat), o opțiune este plasarea codului într-un strămoș comun. A doua opțiune este crearea de [trait-uri |nette:introduction-to-object-oriented-programming#Trait-uri] cu un singur scop. - -Avantajul acestei soluții este că fiecare dintre presenteri poate folosi exact acele trait-uri de care are nevoie cu adevărat, în timp ce moștenirea multiplă nu este posibilă în PHP. - -Aceste trait-uri pot profita de faptul că la crearea presenterului se apelează succesiv toate [metodele inject |inject-method-attribute#Metode inject]. Este necesar doar să se asigure că numele fiecărei metode inject este unic pentru a evita conflictele. - -Trait-urile pot atașa cod de inițializare la evenimentele [onStartup sau onRender |application:presenters#Evenimente]. - -Exemple: - -```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); - }; - } -} -``` - -Presenterul apoi utilizează simplu aceste trait-uri: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/ro/restore-request.texy b/best-practices/ro/restore-request.texy deleted file mode 100644 index a8cee5857c..0000000000 --- a/best-practices/ro/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Cum să reveniți la pagina anterioară? -************************************* - -.[perex] -Ce se întâmplă dacă un utilizator completează un formular și sesiunea sa expiră? Pentru a nu pierde datele, înainte de a redirecționa către pagina de autentificare, salvăm cererea curentă în sesiune. În Nette, acest lucru este extrem de simplu. - -Cererea curentă poate fi salvată în sesiune folosind metoda `storeRequest()`, care returnează identificatorul său sub forma unui șir scurt. Metoda salvează numele presenterului curent, view-ul și parametrii săi. În cazul în care a fost trimis și un formular, se salvează și conținutul câmpurilor (cu excepția fișierelor încărcate). - -Restaurarea cererii se face prin metoda `restoreRequest($key)`, căreia îi transmitem identificatorul obținut. Aceasta redirecționează către presenterul și view-ul original. Dacă însă cererea salvată conține trimiterea unui formular, trece la presenterul original prin metoda `forward()`, transmite formularului valorile completate anterior și îl lasă să se redeseneze din nou. Astfel, utilizatorul are posibilitatea de a retrimite formularul și nu se pierd date. - -Important este că `restoreRequest()` verifică dacă utilizatorul nou autentificat este același cu cel care a completat inițial formularul. Dacă nu, cererea este abandonată și nu se face nimic. - -Vom ilustra totul cu un exemplu. Avem un presenter `AdminPresenter`, în care se editează date și în a cărui metodă `startup()` verificăm dacă utilizatorul este autentificat. Dacă nu este, îl redirecționăm către `SignPresenter`. În același timp, salvăm cererea curentă și trimitem cheia sa (`backlink`) către `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()]); - } - } -} -``` - -Presenterul `SignPresenter` va conține, pe lângă formularul de autentificare, și un parametru persistent `$backlink`, în care se va scrie cheia. Deoarece parametrul este persistent, acesta se va transmite și după trimiterea formularului de autentificare. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... adăugăm câmpurile formularului ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... aici autentificăm utilizatorul ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Metodei `restoreRequest()` îi transmitem cheia cererii salvate și aceasta redirecționează (sau trece) la presenterul original. - -Dacă însă cheia este invalidă (de exemplu, nu mai există în sesiune), metoda nu face nimic. Urmează deci apelul `$this->redirect('Admin:')`, care redirecționează către `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/sl/@home.texy b/best-practices/sl/@home.texy deleted file mode 100644 index a4cf0efbec..0000000000 --- a/best-practices/sl/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Navodila in postopki -******************** - -.[perex] -Navodila, rešitve pogostih nalog in *najboljše prakse* za Nette. - - -<div class=documentation> -<div> - - -Nette Aplikacije ----------------- -- [Metode in atributi inject |inject-method-attribute] -- [Sestavljanje presenterjev iz traitov |presenter-traits] -- [Posredovanje nastavitev v presenterje |passing-settings-to-presenters] -- [Kako se vrniti na prejšnjo stran |restore-request] -- [Strankanje rezultatov podatkovne baze |pagination] -- [Dinamični odrezki |dynamic-snippets] -- [Kako uporabljati atribut #Requires |attribute-requires] -- [Kako pravilno uporabljati POST povezave |post-links] - -</div> -<div> - - -Obrazci -------- -- [Ponovna uporaba obrazcev |form-reuse] -- [Obrazec za ustvarjanje in urejanje zapisa |creating-editing-form] -- [Ustvarjamo kontaktni obrazec |lets-create-contact-form] -- [Odvisni selectboxi |https://blog.nette.org/sl/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Splošno -------- -- [Kako naložiti konfiguracijsko datoteko |bootstrap:] -- [Kako pisati mikro-spletne strani |microsites] -- [Zakaj Nette uporablja PascalCase notacijo konstant? |https://blog.nette.org/sl/for-less-screaming-in-the-code] -- [Zakaj Nette ne uporablja pripone Interface? |https://blog.nette.org/sl/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: nasveti za uporabo |composer] -- [Nasveti za urejevalnike & orodja |editors-and-tools] -- [Uvod v objektno orientirano programiranje |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Primeri rešitev ---------------- -- [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:] - -</div> -<div> - - -Videi ------ -Stotine posnetkov iz Poslednjih sobot in videov o Nette najdete pod eno streho na "Youtube kanalu Nette Frameworka":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/sl/@meta.texy b/best-practices/sl/@meta.texy deleted file mode 100644 index f58ad17850..0000000000 --- a/best-practices/sl/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Navodila in postopki}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/sl/attribute-requires.texy b/best-practices/sl/attribute-requires.texy deleted file mode 100644 index 8fb0588c40..0000000000 --- a/best-practices/sl/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Kako uporabljati atribut `#[Requires]` -************************************** - -.[perex] -Ko pišete spletno aplikacijo, se pogosto srečate s potrebo po omejitvi dostopa do določenih delov vaše aplikacije. Morda želite, da lahko nekateri zahtevki pošiljajo podatke samo s pomočjo obrazca (torej z metodo POST), ali da so dostopni samo za AJAX klice. V Nette Frameworku 3.2 se je pojavilo novo orodje, ki vam omogoča takšne omejitve nastaviti zelo elegantno in pregledno: atribut `#[Requires]`. - -Atribut je posebna oznaka v PHP, ki jo dodate pred definicijo razreda ali metode. Ker gre pravzaprav za razred, da bi vam naslednji primeri delovali, je treba navesti klavzulo use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Atribut `#[Requires]` lahko uporabite pri samem razredu presenterja in tudi na teh metodah: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Zadnji dve metodi se nanašata tudi na komponente, torej atribut lahko uporabljate tudi pri njih. - -Če pogoji, ki jih atribut navaja, niso izpolnjeni, pride do sprožitve HTTP napake 4xx. - - -Metode HTTP ------------ - -Lahko specificirate, katere HTTP metode (kot GET, POST itd.) so za dostop dovoljene. Na primer, če želite dovoliti dostop samo s pošiljanjem obrazca, nastavite: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Zakaj bi morali uporabljati POST namesto GET za akcije, ki spreminjajo stanje, in kako to storiti? [Preberite navodilo |post-links]. - -Lahko navedete metodo ali polje metod. Poseben primer je vrednost `'*'`, ki dovoli vse metode, kar standardno presenterji iz [varnostnih razlogov ne dovoljujejo |application:presenters#Preverjanje HTTP metode]. - - -AJAX klici ----------- - -Če želite, da je presenter ali metoda dostopna samo za AJAX zahtevke, uporabite: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Isti izvor ----------- - -Za povečanje varnosti lahko zahtevate, da je zahtevek narejen iz iste domene. S tem preprečite [ranljivost CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Pri metodah `handle<Signal>()` je dostop iz iste domene zahtevan samodejno. Torej, če nasprotno želite dovoliti dostop iz katerekoli domene, navedite: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Dostop prek posredovanja ------------------------- - -Včasih je koristno omejiti dostop do presenterja tako, da je dostopen samo posredno, na primer z uporabo metode `forward()` ali `switch()` iz drugega presenterja. Tako se na primer ščitijo error-presenterji, da jih ni mogoče poklicati iz URL-ja: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -V praksi je pogosto treba označiti določene poglede (views), do katerih je mogoče priti šele na podlagi logike v presenterju. Torej spet, da jih ni mogoče odpreti neposredno: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Konkretne akcije ----------------- - -Lahko tudi omejite, da bo določena koda, na primer ustvarjanje komponente, dostopna samo za specifične akcije v presenterju: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -V primeru ene akcije ni treba zapisovati polja: `#[Requires(actions: 'default')]` - - -Lastni atributi ---------------- - -Če želite atribut `#[Requires]` uporabiti večkrat z isto nastavitvijo, si lahko ustvarite lasten atribut, ki bo dedoval `#[Requires]` in ga nastavil po potrebi. - -Na primer `#[SingleAction]` bo omogočil dostop samo prek akcije `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Ali `#[RestMethods]` bo omogočil dostop prek vseh HTTP metod, uporabljenih za REST API: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Zaključek ---------- - -Atribut `#[Requires]` vam daje veliko fleksibilnosti in nadzora nad tem, kako so vaše spletne strani dostopne. S pomočjo preprostih, a močnih pravil lahko povečate varnost in pravilno delovanje vaše aplikacije. Kot vidite, lahko uporaba atributov v Nette vaše delo ne samo olajša, ampak tudi zavaruje. diff --git a/best-practices/sl/composer.texy b/best-practices/sl/composer.texy deleted file mode 100644 index bcfe1802c9..0000000000 --- a/best-practices/sl/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: nasveti za uporabo -**************************** - -<div class=perex> - -Composer je orodje za upravljanje odvisnosti v PHP. Omogoča nam, da naštejemo knjižnice, od katerih je naš projekt odvisen, in jih bo za nas nameščal in posodabljal. Pokazali bomo: - -- kako namestiti Composer -- njegovo uporabo v novem ali obstoječem projektu - -</div> - - -Namestitev -========== - -Composer je izvedljiva datoteka `.phar`, ki jo prenesete in namestite na naslednji način: - - -Windows -------- - -Uporabite uradni namestitveni program [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Dovolj so 4 ukazi, ki jih kopirate s [te strani |https://getcomposer.org/download/]. - -Nato z vstavitvijo v mapo, ki je v sistemskem `PATH`, postane Composer dostopen globalno: - -```shell -$ mv ./composer.phar ~/bin/composer # ali /usr/local/bin/composer -``` - - -Uporaba v projektu -================== - -Da bi lahko v svojem projektu začeli uporabljati Composer, potrebujete samo datoteko `composer.json`. Ta opisuje odvisnosti našega projekta in lahko vsebuje tudi druge metapodatke. Osnovni `composer.json` torej lahko izgleda takole: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Tukaj pravimo, da naša aplikacija (ali knjižnica) zahteva paket `nette/database` (ime paketa sestoji iz imena organizacije in imena projekta) in želi različico, ki ustreza pogoju `^3.0` (tj. najnovejšo različico 3). - -Imamo torej v korenu projekta datoteko `composer.json` in zaženemo namestitev: - -```shell -composer update -``` - -Composer bo prenesel Nette Database v mapo `vendor/`. Nato bo ustvaril datoteko `composer.lock`, ki vsebuje informacije o tem, katere različice knjižnic je točno namestil. - -Composer bo generiral datoteko `vendor/autoload.php`, ki jo lahko preprosto vključimo in začnemo uporabljati knjižnice brez kakršnegakoli dodatnega dela: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Posodabljanje paketov na najnovejše različice -============================================= - -Za posodabljanje uporabljenih knjižnic na najnovejše različice glede na pogoje, definirane v `composer.json`, skrbi ukaz `composer update`. Npr. pri odvisnosti `"nette/database": "^3.0"` bo namestil najnovejšo različico 3.x.x, vendar ne več različice 4. - -Za posodobitev pogojev v datoteki `composer.json`, na primer na `"nette/database": "^4.1"`, da bi bilo mogoče namestiti najnovejšo različico, uporabite ukaz `composer require nette/database`. - -Za posodobitev vseh uporabljenih paketov Nette bi bilo treba vse v ukazni vrstici našteti, npr.: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Kar je nepraktično. Uporabite zato preprost skript "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, ki to stori za vas: - -```shell -php composer-frontline.php -``` - - -Ustvarjanje novega projekta -=========================== - -Nov projekt na Nette ustvarite s pomočjo enega samega ukaza: - -```shell -composer create-project nette/web-project ime-projekta -``` - -Kot `ime-projekta` vstavite ime mape za svoj projekt in potrdite. Composer bo prenesel repozitorij `nette/web-project` z GitHuba, ki že vsebuje datoteko `composer.json`, in takoj zatem Nette Framework. Moralo bi že zadostovati samo [nastaviti dovoljenja |nette:troubleshooting#Nastavitev pravic map] za pisanje v mape `temp/` in `log/` in projekt bi moral oživeti. - -Če veste, na kateri različici PHP bo projekt gostoval, ne pozabite [jo nastaviti |#Različica PHP]. - - -Različica PHP -============= - -Composer vedno namešča tiste različice paketov, ki so združljive z različico PHP, ki jo pravkar uporabljate (bolje rečeno z različico PHP, uporabljeno v ukazni vrstici pri zagonu Composerja). Kar pa najverjetneje ni ista različica, kot jo uporablja vaše gostovanje. Zato je zelo pomembno, da si v datoteko `composer.json` dodate informacijo o različici PHP na gostovanju. Nato se bodo nameščale samo različice paketov, združljive z gostovanjem. - -To, da bo projekt tekel na primer na PHP 8.2.3, nastavimo z ukazom: - -```shell -composer config platform.php 8.2.3 -``` - -Tako se različica zapiše v datoteko `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Vendar se številka različice PHP navaja še na drugem mestu datoteke, in sicer v sekciji `require`. Medtem ko prva številka določa, za katero različico se bodo nameščali paketi, druga številka pravi, za katero različico je napisana sama aplikacija. In po njej na primer PhpStorm nastavlja *PHP language level*. (Seveda nima smisla, da bi se te različice razlikovale, zato je dvojni zapis nedomišljenost.) To različico nastavite z ukazom: - -```shell -composer require php 8.2.3 --no-update -``` - -Ali neposredno v datoteki `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ignoriranje različice PHP -========================= - -Paketi praviloma imajo navedeno tako najnižjo različico PHP, s katero so združljivi, kot tudi najvišjo, s katero so testirani. Če nameravate uporabljati še novejšo različico PHP, na primer zaradi testiranja, bo Composer zavrnil namestitev takšnega paketa. Rešitev je možnost `--ignore-platform-req=php+`, ki povzroči, da bo Composer ignoriral zgornje meje zahtevane različice PHP. - - -Lažna sporočila -=============== - -Pri nadgradnji paketov ali spremembah številk različic se zgodi, da pride do konflikta. En paket ima zahteve, ki so v nasprotju z drugim in podobno. Composer pa včasih izpisuje lažna sporočila. Poroča o konfliktu, ki realno ne obstaja. V takem primeru pomaga izbrisati datoteko `composer.lock` in poskusiti znova. - -Če sporočilo o napaki vztraja, potem je mišljeno resno in je treba iz njega razbrati, kaj in kako urediti. - - -Packagist.org - centralni repozitorij -===================================== - -[Packagist |https://packagist.org] je glavni repozitorij, v katerem Composer poskuša iskati pakete, če mu ne povemo drugače. Tukaj lahko objavimo tudi lastne pakete. - - -Kaj če ne želimo uporabljati centralnega repozitorija? ------------------------------------------------------- - -Če imamo znotrajpodjetniške aplikacije, ki jih preprosto ne moremo gostovati javno, si zanje ustvarimo podjetniški repozitorij. - -Več na temo repozitorijev [v uradni dokumentaciji |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Samodejno nalaganje -=================== - -Ključna lastnost Composerja je, da zagotavlja samodejno nalaganje za vse z njim nameščene razrede, ki ga zaženete z vključitvijo datoteke `vendor/autoload.php`. - -Vendar je mogoče uporabljati Composer tudi za nalaganje drugih razredov izven mape `vendor`. Prva možnost je, da pustite Composerju preiskati definirane mape in podmape, najti vse razrede in jih vključiti v samodejni nalagalnik. To dosežete z nastavitvijo `autoload > classmap` v `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # vključi mapo src/ in njene podmape - ] - } -} -``` - -Nato je treba ob vsaki spremembi zagnati ukaz `composer dumpautoload` in pustiti, da se tabele samodejnega nalaganja ponovno generirajo. To je izjemno neprijetno in veliko bolje je to nalogo zaupati [RobotLoaderju|robot-loader:], ki isto dejavnost izvaja samodejno v ozadju in veliko hitreje. - -Druga možnost je upoštevati [PSR-4|https://www.php-fig.org/psr/psr-4/]. Poenostavljeno rečeno gre za sistem, kjer imenski prostori in imena razredov ustrezajo strukturi map in imenom datotek, torej npr. `App\Core\RouterFactory` bo v datoteki `/path/to/App/Core/RouterFactory.php`. Primer konfiguracije: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # imenski prostor App\ je v mapi app/ - } - } -} -``` - -Kako natančno konfigurirati obnašanje, boste izvedeli v [dokumentaciji Composerja|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Testiranje novih različic -========================= - -Želite preizkusiti novo razvojno različico paketa. Kako to storiti? Najprej v datoteko `composer.json` dodajte ta par možnosti, ki dovoli nameščanje razvojnih različic paketov, vendar se k temu zateče samo v primeru, da ne obstaja nobena kombinacija stabilnih različic, ki bi ustrezala zahtevam: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Nato priporočamo izbris datoteke `composer.lock`, včasih namreč Composer nerazumljivo zavrne namestitev in to težavo reši. - -Recimo, da gre za paket `nette/utils` in nova različica ima številko 4.0. Namestite jo z ukazom: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Ali pa lahko namestite konkretno različico, na primer 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Ko pa je od knjižnice odvisen drug paket, ki je zaklenjen na starejšo različico (npr. `^3.1`), je idealno paket posodobiti, da bo deloval z novo različico. Če pa želite omejitev samo zaobiti in prisiliti Composer, da namesti razvojno različico in se pretvarja, da gre za starejšo različico (npr. 3.1.6), lahko uporabite ključno besedo `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Klicanje ukazov -=============== - -Prek Composerja lahko kličete lastne vnaprej pripravljene ukaze in skripte, kot da bi šlo za izvorne ukaze Composerja. Pri skriptih, ki se nahajajo v mapi `vendor/bin`, ni treba te mape navajati. - -Kot primer si definiramo v datoteki `composer.json` skript, ki s pomočjo [Nette Testerja|tester:] zažene teste: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Teste nato zaženemo s pomočjo `composer tester`. Ukaz lahko pokličemo tudi v primeru, da nismo v korenski mapi projekta, ampak v katerem od poddirektorijev. - - -Pošljite zahvalo -================ - -Pokazali vam bomo trik, s katerim boste razveselili avtorje odprte kode. Na preprost način boste na GitHubu dali zvezdico knjižnicam, ki jih vaš projekt uporablja. Dovolj je namestiti knjižnico `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -In nato zagnati: - -```shell -composer thanks -``` - -Poskusite! - - -Konfiguracija -============= - -Composer je tesno povezan z orodjem za verzioniranje [Git |https://git-scm.com]. Če ga nimate nameščenega, je treba Composerju povedati, naj ga ne uporablja: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/sl/creating-editing-form.texy b/best-practices/sl/creating-editing-form.texy deleted file mode 100644 index 650812cb3a..0000000000 --- a/best-practices/sl/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Obrazec za ustvarjanje in urejanje zapisa -***************************************** - -.[perex] -Kako v Nette pravilno implementirati dodajanje in urejanje zapisa, pri čemer za oboje uporabimo isti obrazec? - -V mnogih primerih so obrazci za dodajanje in urejanje zapisa enaki, razlikujejo se morda le po napisu na gumbu. Prikazali bomo primere preprostih presenterjev, kjer bomo obrazec najprej uporabili za dodajanje zapisa, nato za urejanje in na koncu obe rešitvi združili. - - -Dodajanje zapisa ----------------- - -Primer presenterja, ki služi za dodajanje zapisa. Samo delo s podatkovno bazo bomo prepustili razredu `Facade`, katerega koda za prikaz ni bistvena. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... dodamo polja obrazca ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // dodajanje zapisa v podatkovno bazo - $this->flashMessage('Uspešno dodano'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Urejanje zapisa ---------------- - -Zdaj si poglejmo, kako bi izgledal presenter, ki služi za urejanje zapisa: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // preverjanje obstoja zapisa - || !$this->facade->isEditAllowed(/*...*/) // preverjanje dovoljenj - ) { - $this->error(); // napaka 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // preverimo, da je akcija 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... dodamo polja obrazca ... - - $form->setDefaults($this->record); // nastavitev privzetih vrednosti - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // posodobitev zapisa - $this->flashMessage('Uspešno posodobljeno'); - $this->redirect('...'); - } -} -``` - -V metodi *action*, ki se zažene takoj na začetku [življenjskega cikla presenterja |application:presenters#Življenjski cikel presenterja], preverimo obstoj zapisa in dovoljenje uporabnika za urejanje. - -Zapis shranimo v lastnost `$record`, da ga imamo na voljo v metodi `createComponentRecordForm()` za nastavitev privzetih vrednosti in v `recordFormSucceeded()` zaradi ID-ja. Alternativna rešitev bi bila nastavitev privzetih vrednosti neposredno v `actionEdit()` in pridobitev vrednosti ID, ki je del URL-ja, s pomočjo `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // preverjanje obstoja in preverjanje dovoljenj - ) { - $this->error(); - } - - // nastavitev privzetih vrednosti obrazca - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Vendar pa, in to bi moralo biti **najpomembnejše spoznanje celotne kode**, se moramo pri ustvarjanju obrazca prepričati, da je akcija resnično `edit`. Ker sicer preverjanje v metodi `actionEdit()` sploh ne bi potekalo! - - -Isti obrazec za dodajanje in urejanje -------------------------------------- - -In zdaj oba presenterja združimo v enega. Ali bi lahko v metodi `createComponentRecordForm()` razlikovali, za katero akcijo gre, in glede na to konfigurirali obrazec, ali pa to prepustimo neposredno action-metodam in se znebimo pogoja: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private 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 // preverjanje obstoja zapisa - || !$this->facade->isEditAllowed(/*...*/) // preverjanje dovoljenj - ) { - $this->error(); // napaka 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // nastavitev privzetih vrednosti - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // preverimo, da je akcija 'add' ali 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... dodamo polja obrazca ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // dodajanje zapisa v podatkovno bazo - $this->flashMessage('Uspešno dodano'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // posodobitev zapisa - $this->flashMessage('Uspešno posodobljeno'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/sl/dynamic-snippets.texy b/best-practices/sl/dynamic-snippets.texy deleted file mode 100644 index 1f5704cd9c..0000000000 --- a/best-practices/sl/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Dinamični snippeti -****************** - -Precej pogosto se pri razvoju aplikacij pojavi potreba po izvajanju AJAX operacij, na primer nad posameznimi vrsticami tabele ali elementi seznama. Za primer lahko izberemo izpis člankov, pri čemer pri vsakem od njih prijavljenemu uporabniku omogočimo izbiro ocene "všeč mi je/ni mi všeč". Koda presenterja in ustrezne predloge brez AJAX-a bo izgledala približno takole (navajam najpomembnejše odseke, koda predvideva obstoj storitve za označevanje ocen in pridobivanje zbirke člankov - konkretna implementacija za namene tega navodila ni pomembna): - -```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'); -} -``` - -Predloga: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>všeč mi je</a> - {else} - <a n:href="unlike! $article->id" class=ajax>ni mi več všeč</a> - {/if} -</article> -``` - - -Ajaxizacija -=========== - -Zdaj pa opremimo to preprosto aplikacijo z AJAX-om. Sprememba ocene članka ni tako pomembna, da bi moralo priti do preusmeritve, zato bi idealno morala potekati z AJAX-om v ozadju. Uporabili bomo [pomožni skript iz dodatkov |application:ajax#Naja] z običajno konvencijo, da imajo AJAX povezave CSS razred `ajax`. - -Vendar kako to storiti konkretno? Nette ponuja 2 poti: pot t.i. dinamičnih snippetov in pot komponent. Obe imata svoje prednosti in slabosti, zato si ju bomo ogledali eno za drugo. - - -Pot dinamičnih snippetov -======================== - -Dinamični snippet v terminologiji Latte pomeni specifičen primer uporabe značke `{snippet}`, kjer je v imenu snippeta uporabljena spremenljivka. Takšen snippet se v predlogi ne more nahajati kjerkoli - mora biti ovit s statičnim snippetom, tj. običajnim, ali znotraj `{snippetArea}`. Našo predlogo bi lahko prilagodili na naslednji način. - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>všeč mi je</a> - {else} - <a n:href="unlike! $article->id" class=ajax>ni mi več všeč</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Vsak članek zdaj definira en snippet, ki ima v imenu ID članka. Vsi ti snippeti so nato skupaj zaviti v en snippet z imenom `articlesContainer`. Če bi ta ovojni snippet izpustili, bi nas Latte na to opozoril z izjemo. - -Ostane nam še, da v presenter dodamo ponovno izrisovanje - dovolj je ponovno izrisati statični ovoj. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- ni potrebno - } else { - $this->redirect('this'); - } -} -``` - -Podobno prilagodimo tudi sestrsko metodo `handleUnlike()`, in AJAX deluje! - -Rešitev pa ima eno senčno stran. Če bi podrobneje preučili, kako poteka AJAX zahteva, bi ugotovili, da čeprav se aplikacija navzven zdi varčna (vrne samo en sam snippet za določen članek), je v resnici na strežniku izrisala vse snippete. Želeni snippet nam je postavila v payload, ostale pa zavrgla (popolnoma nepotrebno jih je torej tudi pridobila iz podatkovne baze). - -Da bi ta proces optimizirali, bomo morali poseči tja, kjer v predlogo posredujemo zbirko `$articles` (recimo v metodi `renderDefault()`). Izkoristili bomo dejstvo, da obdelava signalov poteka pred metodami `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Zdaj se pri obdelavi signala v predlogo namesto zbirke z vsemi članki posreduje le polje z enim samim člankom - tistim, ki ga želimo izrisati in poslati v payloadu v brskalnik. `{foreach}` se torej izvede samo enkrat in nobeni dodatni snippeti se ne izrišejo. - - -Pot komponent -============= - -Popolnoma drugačen način reševanja se izogne dinamičnim snippetom. Trik je v prenosu celotne logike v posebno komponento - za vnos ocen ne bo več skrbel presenter, temveč namenska `LikeControl`. Razred bo izgledal takole (poleg tega bo vseboval tudi metode `render`, `handleUnlike` itd.): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private 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'); - } - } -} -``` - -Predloga komponente: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>všeč mi je</a> - {else} - <a n:href="unlike!" class=ajax>ni mi več všeč</a> - {/if} -{/snippet} -``` - -Seveda se nam bo spremenila predloga pogleda (view) in v presenter bomo morali dodati tovarno. Ker bomo komponento ustvarili tolikokrat, kolikor člankov pridobimo iz podatkovne baze, bomo za njeno "razmnoževanje" uporabili razred [Multiplier |application:multiplier]. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Predloga pogleda (view) se zmanjša na nujni minimum (in je popolnoma brez snippetov!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Skoraj smo končali: aplikacija bo zdaj delovala AJAX-ovsko. Tudi tukaj nas čaka optimizacija aplikacije, saj se zaradi uporabe Nette Database pri obdelavi signala nepotrebno naložijo vsi članki iz podatkovne baze namesto enega. Prednost pa je, da ne pride do njihovega izrisovanja, ker se dejansko izriše samo naša komponenta. - -{{priority: -1}} diff --git a/best-practices/sl/editors-and-tools.texy b/best-practices/sl/editors-and-tools.texy deleted file mode 100644 index 9ac5d2ff3c..0000000000 --- a/best-practices/sl/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Urejevalniki & orodja -********************* - -.[perex] -Lahko ste spreten programer, vendar šele z dobrimi orodji postanete mojster. V tem poglavju boste našli nasvete za pomembna orodja, urejevalnike in vtičnike. - - -IDE urejevalnik -=============== - -Vsekakor priporočamo, da za razvoj uporabljate polnopravno IDE, kot so na primer PhpStorm, NetBeans, VS Code, in ne le urejevalnika besedil s podporo za PHP. Razlika je resnično bistvena. Ni razloga, da bi se zadovoljili zgolj z urejevalnikom, ki sicer zna obarvati sintakso, vendar ne dosega zmožnosti vrhunskega IDE-ja, ki natančno predlaga, preverja napake, zna refaktorirati kodo in še veliko več. Nekateri IDE-ji so plačljivi, drugi celo brezplačni. - -**NetBeans IDE** ima podporo za Nette, Latte in NEON že vgrajeno. - -**PhpStorm**: namestite te vtičnike v `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: v tržnici (marketplace) poiščite vtičnik "Nette Latte + Neon". - -Povežite tudi Tracy z urejevalnikom. Pri prikazu strani z napako bo potem mogoče klikniti na imena datotek, ki se bodo odprla v urejevalniku s kazalcem na ustrezni vrstici. Preberite, [kako konfigurirati sistem|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan je orodje, ki odkrije logične napake v kodi, preden jo zaženete. - -Namestimo ga s pomočjo Composerja: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -V projektu ustvarimo konfiguracijsko datoteko `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -Nato pustimo, da analizira razrede v mapi `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Izčrpno dokumentacijo najdete neposredno na [straneh PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] preveri in po potrebi popravi nekatere formalne napake v vaši izvorni kodi: - -- odstranjuje [BOM |nette:glossary#BOM] -- preverja veljavnost predlog [Latte |latte:] -- preverja veljavnost datotek `.neon`, `.php` in `.json` -- preverja pojav [kontrolnih znakov |nette:glossary#Kontrolni znaki] -- preverja, ali je datoteka kodirana v UTF-8 -- preverja napačno zapisane `/* @anotacije */` (manjka zvezdica) -- odstranjuje zaključno oznako `?>` pri PHP datotekah -- odstranjuje presledke na desni strani in nepotrebne vrstice na koncu datoteke -- normalizira ločila vrstic na sistemska (če navedete možnost `-l`) - - -Composer -======== - -[Composer |best-practices:composer] je orodje za upravljanje odvisnosti v PHP. Omogoča nam deklariranje poljubno zapletenih odvisnosti posameznih knjižnic in jih nato za nas namesti v naš projekt. - - -Requirements Checker -==================== - -To je bilo orodje, ki je testiralo izvajalno okolje strežnika in obveščalo, ali (in v kolikšni meri) je mogoče ogrodje uporabljati. Trenutno je Nette mogoče uporabljati na vsakem strežniku, ki ima minimalno zahtevano različico PHP. diff --git a/best-practices/sl/form-reuse.texy b/best-practices/sl/form-reuse.texy deleted file mode 100644 index 4a388acd07..0000000000 --- a/best-practices/sl/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Ponovna uporaba obrazcev na več mestih -************************************** - -.[perex] -V Nette imate na voljo več možnosti, kako uporabiti isti obrazec na več mestih in ne podvajati kode. V tem članku si bomo ogledali različne rešitve, vključno s tistimi, ki se jim morate izogibati. - - -Tovarna obrazcev -================ - -Eden od osnovnih pristopov k uporabi iste komponente na več mestih je ustvarjanje metode ali razreda, ki to komponento generira, in nato klicanje te metode na različnih mestih aplikacije. Takšni metodi ali razredu pravimo *tovarna*. Prosimo, ne zamenjujte z oblikovalskim vzorcem *factory method*, ki opisuje specifičen način uporabe tovarn in ni povezan s to temo. - -Kot primer bomo ustvarili tovarno, ki bo sestavljala urejevalni obrazec: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - return $form; - } -} -``` - -Zdaj lahko to tovarno uporabite na različnih mestih v vaši aplikaciji, na primer v presenterjih ali komponentah. In sicer tako, da jo [zahtevamo kot odvisnost|dependency-injection:passing-dependencies]. Najprej torej razred zapišemo v konfiguracijsko datoteko: - -```neon -services: - - FormFactory -``` - -Nato jo uporabimo v presenterju: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // obdelava poslanih podatkov - }; - return $form; - } -} -``` - -Tovarno obrazcev lahko razširite z dodatnimi metodami za ustvarjanje drugih vrst obrazcev glede na potrebe vaše aplikacije. In seveda lahko dodamo tudi metodo, ki ustvari osnovni obrazec brez elementov, in to bodo uporabljale druge metode: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - return $form; - } -} -``` - -Metoda `createForm()` zaenkrat ne počne ničesar uporabnega, vendar se bo to hitro spremenilo. - - -Odvisnosti tovarne -================== - -Sčasoma se bo izkazalo, da potrebujemo, da so obrazci večjezični. To pomeni, da moramo vsem obrazcem nastaviti t.i. [prevajalnik |forms:rendering#Prevajanje]. V ta namen bomo prilagodili razred `FormFactory`, da bo sprejemal objekt `Translator` kot odvisnost v konstruktorju, in ga posredovali obrazcu: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Ker metodo `createForm()` kličejo tudi druge metode, ki ustvarjajo specifične obrazce, je dovolj, da prevajalnik nastavimo samo v njej. In končali smo. Ni treba spreminjati kode nobenega presenterja ali komponente, kar je odlično. - - -Več tovarniških razredov -======================== - -Alternativno lahko ustvarite več razredov za vsak obrazec, ki ga želite uporabiti v svoji aplikaciji. Ta pristop lahko poveča berljivost kode in olajša upravljanje obrazcev. Prvotno `FormFactory` bomo pustili, da ustvarja samo čist obrazec z osnovno konfiguracijo (na primer s podporo za prevode), za urejevalni obrazec pa bomo ustvarili novo tovarno `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ uporaba kompozicije -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - return $form; - } -} -``` - -Zelo pomembno je, da je povezava med razredoma `FormFactory` in `EditFormFactory` realizirana s [kompozicijo |nette:introduction-to-object-oriented-programming#Kompozicija], ne pa z [objektnim dedovanjem |nette:introduction-to-object-oriented-programming#Dedovanje]: - -```php -// ⛔ TAKOLE NE! SEM DEDOVANJE NE SPADA -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - return $form; - } -} -``` - -Uporaba dedovanja bi bila v tem primeru popolnoma kontraproduktivna. Na težave bi naleteli zelo hitro. Na primer v trenutku, ko bi želeli metodi `create()` dodati parametre; PHP bi javil napako, da se njena signatura razlikuje od starševske. Ali pri posredovanju odvisnosti v razred `EditFormFactory` prek konstruktorja. Nastala bi situacija, ki ji pravimo [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. - -Na splošno je bolje dati prednost [kompoziciji pred dedovanjem |dependency-injection:faq#Zakaj se daje prednost kompoziciji pred dedovanjem]. - - -Obdelava obrazca -================ - -Obdelava obrazca, ki se pokliče po uspešnem pošiljanju, je lahko tudi del tovarniškega razreda. Delovala bo tako, da bo poslana podatke posredovala modelu v obdelavo. Morebitne napake [posreduje nazaj |forms:validation#Napake pri obdelavi] v obrazec. Model v naslednjem primeru predstavlja razred `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // obdelava poslanih podatkov - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Samo preusmeritev pa bomo prepustili presenterju. Ta bo dogodku `onSuccess` dodal še en handler, ki bo izvedel preusmeritev. Zaradi tega bo mogoče obrazec uporabiti v različnih presenterjih in v vsakem preusmeriti drugam. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Zapis je bil shranjen'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Ta rešitev izkorišča lastnost obrazcev, da ko se nad obrazcem ali njegovim elementom pokliče `addError()`, se naslednji handler `onSuccess` ne pokliče več. - - -Dedovanje od razreda Form -========================= - -Sestavljen obrazec ne sme biti potomec obrazca. Z drugimi besedami, ne uporabljajte te rešitve: - -```php -// ⛔ TAKOLE NE! SEM DEDOVANJE NE SPADA -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $this->addSubmit('send', 'Pošlji'); - $this->setTranslator($translator); - } -} -``` - -Namesto sestavljanja obrazca v konstruktorju uporabite tovarno. - -Treba se je zavedati, da je razred `Form` v prvi vrsti orodje za sestavljanje obrazca, torej *form builder*. In sestavljen obrazec lahko razumemo kot njen produkt. Vendar produkt ni specifičen primer graditelja (builder), med njimi ni povezave *is a*, ki tvori osnovo dedovanja. - - -Komponenta z obrazcem -===================== - -Popolnoma drugačen pristop predstavlja ustvarjanje [komponente|application:components], katere del je obrazec. To daje nove možnosti, na primer izrisovanje obrazca na specifičen način, saj je del komponente tudi predloga. Ali pa je mogoče uporabiti signale za AJAX komunikacijo in nalaganje informacij v obrazec, na primer za predlaganje itd. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Naslov:'); - // tukaj se dodajajo dodatna polja obrazca - $form->addSubmit('send', 'Pošlji'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // obdelava poslanih podatkov - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // sprožitev dogodka - $this->onSave($this, $data); - } -} -``` - -Ustvarili bomo še tovarno, ki bo izdelovala to komponento. Dovolj je [zapisati njen vmesnik |application:components#Komponente z odvisnostmi]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -In dodati v konfiguracijsko datoteko: - -```neon -services: - - EditControlFactory -``` - -In zdaj lahko že zahtevamo tovarno in jo uporabimo v presenterju: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // ali preusmerimo na rezultat urejanja, npr.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/sl/inject-method-attribute.texy b/best-practices/sl/inject-method-attribute.texy deleted file mode 100644 index e09c5e2471..0000000000 --- a/best-practices/sl/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Metode in atributi inject -************************* - -.[perex] -V tem članku se bomo osredotočili na različne načine posredovanja odvisnosti v presenterje v ogrodju Nette. Primerjali bomo prednostni način, ki je konstruktor, z drugimi možnostmi, kot so metode in atributi `inject`. - -Tudi za presenterje velja, da je posredovanje odvisnosti s pomočjo [konstruktorja |dependency-injection:passing-dependencies#Predajanje s konstruktorjem] prednostna pot. Če pa ustvarjate skupnega prednika, od katerega dedujejo drugi presenterji (npr. `BasePresenter`), in ta prednik ima tudi odvisnosti, nastane problem, ki mu pravimo [constructor hell |dependency-injection:passing-dependencies#Constructor hell]. Temu se lahko izognemo z alternativnimi potmi, ki jih predstavljajo metode in atributi (anotacije) `inject`. - - -Metode `inject*()` -================== - -Gre za obliko posredovanja odvisnosti s [setterjem |dependency-injection:passing-dependencies#Predajanje s setterjem]. Ime teh setterjev se začne s predpono `inject`. Nette DI tako poimenovane metode samodejno pokliče takoj po ustvarjanju instance presenterja in jim posreduje vse zahtevane odvisnosti. Zato morajo biti deklarirane kot public. - -Metode `inject*()` lahko štejemo za nekakšno razširitev konstruktorja v več metod. Zahvaljujoč temu lahko `BasePresenter` prevzame odvisnosti prek druge metode in pusti konstruktor prost za svoje potomce: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Presenter lahko vsebuje poljubno število metod `inject*()` in vsaka lahko ima poljubno število parametrov. Odlično se obnesejo tudi v primerih, ko je presenter [sestavljen iz lastnosti (trait) |presenter-traits] in vsaka od njih zahteva svojo odvisnost. - - -Atributi `Inject` -================= - -Gre za obliko [injiciranja v lastnost |dependency-injection:passing-dependencies#Nastavitev spremenljivke]. Dovolj je označiti, v katere spremenljivke naj se injicira, in Nette DI samodejno posreduje odvisnosti takoj po ustvarjanju instance presenterja. Da jih lahko vstavi, jih je treba deklarirati kot public. - -Lastnosti označimo z atributom: (prej se je uporabljala anotacija `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // ta vrstica je pomembna - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Prednost tega načina posredovanja odvisnosti je bila zelo varčna oblika zapisa. Vendar pa se z uvedbo [constructor property promotion |https://blog.nette.org/sl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] zdi lažje uporabiti konstruktor. - -Nasprotno pa ta način trpi za enakimi pomanjkljivostmi kot posredovanje odvisnosti v lastnosti (properties) na splošno: nimamo nadzora nad spremembami v spremenljivki in hkrati spremenljivka postane del javnega vmesnika razreda, kar je nezaželeno. diff --git a/best-practices/sl/lets-create-contact-form.texy b/best-practices/sl/lets-create-contact-form.texy deleted file mode 100644 index ed3d4baf0b..0000000000 --- a/best-practices/sl/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Ustvarjamo kontaktni obrazec -**************************** - -.[perex] -Pogledali si bomo, kako v Nette ustvariti kontaktni obrazec, vključno s pošiljanjem na e-pošto. Pa začnimo! - -Najprej moramo ustvariti nov projekt. Kako to storiti, pojasnjuje stran [Začenjamo |nette:installation]. Nato pa lahko že začnemo z ustvarjanjem obrazca. - -Najenostavneje je ustvariti [obrazec neposredno v presenterju |forms:in-presenter]. Lahko uporabimo vnaprej pripravljen `HomePresenter`. Vanjo dodamo komponento `contactForm`, ki predstavlja obrazec. To storimo tako, da v kodo zapišemo tovarniško metodo `createComponentContactForm()`, ki bo komponento izdelala: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', 'Ime:') - ->setRequired('Vnesite ime'); - $form->addEmail('email', 'E-pošta:') - ->setRequired('Vnesite e-pošto'); - $form->addTextarea('message', 'Sporočilo:') - ->setRequired('Vnesite sporočilo'); - $form->addSubmit('send', 'Pošlji'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, stdClass $data): void - { - // pošiljanje e-pošte - } -} -``` - -Kot vidite, smo ustvarili dve metodi. Prva metoda `createComponentContactForm()` ustvari nov obrazec. Ta ima polja za ime, e-pošto in sporočilo, ki jih dodajamo z metodami `addText()`, `addEmail()` in `addTextArea()`. Dodali smo tudi gumb za pošiljanje obrazca. Kaj pa, če uporabnik ne izpolni katerega od polj? V takem primeru bi mu morali sporočiti, da je to obvezno polje. To smo dosegli z metodo `setRequired()`. Na koncu smo dodali tudi [dogodek |nette:glossary#Dogodki eventi] `onSuccess`, ki se sproži, če je obrazec uspešno poslan. V našem primeru pokliče metodo `contactFormSucceeded`, ki poskrbi za obdelavo poslanega obrazca. To bomo v kodo dodali čez trenutek. - -Komponento `contactForm` bomo pustili izrisati v predlogi `Home/default.latte`: - -```latte -{block content} -<h1>Kontaktni obrazec</h1> -{control contactForm} -``` - -Za samo pošiljanje e-pošte bomo ustvarili nov razred, ki ga bomo poimenovali `ContactFacade` in ga postavili v datoteko `app/Model/ContactFacade.php`: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // vaša e-pošta - ->setFrom($email, $name) - ->setSubject('Sporočilo iz kontaktnega obrazca') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Metoda `sendMessage()` ustvari in pošlje e-pošto. Za to uporablja t.i. mailer, ki si ga pusti posredovati kot odvisnost prek konstruktorja. Preberite več o [pošiljanju e-pošte |mail:]. - -Zdaj se vrnemo nazaj k presenterju in dokončamo metodo `contactFormSucceeded()`. Ta pokliče metodo `sendMessage()` razreda `ContactFacade` in ji posreduje podatke iz obrazca. In kako pridobimo objekt `ContactFacade`? Pustimo si ga posredovati s konstruktorjem: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Sporočilo je bilo poslano'); - $this->redirect('this'); - } -} -``` - -Ko je e-pošta poslana, uporabniku prikažemo še t.i. [flash sporočilo |application:components#Flash sporočila], ki potrjuje, da je bilo sporočilo poslano, nato pa preusmerimo na naslednjo stran, da obrazca ni mogoče ponovno poslati s pomočjo *refresh* v brskalniku. - - -Tako, in če vse deluje, bi morali biti sposobni poslati e-pošto iz vašega kontaktnega obrazca. Čestitam! - - -HTML predloga e-pošte ---------------------- - -Zaenkrat se pošilja navadno besedilno e-sporočilo, ki vsebuje samo sporočilo, poslano z obrazcem. V e-pošti pa lahko uporabimo HTML in naredimo njen videz privlačnejši. Zanjo bomo ustvarili predlogo v Latte, ki jo bomo zapisali v `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Sporočilo iz kontaktnega obrazca - - -

    Ime: {$name}

    -

    E-pošta: {$email}

    -

    Sporočilo: {$message}

    - - -``` - -Ostane še prilagoditi `ContactFacade`, da bo uporabljal to predlogo. V konstruktorju bomo zahtevali razred `LatteFactory`, ki zna izdelati objekt `Latte\Engine`, torej [izrisovalnik Latte predlog |latte:develop#Kako izrisati predlogo]. S pomočjo metode `renderToString()` bomo predlogo izrisali v datoteko, prvi parameter je pot do predloge, drugi pa so spremenljivke. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // vaša e-pošta - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Generirano HTML e-pošto nato posredujemo metodi `setHtmlBody()` namesto prvotni `setBody()`. Prav tako nam ni treba navajati zadeve e-pošte v `setSubject()`, ker si jo bo knjižnica vzela iz elementa `` predloge. - - -Konfiguracija -------------- - -V kodi razreda `ContactFacade` je še vedno trdo kodiran naš administratorski e-naslov `admin@example.com`. Bolje bi bilo, da ga premaknemo v konfiguracijsko datoteko. Kako to storiti? - -Najprej prilagodimo razred `ContactFacade` in niz z e-pošto nadomestimo s spremenljivko, posredovano s konstruktorjem: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -Drugi korak pa je navedba vrednosti te spremenljivke v konfiguraciji. V datoteko `app/config/services.neon` zapišemo: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -In to je to. Če bi bilo elementov v odseku `services` veliko in bi imeli občutek, da se e-pošta med njimi izgublja, jo lahko naredimo za spremenljivko. Prilagodimo zapis na: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -In v datoteki `app/config/common.neon` definiramo to spremenljivko: - -```neon -parameters: - adminEmail: admin@example.com -``` - -In končano! diff --git a/best-practices/sl/microsites.texy b/best-practices/sl/microsites.texy deleted file mode 100644 index fd4a5be3f2..0000000000 --- a/best-practices/sl/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Kako pisati mikro-spletna mesta -******************************* - -Predstavljajte si, da morate hitro ustvariti majhno spletno mesto za prihajajoči dogodek vašega podjetja. Mora biti preprosto, hitro in brez nepotrebnih zapletov. Morda mislite, da za tako majhen projekt ne potrebujete robustnega ogrodja. Kaj pa, če lahko uporaba ogrodja Nette ta proces bistveno poenostavi in pospeši? - -Saj se tudi pri ustvarjanju preprostih spletnih mest nočete odreči udobju. Nočete izumljati tistega, kar je bilo že enkrat rešeno. Bodite mirno leni in se pustite razvajati. Nette Framework lahko odlično uporabite tudi kot mikro ogrodje. - -Kako lahko izgleda takšno mikro-spletno mesto? Na primer tako, da celotno kodo spletnega mesta postavimo v eno samo datoteko `index.php` v javni mapi: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// ustvari DI vsebnik na podlagi konfiguracije v config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// nastavimo usmerjanje (routing) -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// pot za URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // zaznamo jezik brskalnika in preusmerimo na URL /en ali /de itd. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// pot za URL https://example.com/cs ali https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // prikažemo ustrezno predlogo, na primer ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// zaženi aplikacijo! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Vse ostalo bodo predloge, shranjene v nadrejeni mapi `/templates`. - -PHP koda v `index.php` najprej [pripravi okolje |bootstrap:], nato definira [poti (route) |application:routing#Dinamično usmerjanje s povratnimi klici] in na koncu zažene aplikacijo. Prednost je, da je lahko drugi parameter funkcije `addRoute()` callable, ki se izvede po odprtju ustrezne strani. - - -Zakaj uporabljati Nette za mikro-spletna mesta? ------------------------------------------------ - -- Programerji, ki so kdaj preizkusili [Tracy|tracy:], si danes ne predstavljajo, da bi kaj programirali brez nje. -- Predvsem pa boste izkoristili sistem predlog [Latte|latte:], saj boste že od 2 strani želeli imeti ločeno [postavitev in vsebino|latte:template-inheritance]. -- In zagotovo se želite zanesti na [samodejno ubežanje znakov |latte:safety-first], da ne nastane ranljivost XSS -- Nette bo tudi zagotovil, da se ob napaki nikoli ne prikažejo programerska sporočila o napakah PHP, temveč uporabniku razumljiva stran. -- Če želite pridobivati povratne informacije od uporabnikov, na primer v obliki kontaktnega obrazca, boste dodali še [obrazce|forms:] in [podatkovno bazo|database:]. -- Izpolnjene obrazce si lahko prav tako enostavno [pošiljate po e-pošti|mail:]. -- Včasih vam lahko koristi [predpomnjenje|caching:], na primer če prenašate in prikazujete vire (feeds). - -V današnjem času, ko sta hitrost in učinkovitost ključnega pomena, je pomembno imeti orodja, ki vam omogočajo doseganje rezultatov brez nepotrebnega odlašanja. Ogrodje Nette vam ponuja prav to - hiter razvoj, varnost in široko paleto orodij, kot sta Tracy in Latte, ki poenostavljajo proces. Dovolj je namestiti nekaj Nette paketov in zgraditi takšno mikro-spletno mesto je naenkrat povsem enostavno. In veste, da se nikjer ne skriva nobena varnostna luknja. diff --git a/best-practices/sl/pagination.texy b/best-practices/sl/pagination.texy deleted file mode 100644 index f543008252..0000000000 --- a/best-practices/sl/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Stranskanje rezultatov podatkovne baze -************************************** - -.[perex] -Pri ustvarjanju spletnih aplikacij se zelo pogosto srečate z zahtevo po omejitvi števila izpisanih postavk na strani. - -Izhajali bomo iz stanja, ko izpisujemo vse podatke brez stranskanja. Za izbiro podatkov iz podatkovne baze imamo razred ArticleRepository, ki poleg konstruktorja vsebuje metodo `findPublishedArticles`, ki vrača vse objavljene članke, razvrščene padajoče po datumu objave. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $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 presenterju si nato injiciramo modelni razred in v render metodi zahtevamo objavljene članke, ki jih posredujemo v predlogo: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -V predlogi `default.latte` se nato poskrbimo za izpis člankov: - -```latte -{block content} -<h1>Članki</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Na ta način znamo izpisati vse članke, kar pa začne povzročati težave v trenutku, ko število člankov naraste. V tem trenutku pride prav implementacija mehanizma za stranskanje. - -Ta zagotovi, da se vsi članki razdelijo na več strani in mi prikažemo samo članke ene trenutne strani. Skupno število strani in razdelitev člankov si izračuna [utils:Paginator] sam glede na to, koliko člankov skupaj imamo in koliko člankov na stran želimo prikazati. - -V prvem koraku si prilagodimo metodo za pridobivanje člankov v razredu repozitorija tako, da nam zna vračati samo članke za eno stran. Dodamo tudi metodo za ugotavljanje skupnega števila člankov v podatkovni bazi, ki jo bomo potrebovali za nastavitev Paginatorja: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $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, - ); - } - - /** - * Vrača skupno število objavljenih člankov - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Nato se lotimo prilagoditev presenterja. V render metodo bomo posredovali številko trenutno prikazane strani. Za primer, ko ta številka ne bo del URL-ja, nastavimo privzeto vrednost prve strani. - -Nadalje render metodo razširimo še s pridobivanjem instance Paginatorja, njegovo nastavitvijo in izbiro pravilnih člankov za prikaz v predlogi. HomePresenter bo po prilagoditvah izgledal takole: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Ugotovimo skupno število objavljenih člankov - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Izdelamo instanco Paginatorja in jo nastavimo - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // skupno število člankov - $paginator->setItemsPerPage(10); // število postavk na stran - $paginator->setPage($page); // številka trenutne strani - - // Iz podatkovne baze izvlečemo omejeno množico člankov glede na izračun Paginatorja - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // ki jo posredujemo v predlogo - $this->template->articles = $articles; - // in tudi sam Paginator za prikaz možnosti stranskanja - $this->template->paginator = $paginator; - } -} -``` - -Predloga nam že zdaj iterira samo nad članki ene strani, dodati moramo le še povezave za stranskanje: - -```latte -{block content} -<h1>Članki</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Prva</a> -  |  - <a n:href="default, $paginator->page-1">Prejšnja</a> -  |  - {/if} - - Stran {$paginator->getPage()} od {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Naslednja</a> -  |  - <a n:href="default, $paginator->getPageCount()">Zadnja</a> - {/if} -</div> -``` - - -Tako smo stran dopolnili z možnostjo stranskanja s pomočjo Paginatorja. V primeru, ko namesto [Nette Database Core |database:sql-way] kot podatkovno plast uporabimo [Nette Database Explorer |database:explorer], smo sposobni implementirati stranskanje tudi brez uporabe Paginatorja. Razred `Nette\Database\Table\Selection` namreč vsebuje metodo [page |api:Nette\Database\Table\Selection::page] z logiko stranskanja, prevzeto iz Paginatorja. - -Repozitorij bo pri tem načinu implementacije izgledal takole: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -V presenterju nam ni treba ustvarjati Paginatorja, namesto njega uporabimo metodo razreda `Selection`, ki nam jo vrača repozitorij: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Izvlečemo objavljene članke - $articles = $this->articleRepository->findPublishedArticles(); - - // in v predlogo pošljemo samo njihov del, omejen glede na izračun metode page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // in tudi potrebne podatke za prikaz možnosti stranskanja - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Ker v predlogo zdaj ne pošiljamo Paginatorja, prilagodimo del, ki prikazuje povezave za stranskanje: - -```latte -{block content} -<h1>Članki</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Prva</a> -  |  - <a n:href="default, $page - 1">Prejšnja</a> -  |  - {/if} - - Stran {$page} od {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Naslednja</a> -  |  - <a n:href="default, $lastPage">Zadnja</a> - {/if} -</div> -``` - -Na ta način smo implementirali mehanizem za stranskanje brez uporabe Paginatorja. - -{{priority: -1}} diff --git a/best-practices/sl/passing-settings-to-presenters.texy b/best-practices/sl/passing-settings-to-presenters.texy deleted file mode 100644 index 8ced4b88f2..0000000000 --- a/best-practices/sl/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Posredovanje nastavitev v presenterje -************************************* - -.[perex] -Ali morate v presenterje posredovati argumente, ki niso objekti (npr. informacijo, ali teče v načinu za odpravljanje napak, poti do map itd.), in jih torej ni mogoče samodejno posredovati s pomočjo autowiringa? Rešitev je, da jih zapakirate v objekt `Settings`. - -Storitev `Settings` predstavlja zelo enostaven in hkrati uporaben način za zagotavljanje informacij o tekoči aplikaciji presenterjem. Njena konkretna oblika je odvisna izključno od vaših specifičnih potreb. Primer: - -```php -namespace App; - -class Settings -{ - public function __construct( - // od PHP 8.1 je mogoče navesti readonly - public bool $debugMode, - public string $appDir, - // in tako naprej - ) {} -} -``` - -Primer registracije v konfiguraciji: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Ko bo presenter potreboval informacije, ki jih zagotavlja ta storitev, jo bo preprosto zahteval v konstruktorju: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/sl/post-links.texy b/best-practices/sl/post-links.texy deleted file mode 100644 index 0b99158d78..0000000000 --- a/best-practices/sl/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Kako pravilno uporabljati POST povezave -*************************************** - -.[perex] -V spletnih aplikacijah, zlasti v administrativnih vmesnikih, bi moralo biti osnovno pravilo, da se akcije, ki spreminjajo stanje strežnika, ne izvajajo prek metode HTTP GET. Kot pove že ime metode, bi moral GET služiti samo za pridobivanje podatkov, ne pa za njihovo spreminjanje. Za akcije, kot je na primer brisanje zapisov, je primernejša uporaba metode POST. Čeprav bi bila idealna metoda DELETE, je te brez JavaScripta ni mogoče izvesti, zato se zgodovinsko uporablja POST. - -Kako to storiti v praksi? Uporabite ta preprost trik. Na začetku predloge si ustvarite pomožni obrazec z identifikatorjem `postForm`, ki ga nato uporabite za gumbe za brisanje: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Zahvaljujoč temu obrazcu lahko namesto klasične povezave `<a>` uporabite gumb `<button>`, ki ga lahko vizualno prilagodite tako, da izgleda kot običajna povezava. Na primer, CSS ogrodje Bootstrap ponuja razreda `btn btn-link`, s katerima dosežete, da gumb ne bo vizualno drugačen od ostalih povezav. Z atributom `form="postForm"` ga povežemo z vnaprej pripravljenim obrazcem: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">izbriši</button> - <!-- namesto <a n:href="delete $post->id">izbriši</a> --> - </td> - </tr> -</table> -``` - -Ob kliku na povezavo se zdaj izvede akcija `delete`. Za zagotovitev, da bodo zahteve sprejete samo prek metode POST in iz iste domene (kar je učinkovita obramba pred napadi CSRF), uporabite atribut `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // hipotetična koda, ki briše zapis - $this->redirect('default'); - } -} -``` - -Atribut obstaja od Nette Application 3.2 in več o njegovih možnostih boste izvedeli na strani [Kako uporabljati atribut #Requires |attribute-requires]. - -Če bi namesto akcije `actionDelete()` uporabljali signal `handleDelete()`, ni treba navajati `sameOrigin: true`, ker imajo signali to zaščito nastavljeno implicitno: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Ta pristop ne samo izboljšuje varnost vaše aplikacije, ampak tudi prispeva k spoštovanju pravilnih spletnih standardov in praks. Z uporabo metod POST za akcije, ki spreminjajo stanje, dosežete bolj robustno in varnejšo aplikacijo. diff --git a/best-practices/sl/presenter-traits.texy b/best-practices/sl/presenter-traits.texy deleted file mode 100644 index 894c0add0e..0000000000 --- a/best-practices/sl/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Sestavljanje presenterjev iz lastnosti (trait) -********************************************** - -.[perex] -Če moramo v več presenterjih implementirati isto kodo (npr. preverjanje, ali je uporabnik prijavljen), se ponuja možnost, da kodo postavimo v skupnega prednika. Druga možnost je ustvarjanje namensko usmerjenih [lastnosti (trait) |nette:introduction-to-object-oriented-programming#Lastnosti Traits]. - -Prednost te rešitve je, da lahko vsak od presenterjev uporabi točno tiste lastnosti (traits), ki jih dejansko potrebuje, medtem ko večkratno dedovanje v PHP ni mogoče. - -Te lastnosti (traits) lahko izkoristijo dejstvo, da se ob ustvarjanju presenterja postopoma pokličejo vse [inject metode |inject-method-attribute#Metode inject]. Paziti je treba le, da je ime vsake inject metode edinstveno. - -Lastnosti (traits) lahko pripnejo inicializacijsko kodo na dogodke [onStartup ali onRender |application:presenters#Dogodki]. - -Primeri: - -```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 nato te lastnosti (traits) preprosto uporabi: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/sl/restore-request.texy b/best-practices/sl/restore-request.texy deleted file mode 100644 index 6f81a2d371..0000000000 --- a/best-practices/sl/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Kako se vrniti na prejšnjo stran? -********************************* - -.[perex] -Kaj če uporabnik izpolnjuje obrazec in mu poteče prijava? Da ne bi izgubil podatkov, pred preusmeritvijo na prijavno stran podatke shranimo v sejo (session). V Nette je to povsem enostavno. - -Trenutno zahtevo lahko shranite v sejo s pomočjo metode `storeRequest()`, ki vrne njen identifikator v obliki kratkega niza. Metoda shrani ime trenutnega presenterja, pogled (view) in njegove parametre. V primeru, da je bil poslan tudi obrazec, se shrani tudi vsebina polj (z izjemo naloženih datotek). - -Obnovitev zahteve izvede metoda `restoreRequest($key)`, ki ji posredujemo pridobljeni identifikator. Ta preusmeri na prvotni presenter in pogled. Če pa shranjena zahteva vsebuje pošiljanje obrazca, na prvotni presenter preide z metodo `forward()`, obrazcu posreduje prej izpolnjene vrednosti in ga pusti ponovno izrisati. Uporabnik ima tako možnost obrazec ponovno poslati in nobeni podatki se ne izgubijo. - -Pomembno je, da `restoreRequest()` preveri, ali je novo prijavljeni uporabnik isti, kot tisti, ki je obrazec prvotno izpolnjeval. Če ne, zahtevo zavrže in ne naredi ničesar. - -Poglejmo si vse na primeru. Imejmo presenter `AdminPresenter`, v katerem se urejajo podatki in v njegovi metodi `startup()` preverjamo, ali je uporabnik prijavljen. Če ni, ga preusmerimo na `SignPresenter`. Hkrati shranimo trenutno zahtevo in njen ključ pošljemo v `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` bo poleg obrazca za prijavo vseboval tudi persistentni parameter `$backlink`, v katerega se zapiše ključ. Ker je parameter persistenten, se bo prenašal tudi po pošiljanju prijavnega obrazca. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... dodamo polja obrazca ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... tukaj uporabnika prijavimo ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Metodi `restoreRequest()` posredujemo ključ shranjene zahteve in ta preusmeri (ali preide) na prvotni presenter. - -Če pa je ključ neveljaven (na primer že ne obstaja v seji), metoda ne naredi ničesar. Sledi torej klic `$this->redirect('Admin:')`, ki preusmeri na `AdminPresenter`. - -{{priority: -1}} diff --git a/best-practices/uk/@home.texy b/best-practices/uk/@home.texy deleted file mode 100644 index f9d3510c2b..0000000000 --- a/best-practices/uk/@home.texy +++ /dev/null @@ -1,69 +0,0 @@ -Посібники та практики -********************* - -.[perex] -Посібники, рішення поширених завдань та *best practices* для Nette. - - -<div class=documentation> -<div> - - -Застосунки Nette ----------------- -- [Методи та атрибути inject |inject-method-attribute] -- [Складання презентерів з трейтів |presenter-traits] -- [Передача налаштувань до презентерів |passing-settings-to-presenters] -- [Як повернутися до попередньої сторінки |restore-request] -- [Пагінація результатів бази даних |pagination] -- [Динамічні сніпети |dynamic-snippets] -- [Як використовувати атрибут #Requires |attribute-requires] -- [Як правильно використовувати POST-посилання |post-links] - -</div> -<div> - - -Форми ------ -- [Повторне використання форм |form-reuse] -- [Форма для створення та редагування запису |creating-editing-form] -- [Створюємо контактну форму |lets-create-contact-form] -- [Залежні селектбокси |https://blog.nette.org/uk/dependent-selectboxes-elegantly-in-nette-and-pure-js] - -</div> -<div> - - -Загальне --------- -- [Як завантажити конфігураційний файл |bootstrap:] -- [Як писати мікро-сайти |microsites] -- [Чому Nette використовує PascalCase нотацію констант? |https://blog.nette.org/uk/for-less-screaming-in-the-code] -- [Чому Nette не використовує суфікс Interface? |https://blog.nette.org/uk/prefixes-and-suffixes-do-not-belong-in-interface-names] -- [Composer: поради щодо використання |composer] -- [Поради щодо редакторів та інструментів |editors-and-tools] -- [Вступ до об'єктно-орієнтованого програмування |nette:introduction-to-object-oriented-programming] - -</div> -<div> - - -Приклади рішень ---------------- -- [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] -- [Швидкий старт |quickstart:] - -</div> -<div> - - -Відео ------ -Сотні записів з Posledních sobot та відео про Nette ви знайдете під одним дахом на "Youtube каналі Nette Framework":https://www.youtube.com/user/NetteFramework. - -</div> -</div> diff --git a/best-practices/uk/@meta.texy b/best-practices/uk/@meta.texy deleted file mode 100644 index 5ad8bb9a6b..0000000000 --- a/best-practices/uk/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Посібники та практики}} -{{leftbar: www:@menu-common}} diff --git a/best-practices/uk/attribute-requires.texy b/best-practices/uk/attribute-requires.texy deleted file mode 100644 index f0d8d4a2d1..0000000000 --- a/best-practices/uk/attribute-requires.texy +++ /dev/null @@ -1,177 +0,0 @@ -Як використовувати атрибут `#[Requires]` -**************************************** - -.[perex] -Під час написання веб-додатку часто виникає потреба обмежити доступ до певних частин вашого додатку. Можливо, ви хочете, щоб деякі запити могли надсилати дані лише за допомогою форми (тобто методом POST), або щоб вони були доступні лише для AJAX-викликів. У Nette Framework 3.2 з'явився новий інструмент, який дозволяє встановити такі обмеження дуже елегантно та зрозуміло: атрибут `#[Requires]`. - -Атрибут — це спеціальна позначка в PHP, яку ви додаєте перед визначенням класу або методу. Оскільки це фактично клас, щоб наступні приклади працювали, необхідно вказати оператор use: - -```php -use Nette\Application\Attributes\Requires; -``` - -Атрибут `#[Requires]` можна використовувати для самого класу presenter'а, а також для таких методів: - -- `action<Action>()` -- `render<View>()` -- `handle<Signal>()` -- `createComponent<Name>()` - -Останні два методи стосуються також компонентів, отже, атрибут можна використовувати і для них. - -Якщо умови, зазначені в атрибуті, не виконані, буде викликано HTTP-помилку 4xx. - - -Методи HTTP ------------ - -Ви можете вказати, які HTTP-методи (наприклад, GET, POST тощо) дозволені для доступу. Наприклад, якщо ви хочете дозволити доступ лише шляхом надсилання форми, встановіть: - -```php -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST')] - public function actionDelete(int $id): void - { - } -} -``` - -Чому слід використовувати POST замість GET для дій, що змінюють стан, і як це зробити? [Прочитайте інструкцію |post-links]. - -Ви можете вказати метод або масив методів. Особливим випадком є значення `'*'`, яке дозволяє всі методи, що зазвичай presenter'и [з міркувань безпеки не дозволяють |application:presenters#Перевірка HTTP-методу]. - - -AJAX-виклики ------------- - -Якщо ви хочете, щоб presenter або метод був доступний лише для AJAX-запитів, використовуйте: - -```php -#[Requires(ajax: true)] -class AjaxPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Те саме походження ------------------- - -Для підвищення безпеки ви можете вимагати, щоб запит надходив з того самого домену. Це запобігає [вразливості CSRF |nette:vulnerability-protection#Cross-Site Request Forgery CSRF]: - -```php -#[Requires(sameOrigin: true)] -class SecurePresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Для методів `handle<Signal>()` доступ з того самого домену вимагається автоматично. Тому, якщо ви, навпаки, хочете дозволити доступ з будь-якого домену, вкажіть: - -```php -#[Requires(sameOrigin: false)] -public function handleList(): void -{ -} -``` - - -Доступ через forward --------------------- - -Іноді корисно обмежити доступ до presenter'а так, щоб він був доступний лише опосередковано, наприклад, за допомогою методу `forward()` або `switch()` з іншого presenter'а. Таким чином, наприклад, захищаються error-presenter'и, щоб їх не можна було викликати з URL: - -```php -#[Requires(forward: true)] -class ForwardedPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -На практиці часто буває необхідно позначити певні views, до яких можна отримати доступ лише на основі логіки в presenter'і. Тобто, знову ж таки, щоб їх не можна було відкрити безпосередньо: - -```php -class ProductPresenter extends Nette\Application\UI\Presenter -{ - - public function actionDefault(int $id): void - { - $product = $this->facade->getProduct($id); - if (!$product) { - $this->setView('notfound'); - } - } - - #[Requires(forward: true)] - public function renderNotFound(): void - { - } -} -``` - - -Конкретні дії -------------- - -Ви також можете обмежити, щоб певний код, наприклад, створення компонента, був доступний лише для специфічних дій у presenter'і: - -```php -class EditDeletePresenter extends Nette\Application\UI\Presenter -{ - #[Requires(actions: ['add', 'edit'])] - public function createComponentPostForm() - { - } -} -``` - -У випадку однієї дії не потрібно записувати масив: `#[Requires(actions: 'default')]` - - -Власні атрибути ---------------- - -Якщо ви хочете використовувати атрибут `#[Requires]` повторно з тими самими налаштуваннями, ви можете створити власний атрибут, який успадковуватиме `#[Requires]` і налаштує його відповідно до потреб. - -Наприклад, `#[SingleAction]` дозволить доступ лише через дію `default`: - -```php -#[\Attribute] -class SingleAction extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(actions: 'default'); - } -} - -#[SingleAction] -class SingleActionPresenter extends Nette\Application\UI\Presenter -{ -} -``` - -Або `#[RestMethods]` дозволить доступ через усі HTTP-методи, що використовуються для REST API: - -```php -#[\Attribute] -class RestMethods extends Nette\Application\Attributes\Requires -{ - public function __construct() - { - parent::__construct(methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); - } -} - -#[RestMethods] -class ApiPresenter extends Nette\Application\UI\Presenter -{ -} -``` - - -Висновок --------- - -Атрибут `#[Requires]` надає вам велику гнучкість і контроль над тим, як доступні ваші веб-сторінки. За допомогою простих, але потужних правил ви можете підвищити безпеку та правильне функціонування вашого додатку. Як бачите, використання атрибутів у Nette може не тільки полегшити вашу роботу, але й зробити її безпечнішою. diff --git a/best-practices/uk/composer.texy b/best-practices/uk/composer.texy deleted file mode 100644 index a237caf401..0000000000 --- a/best-practices/uk/composer.texy +++ /dev/null @@ -1,282 +0,0 @@ -Composer: поради щодо використання -********************************** - -<div class=perex> - -Composer — це інструмент для керування залежностями в PHP. Він дозволяє нам перерахувати бібліотеки, від яких залежить наш проект, і буде встановлювати та оновлювати їх за нас. Ми покажемо: - -- як встановити Composer -- його використання в новому або існуючому проекті - -</div> - - -Встановлення -============ - -Composer — це виконуваний файл `.phar`, який ви завантажуєте та встановлюєте наступним чином: - - -Windows -------- - -Використовуйте офіційний інсталятор [Composer-Setup.exe |https://getcomposer.org/Composer-Setup.exe]. - - -Linux, macOS ------------- - -Достатньо 4 команд, які ви можете скопіювати з [цієї сторінки |https://getcomposer.org/download/]. - -Потім, розмістивши його в папці, яка знаходиться в системному `PATH`, Composer стане доступним глобально: - -```shell -$ mv ./composer.phar ~/bin/composer # або /usr/local/bin/composer -``` - - -Використання в проекті -====================== - -Щоб почати використовувати Composer у своєму проекті, вам потрібен лише файл `composer.json`. Він описує залежності вашого проекту і може також містити інші метадані. Базовий `composer.json` може виглядати так: - -```js -{ - "require": { - "nette/database": "^3.0" - } -} -``` - -Тут ми вказуємо, що наш додаток (або бібліотека) вимагає пакет `nette/database` (назва пакета складається з назви організації та назви проекту) і хоче версію, яка відповідає умові `^3.0` (тобто найновішу версію 3). - -Отже, у нас є файл `composer.json` у корені проекту, і ми запускаємо встановлення: - -```shell -composer update -``` - -Composer завантажить Nette Database у папку `vendor/`. Потім він створить файл `composer.lock`, який містить інформацію про те, які саме версії бібліотек він встановив. - -Composer згенерує файл `vendor/autoload.php`, який ми можемо просто підключити і почати використовувати бібліотеки без будь-якої додаткової роботи: - -```php -require __DIR__ . '/vendor/autoload.php'; - -$db = new Nette\Database\Connection('sqlite::memory:'); -``` - - -Оновлення пакетів до останніх версій -==================================== - -Оновлення використовуваних бібліотек до останніх версій відповідно до умов, визначених у `composer.json`, здійснюється командою `composer update`. Наприклад, для залежності `"nette/database": "^3.0"` буде встановлена остання версія 3.x.x, але не версія 4. - -Для оновлення умов у файлі `composer.json`, наприклад, до `"nette/database": "^4.1"`, щоб можна було встановити останню версію, використовуйте команду `composer require nette/database`. - -Для оновлення всіх використовуваних пакетів Nette необхідно було б перерахувати їх усі в командному рядку, наприклад: - -```shell -composer require nette/application nette/forms latte/latte tracy/tracy ... -``` - -Що непрактично. Тому використовуйте простий скрипт "Composer Frontline":https://gist.github.com/dg/734bebf55cf28ad6a5de1156d3099bff, який зробить це за вас: - -```shell -php composer-frontline.php -``` - - -Створення нового проекту -======================== - -Новий проект на Nette створюється за допомогою однієї команди: - -```shell -composer create-project nette/web-project nazev-projektu -``` - -Як `nazev-projektu` введіть назву каталогу для свого проекту та підтвердіть. Composer завантажить репозиторій `nette/web-project` з GitHub, який вже містить файл `composer.json`, а потім одразу Nette Framework. Повинно бути достатньо лише [встановити права |nette:troubleshooting#Налаштування прав доступу до каталогів] на запис у папки `temp/` та `log/`, і проект має запрацювати. - -Якщо ви знаєте, на якій версії PHP буде розміщено проект, не забудьте [її встановити |#Версія PHP]. - - -Версія PHP -========== - -Composer завжди встановлює ті версії пакетів, які сумісні з версією PHP, яку ви зараз використовуєте (точніше, з версією PHP, що використовується в командному рядку під час запуску Composer). Однак це, швидше за все, не та сама версія, яку використовує ваш хостинг. Тому дуже важливо додати до файлу `composer.json` інформацію про версію PHP на хостингу. Після цього будуть встановлюватися лише ті версії пакетів, які сумісні з хостингом. - -Те, що проект працюватиме, наприклад, на PHP 8.2.3, встановлюється командою: - -```shell -composer config platform.php 8.2.3 -``` - -Таким чином версія запишеться у файл `composer.json`: - -```js -{ - "config": { - "platform": { - "php": "8.2.3" - } - } -} -``` - -Однак номер версії PHP вказується ще в одному місці файлу, а саме в секції `require`. У той час як перше число визначає, для якої версії будуть встановлюватися пакети, друге число вказує, для якої версії написаний сам додаток. І за ним, наприклад, PhpStorm встановлює *PHP language level*. (Звичайно, немає сенсу, щоб ці версії відрізнялися, тому подвійний запис є недоліком.) Цю версію встановлюють командою: - -```shell -composer require php 8.2.3 --no-update -``` - -Або безпосередньо у файлі `composer.json`: - -```js -{ - "require": { - "php": "8.2.3" - } -} -``` - - -Ігнорування версії PHP -====================== - -Пакети зазвичай вказують як найнижчу версію PHP, з якою вони сумісні, так і найвищу, з якою вони протестовані. Якщо ви збираєтеся використовувати ще новішу версію PHP, наприклад, для тестування, Composer відмовиться встановлювати такий пакет. Рішенням є опція `--ignore-platform-req=php+`, яка змусить Composer ігнорувати верхні межі необхідної версії PHP. - - -Хибні повідомлення -================== - -Під час оновлення пакетів або зміни номерів версій трапляється, що виникає конфлікт. Один пакет має вимоги, які суперечать іншому, і так далі. Однак Composer іноді видає хибні повідомлення. Він повідомляє про конфлікт, якого насправді не існує. У такому випадку допоможе видалити файл `composer.lock` і спробувати ще раз. - -Якщо повідомлення про помилку залишається, то воно серйозне, і потрібно з нього зрозуміти, що і як виправити. - - -Packagist.org - центральний репозиторій -======================================= - -[Packagist |https://packagist.org] — це головний репозиторій, у якому Composer намагається шукати пакети, якщо йому не вказано інше. Ми також можемо публікувати тут власні пакети. - - -Що робити, якщо ми не хочемо використовувати центральний репозиторій? ---------------------------------------------------------------------- - -Якщо у нас є внутрішньокорпоративні додатки, які ми просто не можемо розміщувати публічно, то ми створимо для них корпоративний репозиторій. - -Більше на тему репозиторіїв [в офіційній документації |https://getcomposer.org/doc/05-repositories.md#repositories]. - - -Автозавантаження -================ - -Ключовою особливістю Composer є те, що він забезпечує автозавантаження для всіх встановлених ним класів, яке ви запускаєте, підключивши файл `vendor/autoload.php`. - -Однак можна використовувати Composer і для завантаження інших класів поза папкою `vendor`. Перший варіант — дозволити Composer просканувати визначені папки та підпапки, знайти всі класи та включити їх до автозавантажувача. Цього можна досягти, налаштувавши `autoload > classmap` у `composer.json`: - -```js -{ - "autoload": { - "classmap": [ - "src/", # включить папку src/ та її підпапки - ] - } -} -``` - -Після цього необхідно при кожній зміні запускати команду `composer dumpautoload` і перегенерувати таблиці автозавантаження. Це надзвичайно незручно, і набагато краще доручити це завдання [RobotLoader|robot-loader:], який виконує ту саму дію автоматично у фоновому режимі та набагато швидше. - -Другий варіант — дотримуватися [PSR-4|https://www.php-fig.org/psr/psr-4/]. Спрощено кажучи, це система, де простори імен та назви класів відповідають структурі каталогів та назвам файлів, тобто, наприклад, `App\Core\RouterFactory` буде знаходитись у файлі `/path/to/App/Core/RouterFactory.php`. Приклад конфігурації: - -```js -{ - "autoload": { - "psr-4": { - "App\\": "app/" # простір імен App\ знаходиться в каталозі app/ - } - } -} -``` - -Як саме налаштувати поведінку, ви дізнаєтеся в [документації Composer|https://getcomposer.org/doc/04-schema.md#psr-4]. - - -Тестування нових версій -======================= - -Ви хочете протестувати нову розробницьку версію пакета. Як це зробити? Спочатку додайте до файлу `composer.json` цю пару опцій, яка дозволить встановлювати розробницькі версії пакетів, але вдасться до цього лише в тому випадку, якщо не існує жодної комбінації стабільних версій, яка б задовольняла вимогам: - -```js -{ - "minimum-stability": "dev", - "prefer-stable": true, -} -``` - -Далі рекомендуємо видалити файл `composer.lock`, іноді Composer незрозуміло відмовляється від встановлення, і це вирішує проблему. - -Припустимо, йдеться про пакет `nette/utils`, і нова версія має номер 4.0. Встановіть її командою: - -```shell -composer require nette/utils:4.0.x-dev -``` - -Або ви можете встановити конкретну версію, наприклад, 4.0.0-RC2: - -```shell -composer require nette/utils:4.0.0-RC2 -``` - -Але якщо від бібліотеки залежить інший пакет, який заблокований на старішій версії (наприклад, `^3.1`), то ідеально оновити цей пакет, щоб він працював з новою версією. Якщо ж ви хочете просто обійти обмеження і змусити Composer встановити розробницьку версію, видаючи її за старішу (наприклад, 3.1.6), ви можете використати ключове слово `as`: - -```shell -composer require nette/utils "4.0.x-dev as 3.1.6" -``` - - -Виклик команд -============= - -Через Composer можна викликати власні підготовлені команди та скрипти, ніби це нативні команди Composer. Для скриптів, що знаходяться в папці `vendor/bin`, не потрібно вказувати цю папку. - -Як приклад, визначимо в файлі `composer.json` скрипт, який за допомогою [Nette Tester|tester:] запустить тести: - -```js -{ - "scripts": { - "tester": "tester tests -s" - } -} -``` - -Тести потім запустимо за допомогою `composer tester`. Команду можна викликати, навіть якщо ми не знаходимося в кореневій папці проекту, а в якомусь підкаталозі. - - -Надішліть подяку -================ - -Ми покажемо вам трюк, яким ви порадуєте авторів open source. Простим способом ви поставите зірочку на GitHub бібліотекам, які використовує ваш проект. Достатньо встановити бібліотеку `symfony/thanks`: - -```shell -composer global require symfony/thanks -``` - -А потім запустити: - -```shell -composer thanks -``` - -Спробуйте! - - -Конфігурація -============ - -Composer тісно пов'язаний з інструментом версіонування [Git |https://git-scm.com]. Якщо він у вас не встановлений, потрібно сказати Composer, щоб він його не використовував: - -```shell -composer -g config preferred-install dist -``` diff --git a/best-practices/uk/creating-editing-form.texy b/best-practices/uk/creating-editing-form.texy deleted file mode 100644 index 415142d3ae..0000000000 --- a/best-practices/uk/creating-editing-form.texy +++ /dev/null @@ -1,205 +0,0 @@ -Форма для створення та редагування запису -***************************************** - -.[perex] -Як правильно реалізувати в Nette додавання та редагування запису, використовуючи для обох операцій одну й ту саму форму? - -У багатьох випадках форми для додавання та редагування запису однакові, відрізняючись, наприклад, лише написом на кнопці. Ми покажемо приклади простих презентерів, де форму спочатку використаємо для додавання запису, потім для редагування, і нарешті об'єднаємо обидва рішення. - - -Додавання запису ----------------- - -Приклад презентера, що служить для додавання запису. Саму роботу з базою даних залишимо класу `Facade`, код якого не є суттєвим для прикладу. - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentRecordForm(): Form - { - $form = new Form; - - // ... додамо поля форми ... - - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // додавання запису до бази даних - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function renderAdd(): void - { - // ... - } -} -``` - - -Редагування запису ------------------- - -Тепер покажемо, як виглядав би презентер, що служить для редагування запису: - - -```php -use Nette\Application\UI\Form; - -class RecordPresenter extends Nette\Application\UI\Presenter -{ - private $record; - - public function __construct( - private Facade $facade, - ) { - } - - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - !$record // перевірка існування запису - || !$this->facade->isEditAllowed(/*...*/) // перевірка прав доступу - ) { - $this->error(); // помилка 404 - } - - $this->record = $record; - } - - protected function createComponentRecordForm(): Form - { - // перевіримо, що дія є 'edit' - if ($this->getAction() !== 'edit') { - $this->error(); - } - - $form = new Form; - - // ... додамо поля форми ... - - $form->setDefaults($this->record); // встановлення значень за замовчуванням - $form->onSuccess[] = [$this, 'recordFormSucceeded']; - return $form; - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $this->facade->update($this->record->id, $data); // оновлення запису - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -У методі *action*, який запускається на самому початку [життєвого циклу презентера |application:presenters#Життєвий цикл презентера], ми перевіряємо існування запису та права користувача на його редагування. - -Запис ми зберігаємо у властивості `$record`, щоб мати до нього доступ у методі `createComponentRecordForm()` для встановлення значень за замовчуванням, та в `recordFormSucceeded()` для отримання ID. Альтернативним рішенням було б встановити значення за замовчуванням безпосередньо в `actionEdit()` та отримати значення ID, яке є частиною URL, за допомогою `getParameter('id')`: - - -```php - public function actionEdit(int $id): void - { - $record = $this->facade->get($id); - if ( - // перевірка існування та прав доступу - ) { - $this->error(); - } - - // встановлення значень за замовчуванням форми - $this->getComponent('recordForm') - ->setDefaults($record); - } - - public function recordFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); - // ... - } -} -``` - -Однак, і це має бути **найважливішим висновком усього коду**, ми повинні при створенні форми переконатися, що дія дійсно є `edit`. Бо інакше перевірка в методі `actionEdit()` взагалі не відбудеться! - - -Однакова форма для додавання та редагування -------------------------------------------- - -А тепер об'єднаємо обидва презентери в один. Ми могли б у методі `createComponentRecordForm()` розрізняти, про яку дію йдеться, і відповідно конфігурувати форму, або ж можемо залишити це безпосередньо для action-методів і позбутися умови: - - -```php -class RecordPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private 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 // перевірка існування запису - || !$this->facade->isEditAllowed(/*...*/) // перевірка прав доступу - ) { - $this->error(); // помилка 404 - } - - $form = $this->getComponent('recordForm'); - $form->setDefaults($record); // встановлення значень за замовчуванням - $form->onSuccess[] = [$this, 'editingFormSucceeded']; - } - - protected function createComponentRecordForm(): Form - { - // перевіримо, що дія є 'add' або 'edit' - if (!in_array($this->getAction(), ['add', 'edit'])) { - $this->error(); - } - - $form = new Form; - - // ... додамо поля форми ... - - return $form; - } - - public function addingFormSucceeded(Form $form, array $data): void - { - $this->facade->add($data); // додавання запису до бази даних - $this->flashMessage('Successfully added'); - $this->redirect('...'); - } - - public function editingFormSucceeded(Form $form, array $data): void - { - $id = (int) $this->getParameter('id'); - $this->facade->update($id, $data); // оновлення запису - $this->flashMessage('Successfully updated'); - $this->redirect('...'); - } -} -``` - -{{priority: -1}} diff --git a/best-practices/uk/dynamic-snippets.texy b/best-practices/uk/dynamic-snippets.texy deleted file mode 100644 index 3a8f9f5f63..0000000000 --- a/best-practices/uk/dynamic-snippets.texy +++ /dev/null @@ -1,173 +0,0 @@ -Динамічні сніпети -***************** - -Досить часто під час розробки додатків виникає потреба виконувати AJAX-операції, наприклад, над окремими рядками таблиці або елементами списку. Для прикладу можемо взяти виведення статей, причому для кожної з них дозволимо зареєстрованому користувачеві вибрати оцінку "подобається/не подобається". Код презентера та відповідного шаблону без AJAX виглядатиме приблизно так (наводжу найважливіші фрагменти, код розраховує на існування сервісу для позначення оцінок та отримання колекції статей - конкретна реалізація не важлива для цілей цього посібника): - -```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'); -} -``` - -Шаблон: - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>{* це мені подобається *}</a> - {else} - <a n:href="unlike! $article->id" class=ajax>{* мені це вже не подобається *}</a> - {/if} -</article> -``` - - -Аяксифікація -============ - -Тепер давайте оснастимо цей простий додаток AJAX. Зміна оцінки статті не настільки важлива, щоб вимагати перенаправлення, тому ідеально було б, щоб вона відбувалася за допомогою AJAX у фоновому режимі. Ми використаємо [скрипт обробки з доповнень |application:ajax#Naja] зі звичайною конвенцією, що AJAX-посилання мають CSS-клас `ajax`. - -Однак, як це зробити конкретно? Nette пропонує 2 шляхи: шлях так званих динамічних сніпетів та шлях компонентів. Обидва мають свої переваги та недоліки, тому ми розглянемо їх по черзі. - - -Шлях динамічних сніпетів -======================== - -Динамічний сніпет в термінології Latte означає специфічний випадок використання тегу `{snippet}`, коли в назві сніпета використовується змінна. Такий сніпет не може знаходитися будь-де в шаблоні - він повинен бути обгорнутий статичним сніпетом, тобто звичайним, або всередині `{snippetArea}`. Наш шаблон можна було б змінити наступним чином. - - -```latte -{snippet articlesContainer} - <article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {snippet article-{$article->id}} - {if !$article->liked} - <a n:href="like! $article->id" class=ajax>{* це мені подобається *}</a> - {else} - <a n:href="unlike! $article->id" class=ajax>{* мені це вже не подобається *}</a> - {/if} - {/snippet} - </article> -{/snippet} -``` - -Кожна стаття тепер визначає один сніпет, який має в назві ID статті. Всі ці сніпети потім разом обгорнуті одним сніпетом з назвою `articlesContainer`. Якби ми пропустили цей обгортаючий сніпет, Latte повідомить нас про це винятком. - -Залишається додати до презентера перемальовування - достатньо перемалювати статичну обгортку. - -```php -public function handleLike(int $articleId): void -{ - $this->ratingService->saveLike($articleId, $this->user->id); - if ($this->isAjax()) { - $this->redrawControl('articlesContainer'); - // $this->redrawControl('article-' . $articleId); -- не потрібно - } else { - $this->redirect('this'); - } -} -``` - -Аналогічно змінимо і сестринський метод `handleUnlike()`, і AJAX запрацює! - -Однак рішення має один недолік. Якщо ми детальніше дослідимо, як відбувається AJAX-запит, то виявимо, що хоча зовні додаток виглядає економним (повертає лише один єдиний сніпет для даної статті), насправді на сервері він відрендерив усі сніпети. Потрібний сніпет він помістив у payload, а решту відкинув (отже, також абсолютно марно отримав їх із бази даних). - -Щоб оптимізувати цей процес, нам доведеться втрутитися там, де ми передаємо колекцію `$articles` до шаблону (скажімо, в методі `renderDefault()`). Ми скористаємося тим фактом, що обробка сигналів відбувається перед методами `render<Something>`: - -```php -public function handleLike(int $articleId): void -{ - // ... - if ($this->isAjax()) { - // ... - $this->template->articles = [ - $this->db->table('articles')->get($articleId), - ]; - } else { - // ... -} - -public function renderDefault(): void -{ - if (!isset($this->template->articles)) { - $this->template->articles = $this->db->table('articles'); - } -} -``` - -Тепер при обробці сигналу до шаблону передається замість колекції з усіма статтями лише масив з єдиною статтею - тією, яку ми хочемо відрендерити та надіслати в payload до браузера. `{foreach}` таким чином пройде лише один раз, і жодних зайвих сніпетів не відрендериться. - - -Шлях компонентів -================ - -Абсолютно інший спосіб вирішення уникає динамічних сніпетів. Трюк полягає в перенесенні всієї логіки в окремий компонент - відтепер про введення оцінки дбатиме не презентер, а спеціалізований `LikeControl`. Клас виглядатиме наступним чином (крім того, він міститиме також методи `render`, `handleUnlike` тощо): - -```php -class LikeControl extends Nette\Application\UI\Control -{ - public function __construct( - private 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'); - } - } -} -``` - -Шаблон компонента: - -```latte -{snippet} - {if !$article->liked} - <a n:href="like!" class=ajax>{* це мені подобається *}</a> - {else} - <a n:href="unlike!" class=ajax>{* мені це вже не подобається *}</a> - {/if} -{/snippet} -``` - -Звичайно, шаблон view зміниться, і нам доведеться додати до презентера фабрику. Оскільки ми створимо компонент стільки разів, скільки статей отримаємо з бази даних, ми використаємо для його "розмноження" клас [application:Multiplier]. - -```php -protected function createComponentLikeControl() -{ - $articles = $this->db->table('articles'); - return new Nette\Application\UI\Multiplier(function (int $articleId) use ($articles) { - return new LikeControl($articles[$articleId]); - }); -} -``` - -Шаблон view зменшиться до необхідного мінімуму (і повністю позбавиться сніпетів!): - -```latte -<article n:foreach="$articles as $article"> - <h2>{$article->title}</h2> - <div class="content">{$article->content}</div> - {control "likeControl-$article->id"} -</article> -``` - -Майже готово: додаток тепер працюватиме за допомогою AJAX. Тут також нас чекає оптимізація додатку, оскільки через використання Nette Database при обробці сигналу марно завантажуються всі статті з бази даних замість однієї. Перевагою, однак, є те, що їх рендеринг не відбудеться, оскільки відрендериться дійсно лише наш компонент. - -{{priority: -1}} diff --git a/best-practices/uk/editors-and-tools.texy b/best-practices/uk/editors-and-tools.texy deleted file mode 100644 index 86380ff279..0000000000 --- a/best-practices/uk/editors-and-tools.texy +++ /dev/null @@ -1,84 +0,0 @@ -Редактори та інструменти -************************ - -.[perex] -Ви можете бути вправним програмістом, але лише з хорошими інструментами ви станете майстром. У цьому розділі ви знайдете поради щодо важливих інструментів, редакторів та плагінів. - - -IDE редактор -============ - -Ми наполегливо рекомендуємо використовувати для розробки повноцінне IDE, таке як PhpStorm, NetBeans, VS Code, а не просто текстовий редактор з підтримкою PHP. Різниця справді суттєва. Немає причин задовольнятися простим редактором, який хоч і вміє підсвічувати синтаксис, але не досягає можливостей топового IDE, яке точно підказує, відстежує помилки, вміє рефакторити код та багато іншого. Деякі IDE платні, інші навіть безкоштовні. - -**NetBeans IDE** має вбудовану підтримку Nette, Latte та NEON. - -**PhpStorm**: встановіть ці плагіни в `Settings > Plugins > Marketplace` -- Nette framework helpers -- Latte -- NEON support -- Nette Tester - -**VS Code**: знайдіть у marketplace плагін "Nette Latte + Neon". - -Також зв'яжіть Tracy з редактором. При відображенні сторінки помилки можна буде клікнути на імена файлів, і вони відкриються в редакторі з курсором на відповідному рядку. Прочитайте, [як налаштувати систему|tracy:open-files-in-ide]. - - -PHPStan -======= - -PHPStan — це інструмент, який виявляє логічні помилки в коді ще до його запуску. - -Встановимо його за допомогою Composer: - -```shell -composer require --dev phpstan/phpstan-nette -``` - -Створимо в проекті конфігураційний файл `phpstan.neon`: - -```neon -includes: - - vendor/phpstan/phpstan-nette/extension.neon - -parameters: - scanDirectories: - - app - - level: 5 -``` - -А потім запустимо аналіз класів у папці `app/`: - -```shell -vendor/bin/phpstan analyse app -``` - -Вичерпну документацію ви знайдете безпосередньо на [сайті PHPStan |https://phpstan.org]. - - -Code Checker -============ - -[Code Checker|code-checker:] перевіряє та, за потреби, виправляє деякі формальні помилки у ваших вихідних кодах: - -- видаляє [BOM |nette:glossary#BOM] -- перевіряє валідність шаблонів [Latte |latte:] -- перевіряє валідність файлів `.neon`, `.php` та `.json` -- перевіряє наявність [контрольних символів |nette:glossary#Керуючі символи] -- перевіряє, чи файл закодований у UTF-8 -- перевіряє помилково записані `/* @anotace */` (відсутня зірочка) -- видаляє завершальний `?>` у PHP файлах -- видаляє пробіли в кінці рядків та зайві рядки в кінці файлу -- нормалізує роздільники рядків до системних (якщо вказати опцію `-l`) - - -Composer -======== - -[Composer | Composer] — це інструмент для керування залежностями в PHP. Він дозволяє нам декларувати довільно складні залежності окремих бібліотек, а потім встановлює їх для нас у наш проект. - - -Requirements Checker -==================== - -Це був інструмент, який тестував середовище виконання сервера та інформував, чи (і якою мірою) можна використовувати фреймворк. На даний момент Nette можна використовувати на будь-якому сервері, який має мінімально необхідну версію PHP. diff --git a/best-practices/uk/form-reuse.texy b/best-practices/uk/form-reuse.texy deleted file mode 100644 index d75d20642b..0000000000 --- a/best-practices/uk/form-reuse.texy +++ /dev/null @@ -1,348 +0,0 @@ -Повторне використання форм у кількох місцях -******************************************* - -.[perex] -У Nette у вас є кілька варіантів використання однієї й тієї ж форми в кількох місцях без дублювання коду. У цій статті ми розглянемо різні рішення, включно з тими, яких слід уникати. - - -Фабрика форм -============ - -Одним з основних підходів до використання одного й того ж компонента в кількох місцях є створення методу або класу, який генерує цей компонент, і подальше викликання цього методу в різних місцях програми. Такий метод або клас називається *фабрикою*. Будь ласка, не плутайте з патерном проектування *factory method*, який описує специфічний спосіб використання фабрик і не пов'язаний з цією темою. - -Як приклад, створимо фабрику, яка буде збирати форму редагування: - -```php -use Nette\Application\UI\Form; - -class FormFactory -{ - public function createEditForm(): Form - { - $form = new Form; - $form->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - return $form; - } -} -``` - -Тепер ви можете використовувати цю фабрику в різних місцях вашої програми, наприклад, у презентерах або компонентах. Це робиться шляхом [запрошення її як залежності|dependency-injection:passing-dependencies]. Спочатку запишемо клас у конфігураційний файл: - -```neon -services: - - FormFactory -``` - -А потім використаємо її в презентері: - - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->createEditForm(); - $form->onSuccess[] = function () { - // обробка надісланих даних - }; - return $form; - } -} -``` - -Фабрику форм можна розширити додатковими методами для створення інших типів форм відповідно до потреб вашої програми. І, звичайно, ми можемо додати метод, який створить базову форму без елементів, і цей метод будуть використовувати інші методи: - -```php -class FormFactory -{ - public function createForm(): Form - { - $form = new Form; - return $form; - } - - public function createEditForm(): Form - { - $form = $this->createForm(); - $form->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - return $form; - } -} -``` - -Метод `createForm()` поки що не робить нічого корисного, але це швидко зміниться. - - -Залежності фабрики -================== - -З часом виявиться, що нам потрібно, щоб форми були багатомовними. Це означає, що всім формам потрібно встановити так званий [translator |forms:rendering#Переклад]. Для цього ми змінимо клас `FormFactory` так, щоб він приймав об'єкт `Translator` як залежність у конструкторі, і передамо його формі: - -```php -use Nette\Localization\Translator; - -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function createForm(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } - - // ... -} -``` - -Оскільки метод `createForm()` викликають і інші методи, що створюють специфічні форми, достатньо встановити translator лише в ньому. І все готово. Не потрібно змінювати код жодного презентера чи компонента, що чудово. - - -Кілька фабричних класів -======================= - -Альтернативно, ви можете створити кілька класів для кожної форми, яку хочете використовувати у вашій програмі. Цей підхід може підвищити читабельність коду та полегшити керування формами. Оригінальну `FormFactory` залишимо створювати лише чисту форму з базовою конфігурацією (наприклад, з підтримкою перекладів), а для форми редагування створимо нову фабрику `EditFormFactory`. - -```php -class FormFactory -{ - public function __construct( - private Translator $translator, - ) { - } - - public function create(): Form - { - $form = new Form; - $form->setTranslator($this->translator); - return $form; - } -} - - -// ✅ використання композиції -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - return $form; - } -} -``` - -Дуже важливо, щоб зв'язок між класами `FormFactory` та `EditFormFactory` був реалізований [композицією |nette:introduction-to-object-oriented-programming#Композиція], а не [об'єктною спадковістю |nette:introduction-to-object-oriented-programming#Успадкування]: - -```php -// ⛔ ТАК НЕ РОБИТИ! ТУТ СПАДКУВАННЯ НЕ ДО РЕЧІ -class EditFormFactory extends FormFactory -{ - public function create(): Form - { - $form = parent::create(); - $form->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - return $form; - } -} -``` - -Використання спадковості в цьому випадку було б абсолютно контрпродуктивним. Ви б дуже швидко зіткнулися з проблемами. Наприклад, коли б ви захотіли додати параметри до методу `create()`; PHP повідомив би про помилку, що його сигнатура відрізняється від батьківської. Або при передачі залежності до класу `EditFormFactory` через конструктор. Виникла б ситуація, яку ми називаємо [constructor hell |dependency-injection:passing-dependencies#Пекло конструкторів]. - -Загалом, краще надавати перевагу [композиції перед спадковістю |dependency-injection:faq#Чому композиції надається перевага перед успадкуванням]. - - -Обробка форми -============= - -Обробник форми, який викликається після успішного надсилання, також може бути частиною фабричного класу. Він працюватиме так, що передасть надіслані дані моделі для обробки. Можливі помилки [передасть назад |forms:validation#Помилки під час обробки] до форми. Модель у наступному прикладі представляє клас `Facade`: - -```php -class EditFormFactory -{ - public function __construct( - private FormFactory $formFactory, - private Facade $facade, - ) { - } - - public function create(): Form - { - $form = $this->formFactory->create(); - $form->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - $form->onSuccess[] = [$this, 'processForm']; - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // обробка надісланих даних - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - } - } -} -``` - -Однак саме перенаправлення ми залишимо на презентері. Він додасть до події `onSuccess` ще один обробник, який виконає перенаправлення. Завдяки цьому форму можна буде використовувати в різних презентерах і в кожному перенаправляти в інше місце. - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditFormFactory $formFactory, - ) { - } - - protected function createComponentEditForm(): Form - { - $form = $this->formFactory->create(); - $form->onSuccess[] = function () { - $this->flashMessage('Запис було збережено'); - $this->redirect('Homepage:'); - }; - return $form; - } -} -``` - -Це рішення використовує властивість форм, що коли над формою або її елементом викликається `addError()`, наступний обробник `onSuccess` вже не викликається. - - -Спадкування від класу Form -========================== - -Скомпонована форма не повинна бути нащадком форми. Іншими словами, не використовуйте це рішення: - -```php -// ⛔ ТАК НЕ РОБИТИ! ТУТ СПАДКУВАННЯ НЕ ДО РЕЧІ -class EditForm extends Form -{ - public function __construct(Translator $translator) - { - parent::__construct(); - $this->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $this->addSubmit('send', 'Надіслати'); - $this->setTranslator($translator); - } -} -``` - -Замість того, щоб збирати форму в конструкторі, використовуйте фабрику. - -Потрібно усвідомити, що клас `Form` є насамперед інструментом для побудови форми, тобто *form builder*. А зібрану форму можна розглядати як її продукт. Однак продукт не є специфічним випадком білдера, між ними немає зв'язку *is a*, що лежить в основі спадковості. - - -Компонент з формою -================== - -Абсолютно інший підхід представляє створення [компонента|application:components], частиною якого є форма. Це дає нові можливості, наприклад, рендерити форму специфічним чином, оскільки частиною компонента є і шаблон. Або можна використовувати сигнали для AJAX-комунікації та дозавантаження інформації у форму, наприклад, для підказок тощо. - - -```php -use Nette\Application\UI\Form; - -class EditControl extends Nette\Application\UI\Control -{ - public array $onSave = []; - - public function __construct( - private Facade $facade, - ) { - } - - protected function createComponentForm(): Form - { - $form = new Form; - $form->addText('title', 'Заголовок:'); - // тут додаються інші поля форми - $form->addSubmit('send', 'Надіслати'); - $form->onSuccess[] = [$this, 'processForm']; - - return $form; - } - - public function processForm(Form $form, array $data): void - { - try { - // обробка надісланих даних - $this->facade->process($data); - - } catch (AnyModelException $e) { - $form->addError('...'); - return; - } - - // виклик події - $this->onSave($this, $data); - } -} -``` - -Ще створимо фабрику, яка буде виробляти цей компонент. Достатньо [записати її інтерфейс |application:components#Компоненти із залежностями]: - -```php -interface EditControlFactory -{ - function create(): EditControl; -} -``` - -І додати до конфігураційного файлу: - -```neon -services: - - EditControlFactory -``` - -А тепер вже можемо запросити фабрику та використати її в презентері: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private EditControlFactory $controlFactory, - ) { - } - - protected function createComponentEditForm(): EditControl - { - $control = $this->controlFactory->create(); - - $control->onSave[] = function (EditControl $control, $data) { - $this->redirect('this'); - // або перенаправляємо на результат редагування, напр.: - // $this->redirect('detail', ['id' => $data->id]); - }; - - return $control; - } -} -``` diff --git a/best-practices/uk/inject-method-attribute.texy b/best-practices/uk/inject-method-attribute.texy deleted file mode 100644 index 8e64ef27ab..0000000000 --- a/best-practices/uk/inject-method-attribute.texy +++ /dev/null @@ -1,61 +0,0 @@ -Методи та атрибути inject -************************* - -.[perex] -У цій статті ми розглянемо різні способи передачі залежностей у презентери у фреймворку Nette. Ми порівняємо бажаний спосіб, яким є конструктор, з іншими варіантами, такими як методи та атрибути `inject`. - -Навіть для презентерів передача залежностей за допомогою [конструктора |dependency-injection:passing-dependencies#Передача конструктором] є бажаним шляхом. Однак, якщо ви створюєте спільного предка, від якого успадковуються інші презентери (наприклад, `BasePresenter`), і цей предок також має залежності, виникає проблема, яку ми називаємо [constructor hell |dependency-injection:passing-dependencies#Пекло конструкторів]. Її можна обійти за допомогою альтернативних шляхів, якими є методи та атрибути (анотації) `inject`. - - -Методи `inject*()` -================== - -Це форма передачі залежності [сеттером |dependency-injection:passing-dependencies#Передача сеттером]. Назва цих сеттерів починається з префікса `inject`. Nette DI автоматично викликає методи з такою назвою одразу після створення екземпляра презентера та передає їм усі необхідні залежності. Тому вони повинні бути оголошені як public. - -Методи `inject*()` можна вважати своєрідним розширенням конструктора на кілька методів. Завдяки цьому `BasePresenter` може приймати залежності через інший метод і залишати конструктор вільним для своїх нащадків: - -```php -abstract class BasePresenter extends Nette\Application\UI\Presenter -{ - private Foo $foo; - - public function injectBase(Foo $foo): void - { - $this->foo = $foo; - } -} - -class MyPresenter extends BasePresenter -{ - private Bar $bar; - - public function __construct(Bar $bar) - { - $this->bar = $bar; - } -} -``` - -Презентер може містити будь-яку кількість методів `inject*()`, і кожен може мати будь-яку кількість параметрів. Це також чудово підходить у випадках, коли презентер [складається з трейтів |presenter-traits], і кожен з них вимагає власної залежності. - - -Атрибути `Inject` -================= - -Це форма [ін'єкції у властивість |dependency-injection:passing-dependencies#Встановленням змінної]. Достатньо позначити, в які змінні слід ін'єктувати, і Nette DI автоматично передасть залежності одразу після створення екземпляра презентера. Щоб їх можна було вставити, необхідно оголосити їх як public. - -Властивості позначимо атрибутом: (раніше використовувалася анотація `/** @inject */`) - -```php -use Nette\DI\Attributes\Inject; // цей рядок важливий - -class MyPresenter extends Nette\Application\UI\Presenter -{ - #[Inject] - public Cache $cache; -} -``` - -Перевагою цього способу передачі залежностей була дуже лаконічна форма запису. Однак з появою [constructor property promotion |https://blog.nette.org/uk/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] простіше використовувати конструктор. - -Навпаки, цей спосіб страждає тими ж недоліками, що й передача залежності у властивості загалом: ми не маємо контролю над змінами в змінній, і водночас змінна стає частиною публічного інтерфейсу класу, що є небажаним. diff --git a/best-practices/uk/lets-create-contact-form.texy b/best-practices/uk/lets-create-contact-form.texy deleted file mode 100644 index dda0437ab6..0000000000 --- a/best-practices/uk/lets-create-contact-form.texy +++ /dev/null @@ -1,221 +0,0 @@ -Створюємо контактну форму -************************* - -.[perex] -Розглянемо, як у Nette створити контактну форму, включно з надсиланням на електронну пошту. Отже, до справи! - -Спочатку потрібно створити новий проект. Як це зробити, пояснюється на сторінці [Починаємо |nette:installation]. А потім вже можемо почати створювати форму. - -Найпростіше створити [форму безпосередньо в презентері |forms:in-presenter]. Можемо використати заготовлений `HomePresenter`. До нього додамо компонент `contactForm`, що представляє форму. Зробимо це так: запишемо в код фабричний метод `createComponentContactForm()`, який створить компонент: - -```php -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - protected function createComponentContactForm(): Form - { - $form = new Form; - $form->addText('name', "Ім'я:") - ->setRequired("Введіть ім'я"); - $form->addEmail('email', 'E-mail:') - ->setRequired('Введіть e-mail'); - $form->addTextarea('message', 'Повідомлення:') - ->setRequired('Введіть повідомлення'); - $form->addSubmit('send', 'Надіслати'); - $form->onSuccess[] = [$this, 'contactFormSucceeded']; - return $form; - } - - public function contactFormSucceeded(Form $form, $data): void - { - // надсилання email - } -} -``` - -Як бачите, ми створили два методи. Перший метод `createComponentContactForm()` створює нову форму. Вона має поля для імені, email та повідомлення, які ми додаємо методами `addText()`, `addEmail()` та `addTextArea()`. Також ми додали кнопку для надсилання форми. Але що, якщо користувач не заповнить якесь поле? У такому випадку ми повинні повідомити йому, що це обов'язкове поле. Цього ми досягли за допомогою методу `setRequired()`. Нарешті, ми також додали [подію |nette:glossary#Події události] `onSuccess`, яка спрацює, якщо форма успішно надіслана. У нашому випадку вона викличе метод `contactFormSucceeded`, який подбає про обробку надісланої форми. Це ми доповнимо в код за мить. - -Компонент `contactForm` виведемо в шаблоні `Home/default.latte`: - -```latte -{block content} -<h1>Контактна форма</h1> -{control contactForm} -``` - -Для самого надсилання email створимо новий клас, який назвемо `ContactFacade` і розмістимо його у файлі `app/Model/ContactFacade.php`: - -```php -<?php -declare(strict_types=1); - -namespace App\Model; - -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $mail = new Message; - $mail->addTo('admin@example.com') // ваш email - ->setFrom($email, $name) - ->setSubject('Повідомлення з контактної форми') - ->setBody($message); - - $this->mailer->send($mail); - } -} -``` - -Метод `sendMessage()` створює та надсилає email. Для цього він використовує так званий mailer, який отримує як залежність через конструктор. Дізнайтеся більше про [надсилання електронних листів |mail:]. - -Тепер повернемося до презентера і завершимо метод `contactFormSucceeded()`. Він викличе метод `sendMessage()` класу `ContactFacade` і передасть йому дані з форми. А як отримати об'єкт `ContactFacade`? Отримаємо його через конструктор: - -```php -use App\Model\ContactFacade; -use Nette\Application\UI\Form; -use Nette\Application\UI\Presenter; - -class HomePresenter extends Presenter -{ - public function __construct( - private ContactFacade $facade, - ) { - } - - protected function createComponentContactForm(): Form - { - // ... - } - - public function contactFormSucceeded(stdClass $data): void - { - $this->facade->sendMessage($data->email, $data->name, $data->message); - $this->flashMessage('Повідомлення було надіслано'); - $this->redirect('this'); - } -} -``` - -Після надсилання email ми ще покажемо користувачеві так зване [flash-повідомлення |application:components#Flash-повідомлення], що підтверджує надсилання повідомлення, а потім перенаправимо на наступну сторінку, щоб не можна було повторно надіслати форму за допомогою *refresh* у браузері. - - -Отже, якщо все працює, ви повинні мати можливість надіслати email з вашої контактної форми. Вітаю! - - -HTML-шаблон електронного листа ------------------------------- - -Поки що надсилається простий текстовий email, що містить лише повідомлення, надіслане формою. Але в email ми можемо використовувати HTML і зробити його вигляд привабливішим. Створимо для нього шаблон у Latte, який запишемо до `app/Model/contactEmail.latte`: - -```latte -<html> - <title>Повідомлення з контактної форми - - -

    Ім'я: {$name}

    -

    E-mail: {$email}

    -

    Повідомлення: {$message}

    - - -``` - -Залишилося змінити `ContactFacade`, щоб він використовував цей шаблон. У конструкторі ми запросимо клас `LatteFactory`, який вміє створювати об'єкт `Latte\Engine`, тобто [рендер шаблонів Latte |latte:develop#Як відобразити шаблон]. За допомогою методу `renderToString()` ми відрендеримо шаблон у файл, першим параметром є шлях до шаблону, а другим – змінні. - -```php -namespace App\Model; - -use Nette\Bridges\ApplicationLatte\LatteFactory; -use Nette\Mail\Mailer; -use Nette\Mail\Message; - -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - $latte = $this->latteFactory->create(); - $body = $latte->renderToString(__DIR__ . '/contactEmail.latte', [ - 'email' => $email, - 'name' => $name, - 'message' => $message, - ]); - - $mail = new Message; - $mail->addTo('admin@example.com') // ваш email - ->setFrom($email, $name) - ->setHtmlBody($body); - - $this->mailer->send($mail); - } -} -``` - -Згенерований HTML email потім передамо методу `setHtmlBody()` замість початкового `setBody()`. Також нам не потрібно вказувати тему email у `setSubject()`, оскільки бібліотека візьме її з елемента `` шаблону. - - -Конфігурація ------------- - -У коді класу `ContactFacade` все ще жорстко прописаний наш адміністраторський email `admin@example.com`. Було б краще перенести його до конфігураційного файлу. Як це зробити? - -Спочатку змінимо клас `ContactFacade` і рядок з email замінимо змінною, переданою конструктором: - -```php -class ContactFacade -{ - public function __construct( - private Mailer $mailer, - private LatteFactory $latteFactory, - private string $adminEmail, - ) { - } - - public function sendMessage(string $email, string $name, string $message): void - { - // ... - $mail = new Message; - $mail->addTo($this->adminEmail) - ->setFrom($email, $name) - ->setHtmlBody($body); - // ... - } -} -``` - -А другим кроком є вказівка значення цієї змінної в конфігурації. До файлу `app/config/services.neon` запишемо: - -```neon -services: - - App\Model\ContactFacade(adminEmail: admin@example.com) -``` - -І все. Якщо елементів у секції `services` буде багато і ви відчуватимете, що email серед них губиться, ми можемо зробити з нього змінну. Змінимо запис на: - -```neon -services: - - App\Model\ContactFacade(adminEmail: %adminEmail%) -``` - -А у файлі `app/config/common.neon` визначимо цю змінну: - -```neon -parameters: - adminEmail: admin@example.com -``` - -І готово! diff --git a/best-practices/uk/microsites.texy b/best-practices/uk/microsites.texy deleted file mode 100644 index 19744c4b86..0000000000 --- a/best-practices/uk/microsites.texy +++ /dev/null @@ -1,63 +0,0 @@ -Як створювати мікросайти -************************ - -Уявіть, що вам потрібно швидко створити невеликий веб-сайт для майбутньої події вашої компанії. Це має бути просто, швидко і без зайвих ускладнень. Можливо, ви думаєте, що для такого маленького проекту вам не потрібен потужний фреймворк. Але що, якщо використання фреймворку Nette може суттєво спростити та прискорити цей процес? - -Адже навіть при створенні простих веб-сайтів ви не хочете відмовлятися від зручності. Ви не хочете вигадувати те, що вже було одного разу вирішено. Будьте спокійно лінивими і дозвольте себе побалувати. Nette Framework можна чудово використовувати і як мікрофреймворк. - -Як може виглядати такий мікросайт? Наприклад, так, що весь код сайту ми розмістимо в єдиному файлі `index.php` у публічній папці: - -```php -<?php - -require __DIR__ . '/../vendor/autoload.php'; - -$configurator = new Nette\Bootstrap\Configurator; -$configurator->enableTracy(__DIR__ . '/../log'); -$configurator->setTempDirectory(__DIR__ . '/../temp'); - -// створи DI-контейнер на основі конфігурації в config.neon -$configurator->addConfig(__DIR__ . '/../app/config.neon'); -$container = $configurator->createContainer(); - -// налаштуємо маршрутизацію -$router = new Nette\Application\Routers\RouteList; -$container->addService('router', $router); - -// маршрут для URL https://example.com/ -$router->addRoute('', function ($presenter, Nette\Http\Request $httpRequest) { - // визначаємо мову браузера та перенаправляємо на URL /en або /de тощо. - $supportedLangs = ['en', 'de', 'cs']; - $lang = $httpRequest->detectLanguage($supportedLangs) ?: reset($supportedLangs); - $presenter->redirectUrl("/$lang"); -}); - -// маршрут для URL https://example.com/cs або https://example.com/en -$router->addRoute('<lang cs|en>', function ($presenter, string $lang) { - // відобразимо відповідний шаблон, наприклад ../templates/en.latte - $template = $presenter->createTemplate() - ->setFile(__DIR__ . '/../templates/' . $lang . '.latte'); - return $template; -}); - -// запустіть додаток! -$container->getByType(Nette\Application\Application::class)->run(); -``` - -Все інше будуть шаблони, збережені в батьківській папці `/templates`. - -PHP-код в `index.php` спочатку [підготує середовище |bootstrap:], потім визначає [маршрути |application:routing#Динамічна маршрутизація з callback-функціями] і нарешті запускає додаток. Перевагою є те, що другий параметр функції `addRoute()` може бути callable, який виконається після відкриття відповідної сторінки. - - -Чому варто використовувати Nette для мікросайтів? -------------------------------------------------- - -- Програмісти, які колись спробували [Tracy|tracy:], сьогодні не уявляють, як програмувати без неї. -- Перш за все, ви скористаєтеся системою шаблонів [Latte|latte:], оскільки вже з 2 сторінок вам захочеться мати розділений [макет та вміст|latte:template-inheritance]. -- І ви точно хочете покладатися на [автоматичне екранування |latte:safety-first], щоб не виникла вразливість XSS. -- Nette також гарантує, що при помилці ніколи не відобразяться повідомлення про помилки PHP для програмістів, а зрозуміла для користувача сторінка. -- Якщо ви хочете отримувати зворотній зв'язок від користувачів, наприклад, у вигляді контактної форми, то ще додасте [форми|forms:] та [базу даних|database:]. -- Заповнені форми ви також можете легко [надсилати електронною поштою|mail:]. -- Іноді вам може знадобитися [кешування|caching:], наприклад, якщо ви завантажуєте та відображаєте стрічки новин. - -У наш час, коли швидкість та ефективність є ключовими, важливо мати інструменти, які дозволять вам досягти результатів без зайвих затримок. Фреймворк Nette пропонує саме це - швидку розробку, безпеку та широкий спектр інструментів, таких як Tracy та Latte, які спрощують процес. Достатньо встановити кілька пакетів Nette, і створення такого мікросайту раптом стає зовсім простою справою. І ви знаєте, що ніде не ховається жодна дірка в безпеці. diff --git a/best-practices/uk/pagination.texy b/best-practices/uk/pagination.texy deleted file mode 100644 index e16f7e4ad4..0000000000 --- a/best-practices/uk/pagination.texy +++ /dev/null @@ -1,273 +0,0 @@ -Пагінація результатів бази даних -******************************** - -.[perex] -При створенні веб-додатків дуже часто виникає вимога обмежити кількість виведених елементів на сторінці. - -Почнемо зі стану, коли ми виводимо всі дані без пагінації. Для вибору даних з бази даних у нас є клас ArticleRepository, який, крім конструктора, містить метод `findPublishedArticles`, що повертає всі опубліковані статті, відсортовані за спаданням дати публікації. - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\ResultSet - { - return $this->database->query(' - SELECT * FROM articles - WHERE created_at < ? - ORDER BY created_at DESC', - new \DateTime, - ); - } -} -``` - -У презентері ми потім ін'єктуємо клас моделі, а в методі render запитуємо опубліковані статті, які передаємо до шаблону: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(): void - { - $this->template->articles = $this->articleRepository->findPublishedArticles(); - } -} -``` - -У шаблоні `default.latte` ми потім подбаємо про виведення статей: - -```latte -{block content} -<h1>Статті</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> -``` - - -Таким чином ми можемо вивести всі статті, що, однак, почне створювати проблеми, коли кількість статей зросте. У цей момент стане в нагоді реалізація механізму пагінації. - -Він забезпечить, що всі статті будуть розділені на кілька сторінок, і ми відобразимо лише статті однієї поточної сторінки. Загальну кількість сторінок та розподіл статей обчислить [utils:Paginator] сам, залежно від того, скільки статей у нас загалом і скільки статей на сторінку ми хочемо відобразити. - -На першому кроці ми змінимо метод для отримання статей у класі репозиторію так, щоб він міг повертати лише статті для однієї сторінки. Також додамо метод для визначення загальної кількості статей у базі даних, який нам знадобиться для налаштування Paginator: - -```php -namespace App\Model; - -use Nette; - - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Connection $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, - ); - } - - /** - * Повертає загальну кількість опублікованих статей - */ - public function getPublishedArticlesCount(): int - { - return $this->database->fetchField('SELECT COUNT(*) FROM articles WHERE created_at < ?', new \DateTime); - } -} -``` - -Потім перейдемо до змін у презентері. У метод render ми будемо передавати номер поточної відображуваної сторінки. У випадку, якщо цей номер не буде частиною URL, встановимо значення за замовчуванням першої сторінки. - -Далі також розширимо метод render отриманням екземпляра Paginator, його налаштуванням та вибором правильних статей для відображення в шаблоні. HomePresenter після змін виглядатиме так: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // З'ясуємо загальну кількість опублікованих статей - $articlesCount = $this->articleRepository->getPublishedArticlesCount(); - - // Створимо екземпляр Paginator і налаштуємо його - $paginator = new Nette\Utils\Paginator; - $paginator->setItemCount($articlesCount); // загальна кількість статей - $paginator->setItemsPerPage(10); // кількість елементів на сторінці - $paginator->setPage($page); // номер поточної сторінки - - // З бази даних витягнемо обмежену множину статей згідно з розрахунком Paginator - $articles = $this->articleRepository->findPublishedArticles($paginator->getLength(), $paginator->getOffset()); - - // яку передамо до шаблону - $this->template->articles = $articles; - // а також сам Paginator для відображення опцій пагінації - $this->template->paginator = $paginator; - } -} -``` - -Шаблон тепер уже ітерує лише над статтями однієї сторінки, нам залишається додати посилання для пагінації: - -```latte -{block content} -<h1>Статті</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if !$paginator->isFirst()} - <a n:href="default, 1">Перша</a> -  |  - <a n:href="default, $paginator->page-1">Попередня</a> -  |  - {/if} - - Сторінка {$paginator->getPage()} з {$paginator->getPageCount()} - - {if !$paginator->isLast()} -  |  - <a n:href="default, $paginator->getPage() + 1">Наступна</a> -  |  - <a n:href="default, $paginator->getPageCount()">Остання</a> - {/if} -</div> -``` - - -Таким чином ми доповнили сторінку можливістю пагінації за допомогою Paginator. У випадку, коли замість [Nette Database Core |database:sql-way] як шар бази даних використовується [Nette Database Explorer |database:explorer], ми можемо реалізувати пагінацію і без використання Paginator. Клас `Nette\Database\Table\Selection` містить метод [page |api:Nette\Database\Table\Selection::_page] з логікою пагінації, взятою з Paginator. - -Репозиторій при такому способі реалізації виглядатиме так: - -```php -namespace App\Model; - -use Nette; - -class ArticleRepository -{ - public function __construct( - private Nette\Database\Explorer $database, - ) { - } - - public function findPublishedArticles(): Nette\Database\Table\Selection - { - return $this->database->table('articles') - ->where('created_at < ', new \DateTime) - ->order('created_at DESC'); - } -} -``` - -У презентері нам не потрібно створювати Paginator, замість нього ми використаємо метод класу `Selection`, який повертає репозиторій: - -```php -namespace App\Presentation\Home; - -use Nette; -use App\Model\ArticleRepository; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private ArticleRepository $articleRepository, - ) { - } - - public function renderDefault(int $page = 1): void - { - // Витягнемо опубліковані статті - $articles = $this->articleRepository->findPublishedArticles(); - - // а до шаблону надішлемо лише їх частину, обмежену згідно з розрахунком методу page - $lastPage = 0; - $this->template->articles = $articles->page($page, 10, $lastPage); - - // а також необхідні дані для відображення опцій пагінації - $this->template->page = $page; - $this->template->lastPage = $lastPage; - } -} -``` - -Оскільки до шаблону ми тепер не надсилаємо Paginator, змінимо частину, що відображає посилання пагінації: - -```latte -{block content} -<h1>Статті</h1> - -<div class="articles"> - {foreach $articles as $article} - <h2>{$article->title}</h2> - <p>{$article->content}</p> - {/foreach} -</div> - -<div class="pagination"> - {if $page > 1} - <a n:href="default, 1">Перша</a> -  |  - <a n:href="default, $page - 1">Попередня</a> -  |  - {/if} - - Сторінка {$page} з {$lastPage} - - {if $page < $lastPage} -  |  - <a n:href="default, $page + 1">Наступна</a> -  |  - <a n:href="default, $lastPage">Остання</a> - {/if} -</div> -``` - -Таким чином ми реалізували механізм пагінації без використання Paginator. - -{{priority: -1}} diff --git a/best-practices/uk/passing-settings-to-presenters.texy b/best-practices/uk/passing-settings-to-presenters.texy deleted file mode 100644 index eab473dbda..0000000000 --- a/best-practices/uk/passing-settings-to-presenters.texy +++ /dev/null @@ -1,49 +0,0 @@ -Передача налаштувань у презентери -********************************* - -.[perex] -Вам потрібно передавати в презентери аргументи, які не є об'єктами (наприклад, інформацію про те, чи працює додаток у режимі налагодження, шляхи до каталогів тощо), і тому їх не можна передати автоматично за допомогою autowiring? Рішенням є інкапсуляція їх в об'єкт `Settings`. - -Сервіс `Settings` представляє дуже простий, але корисний спосіб надання інформації про запущений додаток презентерам. Його конкретна форма залежить виключно від ваших конкретних потреб. Приклад: - -```php -namespace App; - -class Settings -{ - public function __construct( - // від PHP 8.1 можна вказати readonly - public bool $debugMode, - public string $appDir, - // і так далі - ) {} -} -``` - -Приклад реєстрації в конфігурації: - -```neon -services: - - App\Settings( - %debugMode%, - %appDir%, - ) -``` - -Коли презентеру знадобиться інформація, що надається цим сервісом, він просто запросить її в конструкторі: - -```php -class MyPresenter extends Nette\Application\UI\Presenter -{ - public function __construct( - private App\Settings $settings, - ) {} - - public function renderDefault() - { - if ($this->settings->debugMode) { - // ... - } - } -} -``` diff --git a/best-practices/uk/post-links.texy b/best-practices/uk/post-links.texy deleted file mode 100644 index d0c67a0506..0000000000 --- a/best-practices/uk/post-links.texy +++ /dev/null @@ -1,56 +0,0 @@ -Як правильно використовувати POST-посилання -******************************************* - -.[perex] -У веб-додатках, особливо в адміністративних інтерфейсах, основним правилом має бути те, що дії, які змінюють стан сервера, не повинні виконуватися за допомогою HTTP-методу GET. Як випливає з назви методу, GET повинен використовуватися лише для отримання даних, а не для їх зміни. Для дій, таких як видалення записів, краще використовувати метод POST. Хоча ідеальним був би метод DELETE, але його не можна викликати без JavaScript, тому історично використовується POST. - -Як це зробити на практиці? Використовуйте цей простий трюк. На початку шаблону створіть допоміжну форму з ідентифікатором `postForm`, яку потім використовуйте для кнопок видалення: - -```latte .{file:@layout.latte} -<form method="post" id="postForm"></form> -``` - -Завдяки цій формі ви можете замість класичного посилання `<a>` використовувати кнопку `<button>`, яку можна візуально стилізувати так, щоб вона виглядала як звичайне посилання. Наприклад, CSS-фреймворк Bootstrap пропонує класи `btn btn-link`, за допомогою яких ви досягнете того, що кнопка не буде візуально відрізнятися від інших посилань. За допомогою атрибута `form="postForm"` ми пов'яжемо її з підготовленою формою: - -```latte .{file:admin.latte} -<table> - <tr n:foreach="$posts as $post"> - <td>{$post->title}</td> - <td> - <button class="btn btn-link" form="postForm" formaction="{link delete $post->id}">видалити</button> - <!-- замість <a n:href="delete $post->id">видалити</a> --> - </td> - </tr> -</table> -``` - -При натисканні на посилання тепер викликається дія `delete`. Щоб гарантувати, що запити будуть прийматися лише за допомогою методу POST і з того ж домену (що є ефективним захистом від CSRF-атак), використовуйте атрибут `#[Requires]`: - -```php .{file:AdminPresenter.php} -use Nette\Application\Attributes\Requires; - -class AdminPresenter extends Nette\Application\UI\Presenter -{ - #[Requires(methods: 'POST', sameOrigin: true)] - public function actionDelete(int $id): void - { - $this->facade->deletePost($id); // гіпотетичний код, що видаляє запис - $this->redirect('default'); - } -} -``` - -Атрибут існує з Nette Application 3.2, і більше про його можливості ви дізнаєтеся на сторінці [Як використовувати атрибут #Requires |attribute-requires]. - -Якби ви замість дії `actionDelete()` використовували сигнал `handleDelete()`, не потрібно вказувати `sameOrigin: true`, оскільки сигнали мають цей захист встановлений неявно: - -```php .{file:AdminPresenter.php} -#[Requires(methods: 'POST')] -public function handleDelete(int $id): void -{ - $this->facade->deletePost($id); - $this->redirect('this'); -} -``` - -Цей підхід не тільки покращує безпеку вашого додатку, але й сприяє дотриманню правильних веб-стандартів та практик. Використовуючи методи POST для дій, що змінюють стан, ви досягнете більш надійного та безпечного додатку. diff --git a/best-practices/uk/presenter-traits.texy b/best-practices/uk/presenter-traits.texy deleted file mode 100644 index 3b4ce8a1cb..0000000000 --- a/best-practices/uk/presenter-traits.texy +++ /dev/null @@ -1,47 +0,0 @@ -Компонування презентерів із трейтів -*********************************** - -.[perex] -Якщо нам потрібно реалізувати однаковий код у кількох презентерах (наприклад, перевірка, чи користувач увійшов у систему), пропонується розмістити код у спільному предку. Другим варіантом є створення одноцільових [трейтів |nette:introduction-to-object-oriented-programming#Трейди]. - -Перевага цього рішення полягає в тому, що кожен з презентерів може використовувати саме ті трейти, які йому дійсно потрібні, тоді як множинне успадкування в PHP неможливе. - -Ці трейти можуть використовувати той факт, що при створенні презентера послідовно викликаються всі [inject-методи |inject-method-attribute#Методи inject]. Потрібно лише переконатися, що назва кожного inject-методу є унікальною. - -Трейт може навішувати ініціалізаційний код на події [onStartup або onRender |application:presenters#Події]. - -Приклади: - -```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); - }; - } -} -``` - -Презентер потім просто використовує ці трейти: - -```php -class ArticlePresenter extends Nette\Application\UI\Presenter -{ - use StandardTemplateFilters; - use RequireLoggedUser; -} -``` diff --git a/best-practices/uk/restore-request.texy b/best-practices/uk/restore-request.texy deleted file mode 100644 index 63446e2b95..0000000000 --- a/best-practices/uk/restore-request.texy +++ /dev/null @@ -1,62 +0,0 @@ -Як повернутися на попередню сторінку? -************************************* - -.[perex] -Що робити, якщо користувач заповнює форму, а його сесія закінчується? Щоб дані не були втрачені, перед перенаправленням на сторінку входу ми збережемо дані в сесії. У Nette це зовсім просто. - -Поточний запит можна зберегти в сесії за допомогою методу `storeRequest()`, який поверне його ідентифікатор у вигляді короткого рядка. Метод зберігає назву поточного презентера, view та його параметри. У випадку, якщо була надіслана форма, також зберігається вміст полів (за винятком завантажених файлів). - -Відновлення запиту виконує метод `restoreRequest($key)`, якому ми передаємо отриманий ідентифікатор. Він перенаправляє на початковий презентер та view. Однак, якщо збережений запит містить надсилання форми, на початковий презентер він перейде методом `forward()`, передасть формі раніше заповнені значення і дозволить її знову відрендерити. Таким чином, користувач має можливість повторно надіслати форму, і жодні дані не втрачаються. - -Важливо, що `restoreRequest()` перевіряє, чи новозареєстрований користувач є тим самим, хто спочатку заповнював форму. Якщо ні, запит відкидається, і нічого не відбувається. - -Покажемо все на прикладі. Маємо презентер `AdminPresenter`, в якому редагуються дані і в методі `startup()` якого перевіряється, чи користувач увійшов у систему. Якщо ні, перенаправляємо його на `SignPresenter`. Водночас зберігаємо поточний запит і його ключ надсилаємо до `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()]); - } - } -} -``` - -Презентер `SignPresenter` міститиме, крім форми для входу, також персистентний параметр `$backlink`, до якого запишеться ключ. Оскільки параметр є персистентним, він передаватиметься і після надсилання форми входу. - - -```php -use Nette\Application\Attributes\Persistent; - -class SignPresenter extends Nette\Application\UI\Presenter -{ - #[Persistent] - public string $backlink = ''; - - protected function createComponentSignInForm() - { - $form = new Nette\Application\UI\Form; - // ... додамо поля форми ... - $form->onSuccess[] = [$this, 'signInFormSubmitted']; - return $form; - } - - public function signInFormSubmitted($form) - { - // ... тут користувача авторизуємо ... - - $this->restoreRequest($this->backlink); - $this->redirect('Admin:'); - } -} -``` - -Методу `restoreRequest()` ми передаємо ключ збереженого запиту, і він перенаправляє (або переходить) на початковий презентер. - -Однак, якщо ключ недійсний (наприклад, вже не існує в сесії), метод нічого не робить. Тому далі йде виклик `$this->redirect('Admin:')`, який перенаправляє на `AdminPresenter`. - -{{priority: -1}} diff --git a/bootstrap/bg/@home.texy b/bootstrap/bg/@home.texy deleted file mode 100644 index 3f9b20f4a1..0000000000 --- a/bootstrap/bg/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Настройваме отделните компоненти на Nette с помощта на конфигурационни файлове. Ще ви покажем как да зареждате тези файлове. - -.[tip] -Ако използвате целия framework, не е необходимо да правите нищо повече. В проекта имате подготвена директория `config/` за конфигурационните файлове и зареждането им се управлява от [зареждащото устройство на приложението |application:bootstrapping#Конфигурация на DI контейнера]. Тази статия е за потребители, които използват само една библиотека на Nette и искат да използват възможностите на конфигурационните файлове. - -Конфигурационните файлове обикновено се записват във [формат NEON|neon:format] и най-добре се редактират в [редактори с неговата поддръжка |best-practices:editors-and-tools#IDE редактор]. Могат да се разглеждат като ръководства за **създаване и конфигуриране** на обекти. Следователно, резултатът от зареждането на конфигурацията ще бъде така наречената фабрика, която е обект, който по заявка ще ни създаде други обекти, които искаме да използваме. Например връзка с база данни и т.н. - -Тази фабрика се нарича още *dependency injection контейнер* (DI container) и ако се интересувате от подробности, прочетете главата за [dependency injection |dependency-injection:]. - -Зареждането на конфигурацията и създаването на контейнера се извършва от класа [api:Nette\Bootstrap\Configurator], така че първо ще инсталираме неговия пакет `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -И създаваме инстанция на класа `Configurator`. Тъй като генерираният DI контейнер ще се кешира на диска, е необходимо да се зададе пътят до директорията, където ще се съхранява: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -В Linux или macOS задайте на директорията `temp/` [права за запис |nette:troubleshooting#Настройка на правата на директориите]. - -И стигаме до самите конфигурационни файлове. Зареждаме ги с помощта на `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Ако искаме да добавим повече конфигурационни файлове, можем да извикаме функцията `addConfig()` няколко пъти. Ако във файловете се появят елементи със същите ключове, те ще бъдат презаписани (или в случай на масиви [обединени |dependency-injection:configuration#Сливане]). По-късно вмъкнатият файл има по-висок приоритет от предишния. - -Последната стъпка е създаването на DI контейнера: - -```php -$container = $configurator->createContainer(); -``` - -И той вече ще ни създаде желаните обекти. Ако например използвате конфигурация за [Nette Database|database:configuration], можете да го помолите да създаде връзки с базата данни: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// или -$explorer = $container->getByType(Nette\Database\Explorer::class); -// или при създаване на повече връзки -$db = $container->getByName('database.main.connection'); -``` - -И сега вече можете да работите с базата данни! - - -Режим на разработка срещу производствен режим ---------------------------------------------- - -В режим на разработка контейнерът се актуализира автоматично при всяка промяна на конфигурационните файлове. В производствен режим се генерира само веднъж и промените не се проверяват. Режимът на разработка е насочен към максимално удобство на програмиста, докато производственият режим е насочен към производителност и реално внедряване. - -Изборът на режим се извършва чрез автоматично откриване, така че обикновено не е необходимо да конфигурирате или превключвате ръчно. Режимът е разработващ, ако приложението се изпълнява на localhost (т.е. IP адрес `127.0.0.1` или `::1`) и няма налично прокси (т.е. негов HTTP хедър). В противен случай работи в производствен режим. - -Ако искаме да разрешим режима на разработка и в други случаи, например за програмисти, достъпващи от конкретен IP адрес, използваме `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// може да се зададе и масив от IP адреси -``` - -Определено препоръчваме да комбинирате IP адрес с cookie. В cookie `nette-debug` съхраняваме таен токен, например `secret1234`, и по този начин активираме режима на разработка за програмисти, достъпващи от конкретен IP адрес и едновременно имащи споменатия токен в cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Можем също така да изключим напълно режима на разработка, дори за localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Параметри ---------- - -В конфигурационните файлове можете да използвате и параметри, които се дефинират [в секцията `parameters` |dependency-injection:configuration#Параметри]. - -Те могат да бъдат вмъкнати и отвън с помощта на метода `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Параметърът `projectId` може да бъде рефериран в конфигурацията чрез запис `%projectId%`. diff --git a/bootstrap/bg/@meta.texy b/bootstrap/bg/@meta.texy deleted file mode 100644 index 794cbc8522..0000000000 --- a/bootstrap/bg/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документация на Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/el/@home.texy b/bootstrap/el/@home.texy deleted file mode 100644 index 8b4cf46446..0000000000 --- a/bootstrap/el/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Τα διάφορα μέρη του Nette διαμορφώνονται χρησιμοποιώντας αρχεία διαμόρφωσης. Θα δείξουμε πώς να φορτώνετε αυτά τα αρχεία. - -.[tip] -Αν χρησιμοποιείτε ολόκληρο το framework, δεν χρειάζεται να κάνετε τίποτα άλλο. Στο έργο σας, έχετε έναν προετοιμασμένο κατάλογο `config/` για αρχεία διαμόρφωσης, και η φόρτωσή τους αναλαμβάνεται από τον [φορτωτή της εφαρμογής |application:bootstrapping#Διαμόρφωση του DI Container]. Αυτό το άρθρο είναι για χρήστες που χρησιμοποιούν μόνο μία βιβλιοθήκη Nette και θέλουν να εκμεταλλευτούν τις δυνατότητες των αρχείων διαμόρφωσης. - -Τα αρχεία διαμόρφωσης συνήθως γράφονται σε [μορφή NEON |neon:format] και επεξεργάζονται καλύτερα σε [editors με υποστήριξη για αυτό |best-practices:editors-and-tools#IDE editor]. Μπορούν να θεωρηθούν ως οδηγίες για το πώς να **δημιουργείτε και να διαμορφώνετε** αντικείμενα. Έτσι, το αποτέλεσμα της φόρτωσης της διαμόρφωσης θα είναι ένα λεγόμενο factory, το οποίο είναι ένα αντικείμενο που, κατόπιν αιτήματος, θα δημιουργήσει άλλα αντικείμενα που θέλουμε να χρησιμοποιήσουμε. Για παράδειγμα, συνδέσεις βάσης δεδομένων κ.λπ. - -Αυτό το factory ονομάζεται επίσης *dependency injection container* (DI container) και αν σας ενδιαφέρουν οι λεπτομέρειες, διαβάστε το κεφάλαιο για το [dependency injection |dependency-injection:]. - -Η φόρτωση της διαμόρφωσης και η δημιουργία του container αναλαμβάνεται από την κλάση [api:Nette\Bootstrap\Configurator], οπότε πρώτα θα εγκαταστήσουμε το πακέτο της `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -Και θα δημιουργήσουμε ένα στιγμιότυπο της κλάσης `Configurator`. Επειδή ο παραγόμενος DI container θα αποθηκευτεί προσωρινά στον δίσκο, είναι απαραίτητο να ορίσουμε τη διαδρομή προς τον κατάλογο όπου θα αποθηκευτεί: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Σε Linux ή macOS, ορίστε [δικαιώματα εγγραφής |nette:troubleshooting#Ρύθμιση δικαιωμάτων καταλόγου] στον κατάλογο `temp/`. - -Και φτάνουμε στα ίδια τα αρχεία διαμόρφωσης. Τα φορτώνουμε χρησιμοποιώντας το `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Αν θέλουμε να προσθέσουμε περισσότερα αρχεία διαμόρφωσης, μπορούμε να καλέσουμε τη συνάρτηση `addConfig()` πολλές φορές. Αν εμφανιστούν στοιχεία με τα ίδια κλειδιά στα αρχεία, θα αντικατασταθούν (ή στην περίπτωση πινάκων [θα συγχωνευθούν |dependency-injection:configuration#Συγχώνευση]). Το αρχείο που εισάγεται αργότερα έχει υψηλότερη προτεραιότητα από το προηγούμενο. - -Το τελευταίο βήμα είναι η δημιουργία του DI container: - -```php -$container = $configurator->createContainer(); -``` - -Και αυτός θα δημιουργήσει για εμάς τα απαιτούμενα αντικείμενα. Για παράδειγμα, αν χρησιμοποιείτε τη διαμόρφωση για το [Nette Database |database:configuration], μπορείτε να του ζητήσετε να δημιουργήσει συνδέσεις βάσης δεδομένων: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// ή -$explorer = $container->getByType(Nette\Database\Explorer::class); -// ή κατά τη δημιουργία πολλαπλών συνδέσεων -$db = $container->getByName('database.main.connection'); -``` - -Και τώρα μπορείτε να εργαστείτε με τη βάση δεδομένων! - - -Κατάσταση ανάπτυξης vs παραγωγής --------------------------------- - -Στην κατάσταση ανάπτυξης, ο container ενημερώνεται αυτόματα κάθε φορά που αλλάζουν τα αρχεία διαμόρφωσης. Στην κατάσταση παραγωγής, δημιουργείται μόνο μία φορά και οι αλλαγές δεν ελέγχονται. Η κατάσταση ανάπτυξης επικεντρώνεται στην μέγιστη άνεση του προγραμματιστή, ενώ η κατάσταση παραγωγής στην απόδοση και την πραγματική ανάπτυξη. - -Η επιλογή της κατάστασης γίνεται μέσω αυτόματης ανίχνευσης, οπότε συνήθως δεν χρειάζεται να διαμορφώσετε ή να αλλάξετε κάτι χειροκίνητα. Η κατάσταση είναι ανάπτυξης εάν η εφαρμογή εκτελείται σε localhost (δηλ. διεύθυνση IP `127.0.0.1` ή `::1`) και δεν υπάρχει proxy (δηλ. η κεφαλίδα HTTP του). Διαφορετικά, εκτελείται σε κατάσταση παραγωγής. - -Αν θέλουμε να ενεργοποιήσουμε την κατάσταση ανάπτυξης και σε άλλες περιπτώσεις, για παράδειγμα για προγραμματιστές που έχουν πρόσβαση από μια συγκεκριμένη διεύθυνση IP, χρησιμοποιούμε το `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// μπορεί να δοθεί και ένας πίνακας διευθύνσεων IP -``` - -Συνιστούμε ανεπιφύλακτα τον συνδυασμό της διεύθυνσης IP με ένα cookie. Στο cookie `nette-debug` αποθηκεύουμε ένα μυστικό token, π.χ. `secret1234`, και με αυτόν τον τρόπο ενεργοποιούμε την κατάσταση ανάπτυξης για προγραμματιστές που έχουν πρόσβαση από μια συγκεκριμένη διεύθυνση IP και ταυτόχρονα έχουν το αναφερόμενο token στο cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Μπορούμε επίσης να απενεργοποιήσουμε εντελώς την κατάσταση ανάπτυξης, ακόμη και για localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Παράμετροι ----------- - -Στα αρχεία διαμόρφωσης μπορείτε επίσης να χρησιμοποιήσετε παραμέτρους, οι οποίες ορίζονται [στην ενότητα `parameters` |dependency-injection:configuration#Παράμετροι]. - -Μπορούν επίσης να εισαχθούν από έξω χρησιμοποιώντας τη μέθοδο `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Στην παράμετρο `projectId` μπορεί να γίνει αναφορά στη διαμόρφωση με τη σύνταξη `%projectId%`. diff --git a/bootstrap/el/@meta.texy b/bootstrap/el/@meta.texy deleted file mode 100644 index a09ce5fe0d..0000000000 --- a/bootstrap/el/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/hu/@home.texy b/bootstrap/hu/@home.texy deleted file mode 100644 index 86a0ca10df..0000000000 --- a/bootstrap/hu/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -A Nette egyes részeit konfigurációs fájlok segítségével állítjuk be. Megmutatjuk, hogyan kell ezeket a fájlokat betölteni. - -.[tip] -Ha a teljes keretrendszert használja, nincs szükség további teendőkre. A projektben van egy előkészített `config/` könyvtár a konfigurációs fájlok számára, és ezek betöltéséért az [alkalmazás betöltő |application:bootstrapping#DI konténer konfigurálása] felelős. Ez a cikk azoknak a felhasználóknak szól, akik csak egy Nette könyvtárat használnak, és ki szeretnék használni a konfigurációs fájlok lehetőségeit. - -A konfigurációs fájlokat általában [NEON formátumban|neon:format] írják, és a legjobban [az azt támogató szerkesztőkben |best-practices:editors-and-tools#IDE szerkesztő] lehet szerkeszteni. Útmutatóként foghatók fel, hogyan **hozzunk létre és konfiguráljunk** objektumokat. Tehát a konfiguráció betöltésének eredménye egy úgynevezett factory lesz, ami egy olyan objektum, amely kérésre létrehozza számunkra a használni kívánt további objektumokat. Például adatbázis-kapcsolatokat stb. - -Ezt a factory-t *dependency injection konténernek* (DI konténer) is nevezik, és ha érdeklik a részletek, olvassa el a [dependency injection |dependency-injection:] fejezetet. - -A konfiguráció betöltését és a konténer létrehozását az [api:Nette\Bootstrap\Configurator] osztály végzi, ezért először telepítjük a `nette/bootstrap` csomagját: - -```shell -composer require nette/bootstrap -``` - -És létrehozunk egy `Configurator` osztály példányt. Mivel a generált DI konténer a lemezre lesz gyorsítótárazva, meg kell adni annak a könyvtárnak az elérési útját, ahová menteni fogja: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Linuxon vagy macOS-en állítson be [írási jogokat |nette:troubleshooting#Könyvtárjogosultságok beállítása] a `temp/` könyvtárnak. - -És elérkeztünk magukhoz a konfigurációs fájlokhoz. Ezeket az `addConfig()` segítségével töltjük be: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Ha több konfigurációs fájlt szeretnénk hozzáadni, többször is meghívhatjuk az `addConfig()` függvényt. Ha a fájlokban azonos kulcsú elemek jelennek meg, azok felülíródnak (vagy tömbök esetén [összevonódnak |dependency-injection:configuration#Összefésülés]). A később hozzáadott fájl magasabb prioritással rendelkezik, mint az előző. - -Az utolsó lépés a DI konténer létrehozása: - -```php -$container = $configurator->createContainer(); -``` - -És ez már létrehozza számunkra a kívánt objektumokat. Ha például a [Nette Database|database:configuration] konfigurációját használja, kérheti tőle adatbázis-kapcsolatok létrehozását: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// vagy -$explorer = $container->getByType(Nette\Database\Explorer::class); -// vagy több kapcsolat létrehozásakor -$db = $container->getByName('database.main.connection'); -``` - -És most már dolgozhat az adatbázissal! - - -Fejlesztői vs. éles üzemmód ---------------------------- - -Fejlesztői módban a konténer automatikusan frissül minden konfigurációs fájl módosításakor. Éles (produkciós) módban csak egyszer generálódik, és a változásokat nem ellenőrzi. A fejlesztői mód tehát a programozó maximális kényelmére összpontosít, az éles mód a teljesítményre és az éles bevetésre. - -Az üzemmód kiválasztása automatikus felismeréssel történik, így általában nincs szükség semmit konfigurálni vagy manuálisan váltani. Az üzemmód fejlesztői, ha az alkalmazás localhoston fut (azaz IP-cím `127.0.0.1` vagy `::1`), és nincs jelen proxy (azaz annak HTTP fejléce). Ellenkező esetben éles módban fut. - -Ha engedélyezni szeretnénk a fejlesztői módot más esetekben is, például egy adott IP-címről hozzáférő programozók számára, használjuk a `setDebugMode()` metódust: - -```php -$configurator->setDebugMode('23.75.345.200'); -// megadható IP-címek tömbje is -``` - -Mindenképpen javasoljuk az IP-cím és egy cookie kombinálását. A `nette-debug` cookie-ba mentsünk egy titkos tokent, pl. `secret1234`, és így aktiváljuk a fejlesztői módot az adott IP-címről hozzáférő és a cookie-ban említett tokennel rendelkező programozók számára: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -A fejlesztői módot teljesen ki is kapcsolhatjuk, még localhost esetén is: - -```php -$configurator->setDebugMode(false); -``` - - -Paraméterek ------------ - -A konfigurációs fájlokban paramétereket is használhat, amelyeket [a `parameters` szekcióban |dependency-injection:configuration#Paraméterek] definiálunk. - -Ezeket kívülről is beilleszthetjük az `addDynamicParameters()` metódussal: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -A `projectId` paraméterre a konfigurációban a `%projectId%` jelöléssel hivatkozhatunk. diff --git a/bootstrap/hu/@meta.texy b/bootstrap/hu/@meta.texy deleted file mode 100644 index c00a2158aa..0000000000 --- a/bootstrap/hu/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette dokumentáció}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/pt/@home.texy b/bootstrap/pt/@home.texy deleted file mode 100644 index 0344fc188a..0000000000 --- a/bootstrap/pt/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Os componentes individuais do Nette são configurados usando arquivos de configuração. Mostraremos como carregar esses arquivos. - -.[tip] -Se você estiver usando todo o framework, não há necessidade de fazer mais nada. No seu projeto, você tem um diretório `config/` pré-preparado para arquivos de configuração, e o carregamento deles é responsabilidade do [carregador da aplicação |application:bootstrapping#Configuração do contêiner de DI]. Este artigo é para usuários que usam apenas uma biblioteca Nette e desejam aproveitar as opções dos arquivos de configuração. - -Os arquivos de configuração são geralmente escritos no [formato NEON|neon:format] e são melhor editados em [editores com suporte a ele |best-practices:editors-and-tools#Editor IDE]. Eles podem ser entendidos como instruções sobre como **criar e configurar** objetos. Ou seja, o resultado do carregamento da configuração será uma chamada fábrica, que é um objeto que, sob demanda, criará outros objetos que queremos usar. Por exemplo, uma conexão de banco de dados, etc. - -Essa fábrica também é chamada de *contêiner de injeção de dependência* (contêiner DI) e, se você estiver interessado em detalhes, leia o capítulo sobre [injeção de dependência |dependency-injection:]. - -O carregamento da configuração e a criação do contêiner são feitos pela classe [api:Nette\Bootstrap\Configurator], então primeiro instalaremos seu pacote `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -E criamos uma instância da classe `Configurator`. Como o contêiner DI gerado será armazenado em cache no disco, é necessário definir o caminho para o diretório onde ele será salvo: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -No Linux ou macOS, defina as [permissões de escrita |nette:troubleshooting#Configurando Permissões de Diretório] para o diretório `temp/`. - -E chegamos aos próprios arquivos de configuração. Nós os carregamos usando `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Se quisermos adicionar mais arquivos de configuração, podemos chamar a função `addConfig()` várias vezes. Se elementos com as mesmas chaves aparecerem nos arquivos, eles serão sobrescritos (ou, no caso de arrays, [mesclados |dependency-injection:configuration#Mesclagem]). O arquivo inserido posteriormente tem prioridade maior que o anterior. - -O último passo é criar o contêiner de DI: - -```php -$container = $configurator->createContainer(); -``` - -E ele já criará os objetos necessários para nós. Por exemplo, se você estiver usando a configuração para [Nette Database|database:configuration], pode pedir a ele para criar conexões de banco de dados: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// ou -$explorer = $container->getByType(Nette\Database\Explorer::class); -// ou ao criar múltiplas conexões -$db = $container->getByName('database.main.connection'); -``` - -E agora você já pode trabalhar com o banco de dados! - - -Modo de desenvolvimento vs. produção ------------------------------------- - -No modo de desenvolvimento, o contêiner é atualizado automaticamente sempre que os arquivos de configuração são alterados. No modo de produção, ele é gerado apenas uma vez e as alterações não são verificadas. O modo de desenvolvimento é, portanto, focado no máximo conforto do programador, enquanto o modo de produção é focado no desempenho e na implantação em produção. - -A seleção do modo é feita por autodetecção, portanto, geralmente não é necessário configurar nada ou alternar manualmente. O modo é de desenvolvimento se a aplicação for executada em localhost (ou seja, endereço IP `127.0.0.1` ou `::1`) e não houver proxy presente (ou seja, seu cabeçalho HTTP). Caso contrário, ele é executado no modo de produção. - -Se quisermos habilitar o modo de desenvolvimento também em outros casos, por exemplo, para programadores acessando de um endereço IP específico, usamos `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// também pode ser especificado um array de endereços IP -``` - -Recomendamos enfaticamente combinar o endereço IP com um cookie. Armazenamos um token secreto no cookie `nette-debug`, por exemplo, `secret1234`, e dessa forma ativamos o modo de desenvolvimento para programadores acessando de um endereço IP específico e também tendo o token mencionado no cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Também podemos desativar completamente o modo de desenvolvimento, mesmo para localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parâmetros ----------- - -Nos arquivos de configuração, você também pode usar parâmetros, que são definidos [na seção `parameters` |dependency-injection:configuration#Parâmetros]. - -Eles também podem ser inseridos de fora usando o método `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -O parâmetro `projectId` pode ser referenciado na configuração usando a notação `%projectId%`. diff --git a/bootstrap/pt/@meta.texy b/bootstrap/pt/@meta.texy deleted file mode 100644 index e2566bcb44..0000000000 --- a/bootstrap/pt/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentação Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/ro/@home.texy b/bootstrap/ro/@home.texy deleted file mode 100644 index 16606ee9e5..0000000000 --- a/bootstrap/ro/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Componentele individuale Nette sunt configurate folosind fișiere de configurare. Vom arăta cum să încărcați aceste fișiere. - -.[tip] -Dacă utilizați întregul framework, nu este nevoie să faceți nimic altceva. În proiect aveți un director `config/` pregătit pentru fișierele de configurare, iar încărcarea lor este gestionată de [încărcătorul aplicației |application:bootstrapping#Configurarea containerului DI]. Acest articol este pentru utilizatorii care folosesc doar o singură bibliotecă Nette și doresc să profite de posibilitățile fișierelor de configurare. - -Fișierele de configurare sunt de obicei scrise în [formatul NEON|neon:format] și cel mai bine se editează în [editori cu suport pentru acesta |best-practices:editors-and-tools#Editor IDE]. Ele pot fi înțelese ca instrucțiuni despre cum să **creați și configurați** obiecte. Prin urmare, rezultatul încărcării configurației va fi așa-numita fabrică (factory), care este un obiect ce ne va crea la cerere alte obiecte pe care dorim să le folosim. De exemplu, conexiuni la baze de date etc. - -Această fabrică se mai numește și *dependency injection container* (container DI) și, dacă sunteți interesat de detalii, citiți capitolul despre [dependency injection |dependency-injection:]. - -Încărcarea configurației și crearea containerului sunt gestionate de clasa [api:Nette\Bootstrap\Configurator], așa că mai întâi vom instala pachetul său `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -Și vom crea o instanță a clasei `Configurator`. Deoarece containerul DI generat va fi stocat în cache pe disc, este necesar să setați calea către directorul unde va fi salvat: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Pe Linux sau macOS, setați [drepturi de scriere |nette:troubleshooting#Setarea permisiunilor pentru directoare] pentru directorul `temp/`. - -Și ajungem la fișierele de configurare în sine. Le încărcăm folosind `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Dacă dorim să adăugăm mai multe fișiere de configurare, putem apela funcția `addConfig()` de mai multe ori. Dacă în fișiere apar elemente cu aceleași chei, acestea vor fi suprascrise (sau, în cazul array-urilor, [combinate |dependency-injection:configuration#Combinare]). Fișierul încărcat ulterior are prioritate mai mare decât cel anterior. - -Ultimul pas este crearea containerului DI: - -```php -$container = $configurator->createContainer(); -``` - -Și acesta ne va crea obiectele solicitate. De exemplu, dacă utilizați configurația pentru [Nette Database|database:configuration], îi puteți cere să creeze conexiuni la baza de date: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// sau -$explorer = $container->getByType(Nette\Database\Explorer::class); -// sau la crearea mai multor conexiuni -$db = $container->getByName('database.main.connection'); -``` - -Și acum puteți lucra cu baza de date! - - -Mod dezvoltator vs mod producție --------------------------------- - -În modul dezvoltator, containerul se actualizează automat la fiecare modificare a fișierelor de configurare. În modul producție, se generează o singură dată și modificările nu sunt verificate. Modul dezvoltator este, prin urmare, axat pe confortul maxim al programatorului, iar modul producție pe performanță și implementare live. - -Selectarea modului se face prin autodetecție, deci de obicei nu este nevoie să configurați sau să comutați manual nimic. Modul este dezvoltator dacă aplicația este rulată pe localhost (adică adresa IP `127.0.0.1` sau `::1`) și nu este prezent un proxy (adică antetul său HTTP). Altfel, rulează în modul producție. - -Dacă dorim să activăm modul dezvoltator și în alte cazuri, de exemplu pentru programatorii care accesează de la o anumită adresă IP, folosim `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// se poate specifica și un array de adrese IP -``` - -Recomandăm cu tărie combinarea adresei IP cu un cookie. Vom stoca un token secret în cookie-ul `nette-debug`, de exemplu `secret1234`, și astfel vom activa modul dezvoltator pentru programatorii care accesează de la o anumită adresă IP și au în același timp tokenul menționat în cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Putem, de asemenea, să dezactivăm complet modul dezvoltator, chiar și pentru localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parametri ---------- - -În fișierele de configurare puteți utiliza și parametri, care sunt definiți [în secțiunea `parameters` |dependency-injection:configuration#Parametri]. - -Aceștia pot fi, de asemenea, inserați din exterior folosind metoda `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Parametrul `projectId` poate fi referențiat în configurație prin notația `%projectId%`. diff --git a/bootstrap/ro/@meta.texy b/bootstrap/ro/@meta.texy deleted file mode 100644 index 6554692600..0000000000 --- a/bootstrap/ro/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentație Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/sl/@home.texy b/bootstrap/sl/@home.texy deleted file mode 100644 index 76641a1cce..0000000000 --- a/bootstrap/sl/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Posamezne komponente Nette nastavljamo s pomočjo konfiguracijskih datotek. Pokazali bomo, kako te datoteke nalagati. - -.[tip] -Če uporabljate celotno ogrodje, ni treba storiti ničesar dodatnega. V projektu imate za konfiguracijske datoteke pripravljen imenik `config/` in za njihovo nalaganje skrbi [zavajalec aplikacije |application:bootstrapping#Konfiguracija DI vsebnika]. Ta članek je za uporabnike, ki uporabljajo samo eno knjižnico Nette in želijo izkoristiti možnosti konfiguracijskih datotek. - -Konfiguracijske datoteke se običajno pišejo v [formatu NEON|neon:format] in se najbolje urejajo v [urejevalnikih z njegovo podporo |best-practices:editors-and-tools#IDE urejevalnik]. Lahko jih razumemo kot navodila, kako **ustvarjati in konfigurirati** objekte. Torej bo rezultat nalaganja konfiguracije tako imenovana tovarna, kar je objekt, ki nam na zahtevo ustvari druge objekte, ki jih želimo uporabljati. Na primer povezavo s podatkovno bazo itd. - -Tej tovarni se tudi reče *dependency injection vsebnik* (DI vsebnik) in če vas zanimajo podrobnosti, preberite poglavje o [dependency injection |dependency-injection:]. - -Nalaganje konfiguracije in ustvarjanje vsebnika opravi razred [api:Nette\Bootstrap\Configurator], zato najprej namestimo njegov paket `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -In ustvarimo instanco razreda `Configurator`. Ker se bo generirani DI vsebnik predpomnil na disk, je treba nastaviti pot do imenika, kamor se bo shranjeval: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -Na Linuxu ali macOS nastavite imeniku `temp/` [pravice za pisanje |nette:troubleshooting#Nastavitev pravic map]. - -In pridemo do samih konfiguracijskih datotek. Te naložimo s pomočjo `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Če želimo dodati več konfiguracijskih datotek, lahko funkcijo `addConfig()` pokličemo večkrat. Če se v datotekah pojavijo elementi z enakimi ključi, bodo prepisani (ali v primeru polj [združeni |dependency-injection:configuration#Združevanje]). Kasneje vstavljena datoteka ima višjo prioriteto kot prejšnja. - -Zadnji korak je ustvarjanje DI vsebnika: - -```php -$container = $configurator->createContainer(); -``` - -In ta nam bo že ustvaril zahtevane objekte. Če na primer uporabljate konfiguracijo za [Nette Database|database:configuration], ga lahko prosite za ustvarjanje povezav s podatkovno bazo: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// ali -$explorer = $container->getByType(Nette\Database\Explorer::class); -// ali pri ustvarjanju več povezav -$db = $container->getByName('database.main.connection'); -``` - -In zdaj lahko že delate s podatkovno bazo! - - -Razvojni vs produkcijski način ------------------------------- - -V razvojnem načinu se vsebnik samodejno posodablja ob vsaki spremembi konfiguracijskih datotek. V produkcijskem načinu se generira samo enkrat in spremembe se ne preverjajo. Razvojni je torej usmerjen v maksimalno udobje programerja, produkcijski pa v zmogljivost in ostro uvajanje. - -Izbira načina se izvaja s samodejnim zaznavanjem, zato običajno ni treba ničesar konfigurirati ali ročno preklapljati. Način je razvojni takrat, ko je aplikacija zagnana na localhostu (tj. IP naslov `127.0.0.1` ali `::1`) in ni prisotna proxy (tj. njena HTTP glava). Sicer teče v produkcijskem načinu. - -Če želimo razvojni način omogočiti tudi v drugih primerih, na primer programerjem, ki dostopajo iz določenega IP naslova, uporabimo `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// lahko se navede tudi polje IP naslovov -``` - -Vsekakor priporočamo kombiniranje IP naslova s piškotkom. V piškotek `nette-debug` shranimo skrivni žeton, npr. `secret1234`, in na ta način aktiviramo razvojni način za programerje, ki dostopajo iz določenega IP naslova in hkrati imajo v piškotku omenjeni žeton: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Razvojni način lahko tudi popolnoma izklopimo, tudi za localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Parametri ---------- - -V konfiguracijskih datotekah lahko uporabljate tudi parametre, ki se definirajo [v sekciji `parameters` |dependency-injection:configuration#Parametri]. - -Lahko jih vstavljate tudi od zunaj s pomočjo metode `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -Na parameter `projectId` se lahko v konfiguraciji sklicujete z zapisom `%projectId%`. diff --git a/bootstrap/sl/@meta.texy b/bootstrap/sl/@meta.texy deleted file mode 100644 index 282883a3d6..0000000000 --- a/bootstrap/sl/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Dokumentacija}} -{{leftbar: nette:@menu-topics}} diff --git a/bootstrap/uk/@home.texy b/bootstrap/uk/@home.texy deleted file mode 100644 index 1320bdf9fa..0000000000 --- a/bootstrap/uk/@home.texy +++ /dev/null @@ -1,96 +0,0 @@ -Nette Bootstrap -*************** - -.[perex] -Окремі компоненти Nette налаштовуються за допомогою конфігураційних файлів. Ми покажемо, як завантажувати ці файли. - -.[tip] -Якщо ви використовуєте весь фреймворк, нічого додаткового робити не потрібно. У проекті є підготовлений каталог `config/` для конфігураційних файлів, а за їх завантаження відповідає [завантажувач застосунку |application:bootstrapping#Конфігурація DI-контейнера]. Ця стаття призначена для користувачів, які використовують лише одну бібліотеку Nette і хочуть скористатися можливостями конфігураційних файлів. - -Конфігураційні файли зазвичай записуються у [форматі NEON|neon:format] і найкраще редагуються в [редакторах з його підтримкою |best-practices:editors-and-tools#IDE редактор]. Їх можна розглядати як інструкції щодо **створення та конфігурації** об'єктів. Отже, результатом завантаження конфігурації буде так звана фабрика, тобто об'єкт, який за запитом створить для нас інші об'єкти, які ми хочемо використовувати. Наприклад, з'єднання з базою даних тощо. - -Ця фабрика також називається *dependency injection контейнером* (DI container), і якщо вас цікавлять подробиці, прочитайте розділ про [dependency injection |dependency-injection:]. - -Завантаження конфігурації та створення контейнера забезпечує клас [api:Nette\Bootstrap\Configurator], тому спочатку встановимо його пакет `nette/bootstrap`: - -```shell -composer require nette/bootstrap -``` - -І створимо екземпляр класу `Configurator`. Оскільки згенерований DI-контейнер буде кешуватися на диск, необхідно вказати шлях до каталогу, де він буде зберігатися: - -```php -$configurator = new Nette\Bootstrap\Configurator; -$configurator->setTempDirectory(__DIR__ . '/temp'); -``` - -На Linux або macOS встановіть для каталогу `temp/` [права на запис |nette:troubleshooting#Налаштування прав доступу до каталогів]. - -І ми підходимо до самих конфігураційних файлів. Їх завантажуємо за допомогою `addConfig()`: - -```php -$configurator->addConfig(__DIR__ . '/database.neon'); -``` - -Якщо ми хочемо додати більше конфігураційних файлів, можемо викликати функцію `addConfig()` кілька разів. Якщо у файлах з'являться елементи з однаковими ключами, вони будуть перезаписані (або у випадку масивів [об'єднані |dependency-injection:configuration#Об єднання]). Файл, вставлений пізніше, має вищий пріоритет, ніж попередній. - -Останнім кроком є створення DI-контейнера: - -```php -$container = $configurator->createContainer(); -``` - -І він уже створить для нас необхідні об'єкти. Наприклад, якщо ви використовуєте конфігурацію для [Nette Database|database:configuration], ви можете попросити його створити з'єднання з базою даних: - -```php -$db = $container->getByType(Nette\Database\Connection::class); -// або -$explorer = $container->getByType(Nette\Database\Explorer::class); -// або при створенні кількох з'єднань -$db = $container->getByName('database.main.connection'); -``` - -І тепер ви можете працювати з базою даних! - - -Режим розробки проти робочого режиму ------------------------------------- - -У режимі розробки контейнер автоматично оновлюється при кожній зміні конфігураційних файлів. У робочому режимі він генерується лише один раз, і зміни не перевіряються. Отже, режим розробки орієнтований на максимальну зручність програміста, а робочий — на швидкодію та розгортання. - -Вибір режиму здійснюється автовизначенням, тому зазвичай не потрібно нічого конфігурувати або вручну перемикати. Режим є розробницьким, якщо застосунок запущено на localhost (тобто IP-адреса `127.0.0.1` або `::1`) і немає проксі (тобто його HTTP-заголовка). В іншому випадку він працює в робочому режимі. - -Якщо ми хочемо увімкнути режим розробки і в інших випадках, наприклад, для програмістів, які підключаються з конкретної IP-адреси, використовуємо `setDebugMode()`: - -```php -$configurator->setDebugMode('23.75.345.200'); -// можна також вказати масив IP-адрес -``` - -Ми наполегливо рекомендуємо поєднувати IP-адресу з cookie. У cookie `nette-debug` збережемо секретний токен, наприклад, `secret1234`, і таким чином активуємо режим розробки для програмістів, які підключаються з конкретної IP-адреси і водночас мають зазначений токен у cookie: - -```php -$configurator->setDebugMode('secret1234@23.75.345.200'); -``` - -Режим розробки можна також повністю вимкнути, навіть для localhost: - -```php -$configurator->setDebugMode(false); -``` - - -Параметри ---------- - -У конфігураційних файлах ви також можете використовувати параметри, які визначаються [у секції `parameters` |dependency-injection:configuration#Параметри]. - -Їх також можна вставляти ззовні за допомогою методу `addDynamicParameters()`: - -```php -$configurator->addDynamicParameters([ - 'remoteIp' => $_SERVER['REMOTE_ADDR'], -]); -``` - -На параметр `projectId` можна посилатися в конфігурації записом `%projectId%`. diff --git a/bootstrap/uk/@meta.texy b/bootstrap/uk/@meta.texy deleted file mode 100644 index 083a8ab9f7..0000000000 --- a/bootstrap/uk/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документація Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/bg/@home.texy b/caching/bg/@home.texy deleted file mode 100644 index 8c04a7e659..0000000000 --- a/caching/bg/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Кешът ускорява вашето приложение, като съхранява данни, които са били трудно получени веднъж, за бъдеща употреба. Ще ви покажем: - -- как да използвате кеша -- как да промените хранилището -- как правилно да инвалидирате кеша - -</div> - -Използването на кеша в Nette е много лесно, като същевременно покрива и много напреднали нужди. Той е проектиран за производителност и 100% устойчивост. В основата му ще намерите адаптери за най-често срещаните бекенд хранилища. Позволява инвалидация, базирана на тагове, изтичане на времето, има защита срещу cache stampede и др. - - -Инсталация -========== - -Изтеглете и инсталирайте библиотеката с помощта на [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Основна употреба -================ - -Централният елемент на работата с кеша е обектът [api:Nette\Caching\Cache]. Създаваме негова инстанция и предаваме на конструктора така нареченото хранилище като параметър. Това е обект, представляващ мястото, където данните ще се съхраняват физически (база данни, Memcached, файлове на диска, ...). Достъп до хранилището получаваме, като го поискаме чрез [dependency injection |dependency-injection:passing-dependencies] с тип `Nette\Caching\Storage`. Всичко съществено ще научите в [раздела Хранилища |#Хранилища]. - -.[warning] -Във версия 3.0 интерфейсът все още имаше префикс `I`, така че името беше `Nette\Caching\IStorage`. Освен това константите на класа `Cache` бяха написани с главни букви, така че например `Cache::EXPIRE` вместо `Cache::Expire`. - -За следващите примери да предположим, че имаме създаден псевдоним `Cache` и в променливата `$storage` - хранилище. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // инстанция на Nette\Caching\Storage -``` - -Кешът е всъщност *key–value store*, тоест четем и записваме данни под ключове, точно както при асоциативните масиви. Приложенията се състоят от редица независими части и ако всички те използват едно хранилище (представете си една директория на диска), рано или късно ще възникне колизия на ключове. Nette Framework решава проблема, като разделя цялото пространство на именни пространства (поддиректории). Всяка част от програмата използва свое пространство с уникално име и вече не може да възникне колизия. - -Името на пространството се указва като втори параметър на конструктора на класа Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Сега можем да използваме обекта `$cache` за четене и запис в кеша. За двете цели се използва методът `load()`. Първият аргумент е ключът, а вторият е PHP callback, който се извиква, когато ключът не е намерен в кеша. Callback генерира стойността, връща я и тя се записва в кеша: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // сложно изчисление - return $computedValue; -}); -``` - -Ако вторият параметър не е указан `$value = $cache->load($key)`, ще се върне `null`, ако елементът не е в кеша. - -.[tip] -Страхотно е, че в кеша могат да се съхраняват всякакви сериализуеми структури, не само низове. Същото важи дори и за ключовете. - -Изтриваме елемент от кеша с метода `remove()`: - -```php -$cache->remove($key); -``` - -Записването на елемент в кеша може да се извърши и с метода `$cache->save($key, $value, array $dependencies = [])`. Предпочитаният начин обаче е горепосоченият чрез `load()`. - - -Мемоизация -========== - -Мемоизацията означава кеширане на резултата от извикване на функция или метод, така че да можете да го използвате следващия път, без да изчислявате същото нещо отново и отново. - -Методи и функции могат да бъдат извиквани мемоизирано с помощта на `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Функцията `gethostbyaddr()` се извиква само веднъж за всеки параметър `$ip`, а следващия път стойността се връща от кеша. - -Също така е възможно да се създаде мемоизирана обвивка над метод или функция, която може да бъде извикана по-късно: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // изчислява за първи път -$result = $memoizedFactorial(5); // втори път от кеша -``` - - -Изтичане & инвалидация -====================== - -При съхраняването в кеш е необходимо да се реши въпросът кога по-рано съхранените данни стават невалидни. Nette Framework предлага механизъм за ограничаване на валидността на данните или за тяхното контролирано изтриване (в терминологията на framework-а „инвалидиране“). - -Валидността на данните се задава в момента на записване чрез третия параметър на метода `save()`, напр.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Или чрез параметъра `$dependencies`, предаден по референция към callback-а на метода `load()`, напр.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Или чрез 3-тия параметър в метода `load()`, напр: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -В следващите примери ще предположим втория вариант и следователно съществуването на променливата `$dependencies`. - - -Изтичане --------- - -Най-простото изтичане е времевият лимит. По този начин съхраняваме данни в кеша с валидност 20 минути: - -```php -// приема също брой секунди или UNIX timestamp -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Ако искаме да удължим срока на валидност при всяко четене, това може да се постигне по следния начин, но внимавайте, режийните разходи на кеша ще се увеличат: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Удобна е възможността данните да изтекат в момента, в който се промени файл или някой от няколко файла. Това може да се използва например при съхраняване на данни, възникнали при обработката на тези файлове, в кеша. Използвайте абсолютни пътища. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// или -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Можем да накараме елемент в кеша да изтече в момента, в който изтече друг елемент (или някой от няколко други). Това може да се използва, когато съхраняваме в кеша например цяла HTML страница и под други ключове нейните фрагменти. Щом фрагментът се промени, цялата страница се инвалидира. Ако фрагментите са съхранени под ключове напр. `frag1` и `frag2`, използваме: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Изтичането може да се контролира и с помощта на персонализирани функции или статични методи, които при всяко четене решават дали елементът е все още валиден. По този начин например можем да накараме елемент да изтече винаги, когато се промени версията на PHP. Създаваме функция, която сравнява текущата версия с параметъра, и при записване добавяме към зависимостите масив във формат `[име на функция, ...аргументи]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // изтече, когато checkPhpVersion(...) === false -]; -``` - -Всички критерии, разбира се, могат да се комбинират. Кешът тогава изтича, когато поне един критерий не е изпълнен. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Инвалидация чрез тагове ------------------------ - -Много полезен инструмент за инвалидация са така наречените тагове. Към всеки елемент в кеша можем да присвоим списък с тагове, които са произволни низове. Да вземем например HTML страница със статия и коментари, която ще кешираме. При записване посочваме таговете: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Да се преместим в администрацията. Тук намираме форма за редактиране на статия. Заедно със записването на статията в базата данни извикваме командата `clean()`, която изтрива от кеша елементи според тага: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -По същия начин, на мястото на добавяне на нов коментар (или редактиране на коментар) не забравяме да инвалидираме съответния таг: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Какво постигнахме с това? Че HTML кешът ще се инвалидира (изтрива), когато статията или коментарите се променят. Когато се редактира статия с ID = 10, се извършва принудителна инвалидация на тага `article/10` и HTML страницата, която носи посочения таг, се изтрива от кеша. Същото се случва и при вмъкване на нов коментар под съответната статия. - -.[note] -Таговете изискват така наречения [#Journal]. - - -Инвалидация чрез приоритет --------------------------- - -На отделните елементи в кеша можем да зададем приоритет, с който ще може да ги изтриваме, когато например кешът надхвърли определен размер: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Изтриваме всички елементи с приоритет равен или по-малък от 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Приоритетите изискват така наречения [#Journal]. - - -Изтриване на кеша ------------------ - -Параметърът `Cache::All` изтрива всичко: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Групово четене -============== - -За групово четене и запис в кеша се използва методът `bulkLoad()`, на който предаваме масив от ключове и получаваме масив от стойности: - -```php -$values = $cache->bulkLoad($keys); -``` - -Методът `bulkLoad()` работи подобно на `load()` и с втория параметър callback, на който се предава ключът на генерирания елемент: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // сложно изчисление - return $computedValue; -}); -``` - - -Използване с PSR-16 .{data-version:3.3.1} -========================================= - -За използване на Nette Cache с интерфейса PSR-16 можете да използвате адаптера `PsrCacheAdapter`. Той позволява безпроблемна интеграция между Nette Cache и всеки код или библиотека, която очаква PSR-16 съвместим кеш. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Сега можете да използвате `$psrCache` като PSR-16 кеш: - -```php -$psrCache->set('key', 'value', 3600); // съхранява стойността за 1 час -$value = $psrCache->get('key', 'default'); -``` - -Адаптерът поддържа всички методи, дефинирани в PSR-16, включително `getMultiple()`, `setMultiple()` и `deleteMultiple()`. - - -Кеширане на изхода -================== - -Много елегантно може да се улавя и кешира изходът: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // изписваме данни - - $capture->end(); // записваме изхода в кеша -} -``` - -В случай, че изходът вече е съхранен в кеша, методът `capture()` го изписва и връща `null`, така че условието не се изпълнява. В противен случай започва да улавя изхода и връща обект `$capture`, с помощта на който накрая записваме изписаните данни в кеша. - -.[note] -Във версия 3.0 методът се наричаше `$cache->start()`. - - -Кеширане в Latte -================ - -Кеширането в шаблоните [Latte|latte:] е много лесно, достатъчно е част от шаблона да се обвие в тагове `{cache}...{/cache}`. Кешът се инвалидира автоматично в момента, в който се промени изходният шаблон (включително евентуални включени шаблони вътре в кеш блока). Таговете `{cache}` могат да се влагат един в друг и когато вложен блок се инвалидира (например с таг), се инвалидира и родителският блок. - -В тага е възможно да се посочат ключове, към които ще се обвърже кешът (тук променливата `$id`) и да се зададе изтичане и [тагове за инвалидация |#Инвалидация чрез тагове] - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Всички елементи са незадължителни, така че не е необходимо да посочваме нито изтичане, нито тагове, нито дори ключове. - -Използването на кеша може да бъде обусловено и с помощта на `if` - съдържанието тогава ще се кешира само ако условието е изпълнено: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Хранилища -========= - -Хранилището е обект, представляващ мястото, където данните се съхраняват физически. Можем да използваме база данни, сървър Memcached или най-достъпното хранилище, което са файлове на диска. - -|----------------- -| Хранилище | Описание -|----------------- -| [#FileStorage] | хранилище по подразбиране със съхранение във файлове на диска -| [#MemcachedStorage] | използва `Memcached` сървър -| [#MemoryStorage] | данните са временно в паметта -| [#SQLiteStorage] | данните се съхраняват в SQLite база данни -| [#DevNullStorage] | данните не се съхраняват, подходящо за тестване - -Достъп до обекта на хранилището получавате, като го поискате чрез [dependency injection |dependency-injection:passing-dependencies] с тип `Nette\Caching\Storage`. Като хранилище по подразбиране Nette предоставя обект FileStorage, съхраняващ данни в поддиректория `cache` в директорията за [временни файлове |application:bootstrapping#Временни файлове]. - -Можете да промените хранилището в конфигурацията: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Записва кеша във файлове на диска. Хранилището `Nette\Caching\Storages\FileStorage` е много добре оптимизирано за производителност и преди всичко осигурява пълна атомарност на операциите. Какво означава това? Че при използване на кеша не може да се случи да прочетем файл, който все още не е напълно записан от друг поток, или някой да го изтрие "под носа ни". Използването на кеша е напълно безопасно. - -Това хранилище има и вградена важна функция, която предотвратява екстремно нарастване на използването на CPU в момента, когато кешът се изтрие или все още не е загрят (т.е. създаден). Това е превенция срещу "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Случва се в един момент да се съберат по-голям брой едновременни заявки, които искат от кеша едно и също нещо (например резултат от скъпа SQL заявка) и тъй като то не е в кеша, всички процеси започват да изпълняват същата SQL заявка. Натоварването се умножава и дори може да се случи нито един поток да не успее да отговори в рамките на времевия лимит, кешът да не се създаде и приложението да се срине. За щастие, кешът в Nette работи така, че при повече едновременни заявки за един елемент, той се генерира само от първия поток, останалите чакат и след това използват генерирания резултат. - -Пример за създаване на FileStorage: - -```php -// хранилището ще бъде директория '/path/to/temp' на диска -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Сървърът [Memcached|https://memcached.org] е високопроизводителна система за съхранение в разпределена памет, чийто адаптер е `Nette\Caching\Storages\MemcachedStorage`. В конфигурацията посочваме IP адрес и порт, ако се различават от стандартния 11211. - -.[caution] -Изисква PHP разширение `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` е хранилище, което съхранява данни в PHP масив, и следователно те се губят с прекратяването на заявката. - - -SQLiteStorage -------------- - -Базата данни SQLite и адаптерът `Nette\Caching\Storages\SQLiteStorage` предлагат начин за съхраняване на кеша в един файл на диска. В конфигурацията посочваме пътя до този файл. - -.[caution] -Изисква PHP разширения `pdo` и `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Специална имплементация на хранилище е `Nette\Caching\Storages\DevNullStorage`, което всъщност изобщо не съхранява данни. Подходящо е за тестване, когато искаме да елиминираме влиянието на кеша. - - -Използване на кеша в кода -========================= - -При използване на кеша в кода имаме два начина да го направим. Първият е да поискаме хранилището чрез [dependency injection |dependency-injection:passing-dependencies] и да създадем обект `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Втората възможност е директно да поискаме обект `Cache`: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Обектът `Cache` след това се създава директно в конфигурацията по следния начин: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette съхранява тагове и приоритети в така наречения journal. Стандартно за това се използва SQLite и файл `journal.s3db` и **се изискват PHP разширения `pdo` и `pdo_sqlite`.** - -Можете да промените journal-а в конфигурацията: - -```neon -services: - cache.journal: MyJournal -``` - - -DI Сървиси -========== - -Тези сървиси се добавят към DI контейнера: - -| Име | Тип | Описание -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | хранилище - - -Изключване на кеша -================== - -Една от възможностите за изключване на кеша в приложението е да се зададе като хранилище [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Тази настройка не влияе на кеширането на шаблони в Latte или DI контейнера, тъй като тези библиотеки не използват услугите на nette/caching и управляват кеша си самостоятелно. Техният кеш впрочем [не е необходимо да се изключва |nette:troubleshooting#Как да изключите кеша по време на разработка] в режим на разработка. diff --git a/caching/bg/@meta.texy b/caching/bg/@meta.texy deleted file mode 100644 index 794cbc8522..0000000000 --- a/caching/bg/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документация на Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/el/@home.texy b/caching/el/@home.texy deleted file mode 100644 index a9c9f774d5..0000000000 --- a/caching/el/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Η Cache επιταχύνει την εφαρμογή σας αποθηκεύοντας δεδομένα που αποκτήθηκαν με κόπο μία φορά για μελλοντική χρήση. Θα δείξουμε: - -- πώς να χρησιμοποιήσετε την cache -- πώς να αλλάξετε την αποθήκη -- πώς να ακυρώσετε σωστά την cache - -</div> - -Η χρήση της cache στο Nette είναι πολύ εύκολη, ενώ καλύπτει και πολύ προηγμένες ανάγκες. Είναι σχεδιασμένη για απόδοση και 100% ανθεκτικότητα. Στη βάση θα βρείτε προσαρμογείς για τις πιο συνηθισμένες αποθήκες backend. Επιτρέπει την ακύρωση βάσει tags, την λήξη βάσει χρόνου, έχει προστασία έναντι cache stampede κ.λπ. - - -Εγκατάσταση -=========== - -Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας το εργαλείο [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Βασική Χρήση -============ - -Ο πυρήνας της εργασίας με την cache, ή την προσωρινή μνήμη, είναι το αντικείμενο [api:Nette\Caching\Cache]. Δημιουργούμε ένα στιγμιότυπό του και περνάμε στον κατασκευαστή την λεγόμενη αποθήκη ως παράμετρο. Αυτό είναι ένα αντικείμενο που αντιπροσωπεύει τον τόπο όπου τα δεδομένα θα αποθηκευτούν φυσικά (βάση δεδομένων, Memcached, αρχεία στον δίσκο, ...). Έχουμε πρόσβαση στην αποθήκη αφήνοντάς την να περάσει μέσω [dependency injection |dependency-injection:passing-dependencies] με τον τύπο `Nette\Caching\Storage`. Όλα τα απαραίτητα θα τα μάθετε στην [ενότητα Αποθήκες |#Αποθήκες]. - -.[warning] -Στην έκδοση 3.0, το interface είχε ακόμα το πρόθεμα `I`, οπότε το όνομα ήταν `Nette\Caching\IStorage`. Επιπλέον, οι σταθερές της κλάσης `Cache` γράφονταν με κεφαλαία γράμματα, οπότε για παράδειγμα `Cache::EXPIRE` αντί για `Cache::Expire`. - -Για τα παρακάτω παραδείγματα, ας υποθέσουμε ότι έχουμε δημιουργήσει ένα alias `Cache` και στην μεταβλητή `$storage` την αποθήκη. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // στιγμιότυπο του Nette\Caching\Storage -``` - -Η cache είναι στην πραγματικότητα ένα *key–value store*, δηλαδή διαβάζουμε και γράφουμε δεδομένα υπό κλειδιά, όπως και με τους συσχετιστικούς πίνακες. Οι εφαρμογές αποτελούνται από μια σειρά ανεξάρτητων τμημάτων και αν όλα χρησιμοποιούσαν μία αποθήκη (φανταστείτε έναν κατάλογο στον δίσκο), αργά ή γρήγορα θα προέκυπτε σύγκρουση κλειδιών. Το Nette Framework λύνει το πρόβλημα χωρίζοντας ολόκληρο τον χώρο σε namespaces (υποκαταλόγους). Κάθε τμήμα του προγράμματος χρησιμοποιεί τότε τον δικό του χώρο με ένα μοναδικό όνομα και δεν μπορεί πλέον να υπάρξει καμία σύγκρουση. - -Το όνομα του χώρου αναφέρεται ως η δεύτερη παράμετρος του κατασκευαστή της κλάσης Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Τώρα μπορούμε να χρησιμοποιήσουμε το αντικείμενο `$cache` για να διαβάσουμε και να γράψουμε στην προσωρινή μνήμη. Η μέθοδος `load()` χρησιμοποιείται και για τα δύο. Το πρώτο όρισμα είναι το κλειδί και το δεύτερο είναι ένα PHP callback που καλείται όταν το κλειδί δεν βρίσκεται στην cache. Το callback παράγει την τιμή, την επιστρέφει και αποθηκεύεται στην cache: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // απαιτητικός υπολογισμός - return $computedValue; -}); -``` - -Αν η δεύτερη παράμετρος δεν καθοριστεί `$value = $cache->load($key)`, θα επιστραφεί `null` αν το στοιχείο δεν υπάρχει στην cache. - -.[tip] -Είναι υπέροχο που οποιαδήποτε σειριοποιήσιμη δομή μπορεί να αποθηκευτεί στην cache, όχι μόνο συμβολοσειρές. Και το ίδιο ισχύει ακόμη και για τα κλειδιά. - -Ένα στοιχείο διαγράφεται από την προσωρινή μνήμη χρησιμοποιώντας τη μέθοδο `remove()`: - -```php -$cache->remove($key); -``` - -Η αποθήκευση ενός στοιχείου στην προσωρινή μνήμη μπορεί επίσης να γίνει με τη μέθοδο `$cache->save($key, $value, array $dependencies = [])`. Ωστόσο, προτιμάται η παραπάνω μέθοδος χρησιμοποιώντας το `load()`. - - -Memoization -=========== - -Memoization σημαίνει την προσωρινή αποθήκευση του αποτελέσματος μιας κλήσης συνάρτησης ή μεθόδου, ώστε να μπορείτε να το χρησιμοποιήσετε την επόμενη φορά χωρίς να υπολογίζετε ξανά το ίδιο πράγμα. - -Μέθοδοι και συναρτήσεις μπορούν να κληθούν με memoization χρησιμοποιώντας το `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Η συνάρτηση `gethostbyaddr()` καλείται έτσι μόνο μία φορά για κάθε παράμετρο `$ip`, και την επόμενη φορά η τιμή επιστρέφεται από την cache. - -Είναι επίσης δυνατό να δημιουργηθεί ένα memoized wrapper γύρω από μια μέθοδο ή συνάρτηση που μπορεί να κληθεί αργότερα: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // υπολογίζει την πρώτη φορά -$result = $memoizedFactorial(5); // τη δεύτερη φορά από την cache -``` - - -Λήξη & Ακύρωση -============== - -Με την αποθήκευση στην cache, είναι απαραίτητο να αντιμετωπιστεί το ζήτημα του πότε τα προηγουμένως αποθηκευμένα δεδομένα καθίστανται άκυρα. Το Nette Framework προσφέρει έναν μηχανισμό για τον περιορισμό της εγκυρότητας των δεδομένων ή την ελεγχόμενη διαγραφή τους (στην ορολογία του framework "ακύρωση"). - -Η εγκυρότητα των δεδομένων ορίζεται τη στιγμή της αποθήκευσης χρησιμοποιώντας την τρίτη παράμετρο της μεθόδου `save()`, π.χ.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Ή χρησιμοποιώντας την παράμετρο `$dependencies` που περνιέται με αναφορά στο callback της μεθόδου `load()`, π.χ.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Ή χρησιμοποιώντας την 3η παράμετρο στη μέθοδο `load()`, π.χ: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -Στα επόμενα παραδείγματα, θα υποθέσουμε τη δεύτερη παραλλαγή και συνεπώς την ύπαρξη της μεταβλητής `$dependencies`. - - -Λήξη ----- - -Η απλούστερη λήξη είναι ένα χρονικό όριο. Έτσι αποθηκεύουμε δεδομένα στην cache με ισχύ 20 λεπτών: - -```php -// δέχεται επίσης τον αριθμό των δευτερολέπτων ή UNIX timestamp -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Αν θέλαμε να παρατείνουμε την περίοδο ισχύος με κάθε ανάγνωση, αυτό μπορεί να επιτευχθεί ως εξής, αλλά προσέξτε, το overhead της cache αυξάνεται: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Είναι χρήσιμη η δυνατότητα να λήξουν τα δεδομένα τη στιγμή που αλλάζει ένα αρχείο ή κάποιο από τα περισσότερα αρχεία. Αυτό μπορεί να χρησιμοποιηθεί, για παράδειγμα, κατά την αποθήκευση δεδομένων που προκύπτουν από την επεξεργασία αυτών των αρχείων στην cache. Χρησιμοποιήστε απόλυτες διαδρομές. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// ή -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Μπορούμε να αφήσουμε ένα στοιχείο στην cache να λήξει τη στιγμή που λήγει ένα άλλο στοιχείο (ή κάποιο από τα περισσότερα άλλα). Αυτό μπορεί να χρησιμοποιηθεί όταν αποθηκεύουμε, για παράδειγμα, ολόκληρη τη σελίδα HTML στην cache και τα τμήματά της κάτω από άλλα κλειδιά. Μόλις αλλάξει ένα τμήμα, ακυρώνεται ολόκληρη η σελίδα. Αν έχουμε αποθηκεύσει τα τμήματα κάτω από κλειδιά π.χ. `frag1` και `frag2`, χρησιμοποιούμε: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Η λήξη μπορεί επίσης να ελεγχθεί χρησιμοποιώντας προσαρμοσμένες συναρτήσεις ή στατικές μεθόδους, οι οποίες αποφασίζουν πάντα κατά την ανάγνωση αν το στοιχείο είναι ακόμα έγκυρο. Έτσι, για παράδειγμα, μπορούμε να αφήσουμε ένα στοιχείο να λήξει κάθε φορά που αλλάζει η έκδοση της PHP. Δημιουργούμε μια συνάρτηση που συγκρίνει την τρέχουσα έκδοση με την παράμετρο, και κατά την αποθήκευση προσθέτουμε μεταξύ των εξαρτήσεων έναν πίνακα της μορφής `[όνομα συνάρτησης, ...ορίσματα]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // λήξη όταν checkPhpVersion(...) === false -]; -``` - -Όλα τα κριτήρια μπορούν φυσικά να συνδυαστούν. Η cache θα λήξει τότε όταν τουλάχιστον ένα κριτήριο δεν πληρείται. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Ακύρωση με χρήση tags ---------------------- - -Ένα πολύ χρήσιμο εργαλείο ακύρωσης είναι τα λεγόμενα tags. Μπορούμε να αντιστοιχίσουμε σε κάθε στοιχείο της cache μια λίστα από tags, που είναι οποιεσδήποτε συμβολοσειρές. Ας υποθέσουμε ότι έχουμε μια HTML σελίδα με ένα άρθρο και σχόλια, την οποία θα αποθηκεύσουμε στην cache. Κατά την αποθήκευση, καθορίζουμε τα tags: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Ας μεταφερθούμε στη διαχείριση. Εδώ βρίσκουμε μια φόρμα για την επεξεργασία του άρθρου. Μαζί με την αποθήκευση του άρθρου στη βάση δεδομένων, καλούμε την εντολή `clean()`, η οποία διαγράφει από την cache τα στοιχεία σύμφωνα με το tag: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Ομοίως, στο σημείο προσθήκης νέου σχολίου (ή επεξεργασίας σχολίου), δεν παραλείπουμε να ακυρώσουμε το σχετικό tag: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Τι πετύχαμε με αυτό; Ότι η HTML cache μας θα ακυρώνεται (διαγράφεται) κάθε φορά που αλλάζει το άρθρο ή τα σχόλια. Όταν επεξεργάζεται το άρθρο με ID = 10, γίνεται αναγκαστική ακύρωση του tag `article/10` και η HTML σελίδα που φέρει το εν λόγω tag διαγράφεται από την cache. Το ίδιο συμβαίνει κατά την εισαγωγή νέου σχολίου κάτω από το σχετικό άρθρο. - -.[note] -Τα tags απαιτούν το λεγόμενο [#Journal]. - - -Ακύρωση με χρήση προτεραιότητας -------------------------------- - -Μπορούμε να ορίσουμε μια προτεραιότητα για μεμονωμένα στοιχεία στην cache, με βάση την οποία θα μπορούν να διαγραφούν όταν, για παράδειγμα, η cache υπερβεί ένα συγκεκριμένο μέγεθος: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Διαγράφουμε όλα τα στοιχεία με προτεραιότητα ίση ή μικρότερη από 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Οι προτεραιότητες απαιτούν το λεγόμενο [#Journal]. - - -Διαγραφή της cache ------------------- - -Η παράμετρος `Cache::All` διαγράφει τα πάντα: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Μαζική ανάγνωση -=============== - -Για μαζικές αναγνώσεις και εγγραφές στην cache χρησιμοποιείται η μέθοδος `bulkLoad()`, στην οποία περνάμε έναν πίνακα κλειδιών και λαμβάνουμε έναν πίνακα τιμών: - -```php -$values = $cache->bulkLoad($keys); -``` - -Η μέθοδος `bulkLoad()` λειτουργεί παρόμοια με το `load()` και με τη δεύτερη παράμετρο callback, στην οποία περνιέται το κλειδί του παραγόμενου στοιχείου: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // απαιτητικός υπολογισμός - return $computedValue; -}); -``` - - -Χρήση με PSR-16 .{data-version:3.3.1} -===================================== - -Για να χρησιμοποιήσετε την Nette Cache με το interface PSR-16, μπορείτε να χρησιμοποιήσετε τον προσαρμογέα `PsrCacheAdapter`. Επιτρέπει την απρόσκοπτη ενσωμάτωση μεταξύ της Nette Cache και οποιουδήποτε κώδικα ή βιβλιοθήκης που αναμένει μια cache συμβατή με PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Τώρα μπορείτε να χρησιμοποιήσετε το `$psrCache` ως PSR-16 cache: - -```php -$psrCache->set('key', 'value', 3600); // αποθηκεύει την τιμή για 1 ώρα -$value = $psrCache->get('key', 'default'); -``` - -Ο προσαρμογέας υποστηρίζει όλες τις μεθόδους που ορίζονται στο PSR-16, συμπεριλαμβανομένων των `getMultiple()`, `setMultiple()`, και `deleteMultiple()`. - - -Caching εξόδου -============== - -Μπορείτε να συλλάβετε και να αποθηκεύσετε στην cache την έξοδο πολύ κομψά: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // εκτυπώνουμε δεδομένα - - $capture->end(); // αποθηκεύουμε την έξοδο στην cache -} -``` - -Σε περίπτωση που η έξοδος είναι ήδη αποθηκευμένη στην cache, η μέθοδος `capture()` την εκτυπώνει και επιστρέφει `null`, οπότε η συνθήκη δεν εκτελείται. Διαφορετικά, αρχίζει να συλλαμβάνει την έξοδο και επιστρέφει το αντικείμενο `$capture`, με το οποίο τελικά αποθηκεύουμε τα εκτυπωμένα δεδομένα στην cache. - -.[note] -Στην έκδοση 3.0, η μέθοδος ονομαζόταν `$cache->start()`. - - -Caching στο Latte -================= - -Το caching στα πρότυπα [Latte |latte:] είναι πολύ εύκολο, αρκεί να περιβάλλετε ένα μέρος του προτύπου με τα tags `{cache}...{/cache}`. Η cache ακυρώνεται αυτόματα τη στιγμή που αλλάζει το πρότυπο προέλευσης (συμπεριλαμβανομένων τυχόν ενσωματωμένων προτύπων εντός του μπλοκ cache). Τα tags `{cache}` μπορούν να ενσωματωθούν το ένα μέσα στο άλλο, και όταν ένα ενσωματωμένο μπλοκ ακυρωθεί (για παράδειγμα, με ένα tag), ακυρώνεται και το γονικό μπλοκ. - -Στο tag είναι δυνατό να αναφερθούν κλειδιά στα οποία θα συνδεθεί η cache (εδώ η μεταβλητή `$id`) και να οριστεί η λήξη και τα [tags για ακύρωση |#Ακύρωση με χρήση tags] - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Όλα τα στοιχεία είναι προαιρετικά, οπότε δεν χρειάζεται να καθορίσουμε ούτε λήξη, ούτε tags, ούτε καν κλειδιά. - -Η χρήση της cache μπορεί επίσης να εξαρτηθεί από συνθήκη χρησιμοποιώντας το `if` - το περιεχόμενο θα αποθηκευτεί στην cache μόνο αν η συνθήκη πληρείται: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Αποθήκες -======== - -Μια αποθήκη είναι ένα αντικείμενο που αντιπροσωπεύει τον τόπο όπου τα δεδομένα αποθηκεύονται φυσικά. Μπορούμε να χρησιμοποιήσουμε μια βάση δεδομένων, έναν διακομιστή Memcached, ή την πιο προσιτή αποθήκη, που είναι τα αρχεία στον δίσκο. - -|----------------- -| Αποθήκη | Περιγραφή -|----------------- -| [#FileStorage] | προεπιλεγμένη αποθήκη με αποθήκευση σε αρχεία στον δίσκο -| [#MemcachedStorage] | χρησιμοποιεί τον διακομιστή `Memcached` -| [#MemoryStorage] | τα δεδομένα είναι προσωρινά στη μνήμη -| [#SQLiteStorage] | τα δεδομένα αποθηκεύονται σε βάση δεδομένων SQLite -| [#DevNullStorage] | τα δεδομένα δεν αποθηκεύονται, κατάλληλο για testing - -Μπορείτε να αποκτήσετε πρόσβαση στο αντικείμενο αποθήκης αφήνοντάς το να περάσει μέσω [dependency injection |dependency-injection:passing-dependencies] με τον τύπο `Nette\Caching\Storage`. Ως προεπιλεγμένη αποθήκη, το Nette παρέχει το αντικείμενο FileStorage που αποθηκεύει δεδομένα στον υποκατάλογο `cache` στον κατάλογο για [προσωρινά αρχεία |application:bootstrapping#Προσωρινά Αρχεία]. - -Μπορείτε να αλλάξετε την αποθήκη στη διαμόρφωση: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Γράφει την cache σε αρχεία στον δίσκο. Η αποθήκη `Nette\Caching\Storages\FileStorage` είναι πολύ καλά βελτιστοποιημένη για απόδοση και κυρίως εξασφαλίζει πλήρη ατομικότητα των λειτουργιών. Τι σημαίνει αυτό; Ότι κατά τη χρήση της cache, δεν μπορεί να συμβεί να διαβάσουμε ένα αρχείο που δεν έχει ακόμη γραφτεί πλήρως από άλλο νήμα, ή να το διαγράψει κάποιος "κάτω από τα χέρια μας". Η χρήση της cache είναι επομένως απολύτως ασφαλής. - -Αυτή η αποθήκη έχει επίσης ενσωματωμένη μια σημαντική λειτουργία που εμποδίζει την ακραία αύξηση της χρήσης της CPU τη στιγμή που η cache διαγράφεται ή δεν έχει ακόμη θερμανθεί (δηλ. δημιουργηθεί). Πρόκειται για πρόληψη έναντι του "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Συμβαίνει ότι σε μία στιγμή συγκεντρώνεται μεγαλύτερος αριθμός ταυτόχρονων αιτημάτων που θέλουν το ίδιο πράγμα από την cache (π.χ. το αποτέλεσμα ενός ακριβού ερωτήματος SQL) και επειδή δεν υπάρχει στην προσωρινή μνήμη, όλες οι διεργασίες αρχίζουν να εκτελούν το ίδιο ερώτημα SQL. Η φόρτωση έτσι πολλαπλασιάζεται και μπορεί ακόμη και να συμβεί καμία διεργασία να μην προλάβει να απαντήσει εντός του χρονικού ορίου, η cache να μην δημιουργηθεί και η εφαρμογή να καταρρεύσει. Ευτυχώς, η cache στο Nette λειτουργεί έτσι ώστε κατά τη διάρκεια πολλαπλών ταυτόχρονων αιτημάτων για ένα στοιχείο, το παράγει μόνο το πρώτο νήμα, τα υπόλοιπα περιμένουν και στη συνέχεια χρησιμοποιούν το παραγόμενο αποτέλεσμα. - -Παράδειγμα δημιουργίας FileStorage: - -```php -// η αποθήκη θα είναι ο κατάλογος '/path/to/temp' στον δίσκο -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Ο διακομιστής [Memcached |https://memcached.org] είναι ένα σύστημα αποθήκευσης υψηλής απόδοσης σε κατανεμημένη μνήμη, του οποίου ο προσαρμογέας είναι ο `Nette\Caching\Storages\MemcachedStorage`. Στη διαμόρφωση, αναφέρουμε τη διεύθυνση IP και τη θύρα, αν διαφέρει από την προεπιλεγμένη 11211. - -.[caution] -Απαιτεί την επέκταση PHP `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -Το `Nette\Caching\Storages\MemoryStorage` είναι μια αποθήκη που αποθηκεύει δεδομένα σε έναν πίνακα PHP, και επομένως χάνονται με τον τερματισμό του αιτήματος. - - -SQLiteStorage -------------- - -Η βάση δεδομένων SQLite και ο προσαρμογέας `Nette\Caching\Storages\SQLiteStorage` προσφέρουν έναν τρόπο αποθήκευσης της cache σε ένα μόνο αρχείο στον δίσκο. Στη διαμόρφωση, αναφέρουμε τη διαδρομή προς αυτό το αρχείο. - -.[caution] -Απαιτεί τις επεκτάσεις PHP `pdo` και `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Μια ειδική υλοποίηση αποθήκης είναι η `Nette\Caching\Storages\DevNullStorage`, η οποία στην πραγματικότητα δεν αποθηκεύει καθόλου δεδομένα. Είναι επομένως κατάλληλη για testing, όταν θέλουμε να εξαλείψουμε την επίδραση της cache. - - -Χρήση της cache στον κώδικα -=========================== - -Κατά τη χρήση της cache στον κώδικα, έχουμε δύο τρόπους για να το κάνουμε. Ο πρώτος είναι να αφήσουμε την αποθήκη να περάσει μέσω [dependency injection |dependency-injection:passing-dependencies] και να δημιουργήσουμε ένα αντικείμενο `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Η δεύτερη επιλογή είναι να αφήσουμε το αντικείμενο `Cache` να περάσει απευθείας: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Το αντικείμενο `Cache` δημιουργείται στη συνέχεια απευθείας στη διαμόρφωση με αυτόν τον τρόπο: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Το Nette αποθηκεύει τα tags και τις προτεραιότητες στο λεγόμενο journal. Για αυτό χρησιμοποιείται συνήθως το SQLite και το αρχείο `journal.s3db` και **απαιτούνται οι επεκτάσεις PHP `pdo` και `pdo_sqlite`.** - -Μπορείτε να αλλάξετε το journal στη διαμόρφωση: - -```neon -services: - cache.journal: MyJournal -``` - - -Υπηρεσίες DI -============ - -Αυτές οι υπηρεσίες προστίθενται στον DI container: - -| Όνομα | Τύπος | Περιγραφή -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | αποθήκη - - -Απενεργοποίηση της cache -======================== - -Μία από τις επιλογές για την απενεργοποίηση της cache στην εφαρμογή είναι να ορίσετε ως αποθήκη την [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Αυτή η ρύθμιση δεν επηρεάζει το caching των προτύπων στο Latte ή τον DI container, καθώς αυτές οι βιβλιοθήκες δεν χρησιμοποιούν τις υπηρεσίες nette/caching και διαχειρίζονται την cache τους ανεξάρτητα. Εξάλλου, η cache τους [δεν χρειάζεται να απενεργοποιηθεί |nette:troubleshooting#Πώς να απενεργοποιήσετε την cache κατά την ανάπτυξη] στη λειτουργία ανάπτυξης. diff --git a/caching/el/@meta.texy b/caching/el/@meta.texy deleted file mode 100644 index a09ce5fe0d..0000000000 --- a/caching/el/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/hu/@home.texy b/caching/hu/@home.texy deleted file mode 100644 index 25518b3721..0000000000 --- a/caching/hu/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -A Cache felgyorsítja az alkalmazást azáltal, hogy az egyszer nehezen megszerzett adatokat elmenti a későbbi felhasználásra. Megmutatjuk: - -- hogyan használjuk a cache-t -- hogyan változtassuk meg a tárolót -- hogyan érvénytelenítsük helyesen a cache-t - -</div> - -A cache használata a Nette-ben nagyon egyszerű, miközben nagyon fejlett igényeket is lefed. Teljesítményre és 100%-os ellenállóságra tervezték. Alapból adaptereket talál a leggyakoribb háttértárolókhoz. Lehetővé teszi a tag-ek alapján történő érvénytelenítést, az időbeli lejárást, védelmet nyújt a cache stampede ellen stb. - - -Telepítés -========= - -A könyvtárat a [Composer|best-practices:composer] eszközzel töltheti le és telepítheti: - -```shell -composer require nette/caching -``` - - -Alapvető használat -================== - -A cache-sel vagy gyorsítótárral való munka középpontjában az [api:Nette\Caching\Cache] objektum áll. Létrehozunk egy példányt belőle, és paraméterként átadjuk a konstruktornak az úgynevezett tárolót. Ez egy olyan objektum, amely azt a helyet képviseli, ahol az adatok fizikailag tárolódnak (adatbázis, Memcached, fájlok a lemezen, ...). A tárolóhoz úgy juthatunk hozzá, hogy [dependency injection |dependency-injection:passing-dependencies] segítségével kérjük át a `Nette\Caching\Storage` típussal. Minden lényegeset megtudhat a [Tárolók szakaszban |#Tárolók]. - -.[warning] -A 3.0-s verzióban az interfésznek még volt `I` előtagja, tehát a neve `Nette\Caching\IStorage` volt. Továbbá a `Cache` osztály konstansai nagybetűkkel voltak írva, tehát például `Cache::EXPIRE` a `Cache::Expire` helyett. - -A következő példákhoz feltételezzük, hogy létrehoztunk egy `Cache` aliast, és a `$storage` változóban van a tároló. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instance of Nette\Caching\Storage -``` - -A cache valójában egy *key–value store*, tehát az adatokat kulcsok alatt olvassuk és írjuk, ugyanúgy, mint az asszociatív tömböknél. Az alkalmazások számos független részből állnak, és ha mindegyik ugyanazt a tárolót használná (képzeljünk el egyetlen könyvtárat a lemezen), előbb-utóbb kulcsütközés következne be. A Nette Framework ezt a problémát úgy oldja meg, hogy az egész teret névtérekre (alkönyvtárakra) osztja. Minden programrész ezután a saját, egyedi nevű terét használja, és így már nem fordulhat elő ütközés. - -A névtér nevét a Cache osztály konstruktorának második paramétereként adjuk meg: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Most már a `$cache` objektum segítségével olvashatunk a gyorsítótárból és írhatunk bele. Mindkettőre a `load()` metódus szolgál. Az első argumentum a kulcs, a második pedig egy PHP callback, amely akkor hívódik meg, ha a kulcs nem található a cache-ben. A callback generálja az értéket, visszaadja, és az elmentődik a cache-be: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // költséges számítás - return $computedValue; -}); -``` - -Ha a második paramétert nem adjuk meg `$value = $cache->load($key)`, akkor `null`-t ad vissza, ha az elem nincs a cache-ben. - -.[tip] -Nagyszerű, hogy a cache-be bármilyen szerializálható struktúrát tárolhatunk, nem csak stringeket. És ugyanez igaz még a kulcsokra is. - -Az elemet a gyorsítótárból a `remove()` metódussal töröljük: - -```php -$cache->remove($key); -``` - -Elemet a gyorsítótárba a `$cache->save($key, $value, array $dependencies = [])` metódussal is menthetünk. Azonban a fentebb bemutatott `load()` használata preferált. - - -Memoizáció -========== - -A memoizáció egy függvény vagy metódus hívásának eredményének gyorsítótárazását jelenti, hogy legközelebb újra felhasználhassuk anélkül, hogy újra kiszámítanánk ugyanazt. - -Metódusokat és függvényeket memoizáltan hívhatunk a `call(callable $callback, ...$args)` segítségével: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -A `gethostbyaddr()` függvény így minden `$ip` paraméterre csak egyszer hívódik meg, és legközelebb már a cache-ből adódik vissza az érték. - -Lehetőség van arra is, hogy egy memoizált burkolót hozzunk létre egy metódus vagy függvény köré, amelyet később hívhatunk meg: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // először kiszámítja -$result = $memoizedFactorial(5); // másodszor a cache-ből -``` - - -Lejárat & érvénytelenítés -========================= - -A cache-be való mentéskor felmerül a kérdés, hogy a korábban elmentett adatok mikor válnak érvénytelenné. A Nette Framework egy mechanizmust kínál az adatok érvényességének korlátozására vagy azok irányított törlésére (a keretrendszer terminológiájában „érvénytelenítésére”). - -Az adatok érvényességét a mentéskor állítjuk be a `save()` metódus harmadik paraméterével, pl.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Vagy a `load()` metódus callbackjének referenciaként átadott `$dependencies` paraméterével, pl.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Vagy a `load()` metódus 3. paraméterével, pl.: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -A további példákban a második változatot feltételezzük, és így a `$dependencies` változó létezését. - - -Lejárat -------- - -A legegyszerűbb lejárat az időkorlát. Így 20 perces érvényességgel mentünk adatokat a cache-be: - -```php -// elfogadja a másodpercek számát vagy UNIX timestamp-et is -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Ha minden olvasással meg szeretnénk hosszabbítani az érvényességi időt, azt a következőképpen érhetjük el, de vigyázat, a cache rezsije ezzel megnő: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Ügyes lehetőség, hogy az adatokat akkor járassuk le, amikor egy fájl vagy több fájl közül valamelyik megváltozik. Ezt például akkor használhatjuk, ha ezeknek a fájloknak a feldolgozásából származó adatokat mentjük a cache-be. Használjon abszolút elérési utakat. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// vagy -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Lejárathatunk egy elemet a cache-ben akkor, amikor egy másik elem (vagy több másik közül valamelyik) lejár. Ezt akkor használhatjuk, ha például egy egész HTML oldalt mentünk a cache-be, és más kulcsok alatt annak töredékeit. Amint egy töredék megváltozik, az egész oldal érvénytelenné válik. Ha a töredékeket pl. `frag1` és `frag2` kulcsok alatt tároljuk, használjuk ezt: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -A lejáratot saját függvényekkel vagy statikus metódusokkal is vezérelhetjük, amelyek minden olvasáskor eldöntik, hogy az elem még érvényes-e. Így például lejárathatunk egy elemet mindig, amikor a PHP verziója megváltozik. Létrehozunk egy függvényt, amely összehasonlítja az aktuális verziót a paraméterrel, és a mentéskor hozzáadjuk a függőségek közé a `[függvény neve, ...argumentumok]` formátumú tömböt: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // járjon le, ha checkPhpVersion(...) === false -]; -``` - -Természetesen minden kritérium kombinálható. A cache akkor jár le, ha legalább egy kritérium nem teljesül. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Érvénytelenítés tag-ekkel -------------------------- - -Nagyon hasznos érvénytelenítő eszközök az úgynevezett tag-ek. Minden cache-beli elemhez hozzárendelhetünk egy tag-listát, amelyek tetszőleges stringek. Legyen például egy HTML oldalunk egy cikkel és hozzászólásokkal, amelyet gyorsítótárazni fogunk. Mentéskor megadjuk a tag-eket: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Lépjünk át az adminisztrációba. Itt találunk egy űrlapot a cikk szerkesztéséhez. A cikk adatbázisba mentésével együtt meghívjuk a `clean()` parancsot, amely törli a cache-ből az elemeket a tag alapján: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Ugyanígy az új hozzászólás hozzáadásának (vagy egy hozzászólás szerkesztésének) helyén ne felejtsük el érvényteleníteni a megfelelő tag-et: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Mit értünk el ezzel? Azt, hogy a HTML cache érvénytelenné válik (törlődik), amikor a cikk vagy a hozzászólások megváltoznak. Ha egy 10-es ID-jú cikket szerkesztünk, akkor kényszerített érvénytelenítés történik az `article/10` tag-re, és a HTML oldal, amely ezt a tag-et hordozza, törlődik a cache-ből. Ugyanez történik egy új hozzászólás beszúrásakor a megfelelő cikk alá. - -.[note] -A tag-ekhez úgynevezett [#Journal] szükséges. - - -Érvénytelenítés prioritással ----------------------------- - -Az egyes cache-elemekhez beállíthatunk prioritást, amellyel törölhetjük őket, ha például a cache meghalad egy bizonyos méretet: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Töröljük az összes elemet, amelyek prioritása 100 vagy annál kisebb: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -A prioritásokhoz úgynevezett [#Journal] szükséges. - - -Cache törlése -------------- - -A `Cache::All` paraméter mindent töröl: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Tömeges olvasás -=============== - -A cache-ből való tömeges olvasásra és írásra a `bulkLoad()` metódus szolgál, amelynek átadunk egy kulcstömböt, és egy értéktömböt kapunk vissza: - -```php -$values = $cache->bulkLoad($keys); -``` - -A `bulkLoad()` metódus hasonlóan működik, mint a `load()`, a második paraméter callbackkel is, amelynek átadódik a generált elem kulcsa: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // költséges számítás - return $computedValue; -}); -``` - - -Használat PSR-16-tal .{data-version:3.3.1} -========================================== - -A Nette Cache PSR-16 interfésszel való használatához használhatja a `PsrCacheAdapter` adaptert. Lehetővé teszi a zökkenőmentes integrációt a Nette Cache és bármely olyan kód vagy könyvtár között, amely PSR-16 kompatibilis cache-t vár. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Most már használhatja a `$psrCache`-t PSR-16 cache-ként: - -```php -$psrCache->set('key', 'value', 3600); // 1 órára menti az értéket -$value = $psrCache->get('key', 'default'); -``` - -Az adapter támogatja az összes PSR-16-ban definiált metódust, beleértve a `getMultiple()`, `setMultiple()` és `deleteMultiple()` metódusokat is. - - -Kimenet gyorsítótárazása -======================== - -Nagyon elegánsan lehet a kimenetet elfogni és gyorsítótárazni: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // kiírjuk az adatokat - - $capture->end(); // elmentjük a kimenetet a cache-be -} -``` - -Abban az esetben, ha a kimenet már a cache-ben van, a `capture()` metódus kiírja azt és `null`-t ad vissza, tehát a feltétel nem teljesül. Ellenkező esetben elkezdi a kimenet elfogását és visszaadja a `$capture` objektumot, amelynek segítségével végül elmentjük a kiírt adatokat a cache-be. - -.[note] -A 3.0-s verzióban a metódus neve `$cache->start()` volt. - - -Gyorsítótárazás Latte-ban -========================= - -A sablonokban való gyorsítótárazás a [Latte|latte:]-ban nagyon egyszerű, csak a sablon egy részét kell `{cache}...{/cache}` tagekkel körbevenni. A cache automatikusan érvénytelenné válik, amikor a forrás sablon megváltozik (beleértve az esetlegesen beillesztett sablonokat a cache blokkon belül). A `{cache}` tagek egymásba ágyazhatók, és ha egy beágyazott blokk érvénytelenné válik (például egy tag miatt), akkor a fölérendelt blokk is érvénytelenné válik. - -A tagben megadhatók kulcsok, amelyekhez a cache kötődni fog (itt a `$id` változó), és beállítható a lejárat és a [címkék az érvénytelenítéshez |#Érvénytelenítés tag-ekkel]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Minden elem opcionális, így nem kell megadnunk sem a lejáratot, sem a címkéket, végül még a kulcsokat sem. - -A cache használata feltételhez is köthető az `if` segítségével - a tartalom csak akkor lesz gyorsítótárazva, ha a feltétel teljesül: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Tárolók -======= - -A tároló egy objektum, amely azt a helyet képviseli, ahol az adatok fizikailag tárolódnak. Használhatunk adatbázist, Memcached szervert, vagy a leginkább elérhető tárolót, ami a lemezen lévő fájlok. - -|----------------- -| Tároló | Leírás -|----------------- -| [#FileStorage] | alapértelmezett tároló, amely a lemezen lévő fájlokba ment -| [#MemcachedStorage] | `Memcached` szervert használ -| [#MemoryStorage] | az adatok ideiglenesen a memóriában vannak -| [#SQLiteStorage] | az adatok SQLite adatbázisba mentődnek -| [#DevNullStorage] | az adatok nem mentődnek, tesztelésre alkalmas - -A tároló objektumhoz úgy juthat hozzá, hogy [dependency injection |dependency-injection:passing-dependencies] segítségével kéri át a `Nette\Caching\Storage` típussal. Alapértelmezett tárolóként a Nette a FileStorage objektumot biztosítja, amely az adatokat az [ideiglenes fájlok |application:bootstrapping#Ideiglenes fájlok] könyvtárában lévő `cache` alkönyvtárba menti. - -A tárolót a konfigurációban módosíthatja: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -A cache-t fájlokba írja a lemezen. A `Nette\Caching\Storages\FileStorage` tároló nagyon jól optimalizált a teljesítményre, és mindenekelőtt biztosítja a műveletek teljes atomicitását. Mit jelent ez? Azt, hogy a cache használatakor nem fordulhat elő, hogy olyan fájlt olvassunk be, amelyet egy másik szál még nem írt ki teljesen, vagy hogy valaki "a kezünk alól" törölje azt. A cache használata tehát teljesen biztonságos. - -Ez a tároló egy fontos beépített funkcióval is rendelkezik, amely megakadályozza a CPU extrém kihasználtságának növekedését abban a pillanatban, amikor a cache törlődik vagy még nincs felmelegítve (azaz létrehozva). Ez a "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede elleni védelem. Előfordul, hogy egy időben több párhuzamos kérés érkezik, amelyek ugyanazt a dolgot akarják a cache-ből (pl. egy drága SQL lekérdezés eredményét), és mivel az nincs a gyorsítótárban, minden folyamat ugyanazt az SQL lekérdezést kezdi el végrehajtani. A terhelés így megsokszorozódik, és akár az is előfordulhat, hogy egyetlen szál sem tud válaszolni az időkorláton belül, a cache nem jön létre, és az alkalmazás összeomlik. Szerencsére a Nette cache úgy működik, hogy több párhuzamos kérés esetén egy elemre csak az első szál generálja azt, a többiek várnak, majd felhasználják a generált eredményt. - -Példa a FileStorage létrehozására: - -```php -// a tároló a '/path/to/temp' könyvtár lesz a lemezen -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -A [Memcached|https://memcached.org] szerver egy nagy teljesítményű, elosztott memóriában történő tárolási rendszer, amelynek adaptere a `Nette\Caching\Storages\MemcachedStorage`. A konfigurációban megadjuk az IP-címet és a portot, ha az eltér a standard 11211-től. - -.[caution] -Szükséges a `memcached` PHP kiterjesztés. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -A `Nette\Caching\Storages\MemoryStorage` egy olyan tároló, amely az adatokat egy PHP tömbben tárolja, és így a kérés befejeztével elvesznek. - - -SQLiteStorage -------------- - -Az SQLite adatbázis és az `Nette\Caching\Storages\SQLiteStorage` adapter lehetőséget kínál a cache egyetlen fájlba történő mentésére a lemezen. A konfigurációban megadjuk ennek a fájlnak az elérési útját. - -.[caution] -Szükséges a `pdo` és `pdo_sqlite` PHP kiterjesztés. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -A tároló speciális implementációja a `Nette\Caching\Storages\DevNullStorage`, amely valójában egyáltalán nem tárol adatokat. Így tesztelésre alkalmas, amikor ki akarjuk küszöbölni a cache hatását. - - -Cache használata a kódban -========================= - -A cache kódban való használatakor kétféleképpen járhatunk el. Az első az, hogy [dependency injection |dependency-injection:passing-dependencies] segítségével átkérjük a tárolót, és létrehozunk egy `Cache` objektumot: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -A második lehetőség az, hogy közvetlenül a `Cache` objektumot kérjük át: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -A `Cache` objektumot ezután közvetlenül a konfigurációban hozzuk létre ezzel a módszerrel: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -A Nette a címkéket és prioritásokat az úgynevezett journalba menti. Alapértelmezés szerint ehhez SQLite-ot és a `journal.s3db` fájlt használja, és **szükséges a `pdo` és `pdo_sqlite` PHP kiterjesztés.** - -A journalt a konfigurációban módosíthatja: - -```neon -services: - cache.journal: MyJournal -``` - - -DI szolgáltatások -================= - -Ezek a szolgáltatások kerülnek hozzáadásra a DI konténerhez: - -| Név | Típus | Leírás -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | tároló - - -Cache kikapcsolása -================== - -Az alkalmazásban a cache kikapcsolásának egyik módja, ha a [#DevNullStorage]-t állítjuk be tárolóként: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Ez a beállítás nincs hatással a sablonok gyorsítótárazására a Latte-ban vagy a DI konténerben, mivel ezek a könyvtárak nem használják a nette/caching szolgáltatásait, és önállóan kezelik a cache-t. Egyébként a cache-üket [nem szükséges kikapcsolni |nette:troubleshooting#Hogyan kapcsoljuk ki a cache-t fejlesztés közben] fejlesztői módban. diff --git a/caching/hu/@meta.texy b/caching/hu/@meta.texy deleted file mode 100644 index c00a2158aa..0000000000 --- a/caching/hu/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette dokumentáció}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/pt/@home.texy b/caching/pt/@home.texy deleted file mode 100644 index 82c8cbb29f..0000000000 --- a/caching/pt/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -A Cache acelera sua aplicação armazenando dados obtidos com dificuldade uma vez para uso futuro. Mostraremos: - -- como usar a cache -- como alterar o armazenamento -- como invalidar corretamente a cache - -</div> - -Usar a cache no Nette é muito fácil, mas cobre até mesmo as necessidades mais avançadas. É projetado para desempenho e 100% de resiliência. Basicamente, você encontrará adaptadores para os armazenamentos de backend mais comuns. Permite invalidação baseada em tags, expiração por tempo, tem proteção contra cache stampede, etc. - - -Instalação -========== - -Faça o download e instale a biblioteca usando o [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Uso Básico -========== - -O centro do trabalho com a cache é o objeto [api:Nette\Caching\Cache]. Criamos sua instância e passamos o chamado armazenamento como parâmetro para o construtor. Este é um objeto que representa o local onde os dados serão fisicamente armazenados (banco de dados, Memcached, arquivos em disco, ...). Acessamos o armazenamento pedindo que ele seja passado usando [injeção de dependência |dependency-injection:passing-dependencies] com o tipo `Nette\Caching\Storage`. Tudo o essencial pode ser encontrado na [seção Armazenamentos |#Armazenamentos]. - -.[warning] -Na versão 3.0, a interface ainda tinha o prefixo `I`, então o nome era `Nette\Caching\IStorage`. Além disso, as constantes da classe `Cache` eram escritas em maiúsculas, como `Cache::EXPIRE` em vez de `Cache::Expire`. - -Para os exemplos a seguir, suponha que temos um alias `Cache` criado e o armazenamento na variável `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instance of Nette\Caching\Storage -``` - -A cache é na verdade um *key–value store*, ou seja, lemos e escrevemos dados sob chaves, assim como em arrays associativos. As aplicações consistem em várias partes independentes e, se todas usassem um único armazenamento (imagine um único diretório no disco), mais cedo ou mais tarde ocorreria uma colisão de chaves. O Nette Framework resolve o problema dividindo todo o espaço em namespaces (subdiretórios). Cada parte do programa então usa seu próprio espaço com um nome único e nenhuma colisão pode ocorrer. - -O nome do espaço é especificado como o segundo parâmetro do construtor da classe Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Agora podemos usar o objeto `$cache` para ler e escrever na cache. O método `load()` serve para ambos. O primeiro argumento é a chave e o segundo é um callback PHP, que é chamado quando a chave não é encontrada na cache. O callback gera o valor, retorna-o e ele é armazenado na cache: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // cálculo intensivo - return $computedValue; -}); -``` - -Se o segundo parâmetro não for especificado `$value = $cache->load($key)`, `null` será retornado se o item não estiver na cache. - -.[tip] -O bom é que qualquer estrutura serializável pode ser armazenada na cache, não precisa ser apenas strings. E o mesmo se aplica até mesmo às chaves. - -Removemos um item da cache usando o método `remove()`: - -```php -$cache->remove($key); -``` - -Também é possível salvar um item na cache usando o método `$cache->save($key, $value, array $dependencies = [])`. No entanto, o método preferido é o mencionado acima usando `load()`. - - -Memoização -========== - -Memoização significa armazenar em cache o resultado de uma chamada de função ou método para que você possa usá-lo na próxima vez sem calcular a mesma coisa repetidamente. - -Métodos e funções podem ser chamados com memoização usando `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -A função `gethostbyaddr()` será chamada apenas uma vez para cada parâmetro `$ip` e, na próxima vez, o valor da cache será retornado. - -Também é possível criar um invólucro memoizado em torno de um método ou função que pode ser chamado posteriormente: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // calcula pela primeira vez -$result = $memoizedFactorial(5); // pela segunda vez, da cache -``` - - -Expiração & Invalidação -======================= - -Ao armazenar em cache, é necessário resolver a questão de quando os dados armazenados anteriormente se tornam inválidos. O Nette Framework oferece um mecanismo para limitar a validade dos dados ou excluí-los de forma controlada (na terminologia do framework, "invalidar"). - -A validade dos dados é definida no momento do armazenamento usando o terceiro parâmetro do método `save()`, por exemplo: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Ou usando o parâmetro `$dependencies` passado por referência para o callback do método `load()`, por exemplo: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Ou usando o 3º parâmetro no método `load()`, que define as dependências se o item for gerado: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -Nos exemplos a seguir, assumiremos a segunda variante e, portanto, a existência da variável `$dependencies`. - - -Expiração ---------- - -A expiração mais simples é um limite de tempo. Desta forma, armazenamos dados na cache com validade de 20 minutos: - -```php -// também aceita número de segundos ou timestamp UNIX -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Se quisermos estender o período de validade a cada leitura, isso pode ser alcançado da seguinte forma, mas atenção, a sobrecarga da cache aumentará: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Uma opção útil é deixar os dados expirarem quando um arquivo ou um de vários arquivos for alterado. Isso pode ser usado, por exemplo, ao armazenar na cache dados gerados pelo processamento desses arquivos. Use caminhos absolutos. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// ou -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Podemos deixar um item na cache expirar quando outro item (ou um de vários outros) expirar. Isso pode ser usado quando armazenamos, por exemplo, uma página HTML inteira na cache e seus fragmentos sob outras chaves. Assim que um fragmento muda, a página inteira é invalidada. Se tivermos fragmentos armazenados sob chaves como `frag1` e `frag2`, usamos: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -A expiração também pode ser controlada usando funções personalizadas ou métodos estáticos, que sempre decidem na leitura se o item ainda é válido. Desta forma, por exemplo, podemos deixar um item expirar sempre que a versão do PHP mudar. Criamos uma função que compara a versão atual com um parâmetro e, ao salvar, adicionamos um array no formato `[callable, ...argumentos]` entre as dependências: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // expirar quando checkPhpVersion(...) === false -]; -``` - -Todos os critérios podem, obviamente, ser combinados. A cache então expirará quando pelo menos um critério não for atendido. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Invalidação usando tags ------------------------ - -Uma ferramenta de invalidação muito útil são as chamadas tags. Podemos atribuir uma lista de tags a cada item na cache, que são strings arbitrárias. Por exemplo, tenhamos uma página HTML com um artigo e comentários que iremos armazenar em cache. Ao salvar, especificamos as tags: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Vamos para a administração. Aqui encontramos um formulário para editar o artigo. Juntamente com o salvamento do artigo no banco de dados, chamamos o comando `clean()`, que exclui itens da cache por tag: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Da mesma forma, no local de adição de um novo comentário (ou edição de um comentário), não nos esquecemos de invalidar a tag apropriada: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -O que alcançamos com isso? Que nossa cache HTML será invalidada (excluída) sempre que o artigo ou os comentários forem alterados. Quando um artigo com ID = 123 é editado, a tag `article/123` é invalidada à força e a página HTML que carrega a tag mencionada é excluída da cache. O mesmo acontece ao inserir um novo comentário sob o artigo relevante. - -.[note] -Tags requerem o chamado [#Journal]. - - -Invalidação usando prioridade ------------------------------ - -Podemos definir uma prioridade para itens individuais na cache, que pode ser usada para excluí-los quando, por exemplo, a cache exceder um determinado tamanho: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Excluiremos todos os itens com prioridade igual ou menor que 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Prioridades requerem o chamado [#Journal]. - - -Limpar a cache --------------- - -O parâmetro `Cache::All` exclui tudo: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Leitura em massa -================ - -Para leituras e escritas em massa na cache, usamos o método `bulkLoad()`, ao qual passamos um array de chaves e obtemos um array de valores (chave => valor): - -```php -$values = $cache->bulkLoad($keys); -``` - -O método `bulkLoad()` funciona de forma semelhante a `load()`, também com o segundo parâmetro callback, ao qual é passada a chave do item gerado: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // cálculo intensivo - return $computedValue; -}); -``` - - -Uso com PSR-16 .{data-version:3.3.1} -==================================== - -Para usar a Nette Cache com a interface PSR-16, você pode utilizar o adaptador `PsrCacheAdapter`. Ele permite uma integração perfeita entre a Nette Cache e qualquer código ou biblioteca que espera uma cache compatível com PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Agora você pode usar `$psrCache` como uma cache PSR-16: - -```php -$psrCache->set('key', 'value', 3600); // armazena o valor por 1 hora -$value = $psrCache->get('key', 'default'); -``` - -O adaptador suporta todos os métodos definidos em PSR-16, incluindo `getMultiple()`, `setMultiple()` e `deleteMultiple()`. Note que namespaces e dependências complexas (tags, prioridade, etc.) do Nette Cache não são diretamente expostos pela interface PSR-16. - - -Armazenamento em cache da saída -=============================== - -É muito elegante capturar e armazenar em cache a saída: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // imprimimos os dados - - $capture->end(); // salvamos a saída na cache -} -``` - -Caso a saída já esteja armazenada na cache, o método `capture()` a imprimirá e retornará `null`, portanto a condição não será executada. Caso contrário, ele começará a capturar a saída e retornará o objeto `$capture`, com o qual finalmente salvamos os dados impressos na cache. - -.[note] -Na versão 3.0, o método era chamado `$cache->start()`. - - -Armazenamento em cache no Latte -=============================== - -Armazenar em cache nos templates [Latte|latte:] é muito fácil, basta envolver a parte do template com as tags `{cache}...{/cache}`. A cache é invalidada automaticamente quando o template de origem é alterado (incluindo quaisquer templates incluídos dentro do bloco de cache). As tags `{cache}` podem ser aninhadas e, quando um bloco aninhado é invalidado (por exemplo, por uma tag), o bloco pai também é invalidado. - -Na tag, é possível especificar as chaves às quais a cache estará vinculada (aqui a variável `$id`) e definir a expiração e as [tags para invalidação |#Invalidação usando tags]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Todos os itens são opcionais, portanto não precisamos especificar nem a expiração, nem as tags, e finalmente nem as chaves. - -O uso da cache também pode ser condicionado usando `if` - o conteúdo será então armazenado em cache apenas se a condição for atendida: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Armazenamentos -============== - -Um armazenamento é um objeto que representa o local onde os dados são fisicamente armazenados. Podemos usar um banco de dados, um servidor Memcached ou o armazenamento mais acessível, que são arquivos em disco. - -|--------------------- |------------------------------------------------------- -| Armazenamento | Descrição -|--------------------- |------------------------------------------------------- -| [#FileStorage] | Armazenamento padrão, salva em arquivos no disco. -| [#MemcachedStorage] | Utiliza um servidor [Memcached|https://memcached.org]. -| [#MemoryStorage] | Os dados ficam temporariamente na memória (por requisição). -| [#SQLiteStorage] | Os dados são salvos em um banco de dados SQLite. -| [#DevNullStorage] | Os dados não são salvos, útil para testes. - -Você acessa o objeto de armazenamento padrão pedindo que ele seja passado usando [injeção de dependência |dependency-injection:passing-dependencies] com o tipo `Nette\Caching\Storage`. Como armazenamento padrão, o Nette fornece o objeto `FileStorage` que armazena dados no subdiretório `cache` no diretório para [arquivos temporários |application:bootstrapping#Arquivos temporários]. - -Você pode alterar o armazenamento na configuração: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Grava a cache em arquivos no disco. O armazenamento `Nette\Caching\Storages\FileStorage` é muito bem otimizado para desempenho e, acima de tudo, garante total atomicidade das operações. O que isso significa? Que ao usar a cache, não pode acontecer de lermos um arquivo que ainda não foi completamente escrito por outro processo, ou que alguém o exclua "enquanto estamos usando". O uso da cache é, portanto, completamente seguro. - -Este armazenamento também possui uma função importante integrada que evita um aumento extremo no uso da CPU quando a cache é excluída ou ainda não está aquecida (ou seja, criada). Esta é uma prevenção contra o "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Acontece que, em um determinado momento, um número maior de requisições simultâneas chega, querendo a mesma coisa da cache (por exemplo, o resultado de uma consulta SQL cara) e, como não está na cache, todos os processos começam a executar a mesma consulta SQL. A carga é assim multiplicada e pode até acontecer que nenhum processo consiga responder dentro do limite de tempo, a cache não seja criada e a aplicação entre em colapso. Felizmente, a cache no Nette funciona de forma que, com várias requisições simultâneas para um item, ele é gerado apenas pelo primeiro processo, os outros esperam e então usam o resultado gerado. - -Exemplo de criação manual de FileStorage (geralmente feito via DI): - -```php -// o armazenamento será o diretório '/path/to/temp' no disco -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -O servidor [Memcached|https://memcached.org] é um sistema de armazenamento em memória distribuída de alto desempenho, cujo adaptador é `Nette\Caching\Storages\MemcachedStorage`. Na configuração, especificamos o endereço IP e a porta, se for diferente do padrão 11211. - -.[caution] -Requer a extensão PHP `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` é um armazenamento que guarda dados em um array PHP e, portanto, são perdidos com o término da requisição. - - -SQLiteStorage -------------- - -O banco de dados SQLite e o adaptador `Nette\Caching\Storages\SQLiteStorage` oferecem uma maneira de armazenar a cache em um único arquivo no disco. Na configuração, especificamos o caminho para este arquivo. - -.[caution] -Requer as extensões PHP `pdo` e `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Uma implementação especial de armazenamento é `Nette\Caching\Storages\DevNullStorage`, que na verdade não armazena dados. É, portanto, adequado para testes ou para desativar completamente a cache. - - -Uso da cache no código -====================== - -Ao usar a cache no código, temos duas maneiras de fazer isso. A primeira é pedir que o armazenamento seja passado usando [injeção de dependência |dependency-injection:passing-dependencies] e criar o objeto `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -A segunda opção é pedir que o objeto `Cache` seja passado diretamente: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -O objeto `Cache` é então criado diretamente na configuração desta forma: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette armazena tags e prioridades no chamado journal. Por padrão, o SQLite e o arquivo `journal.s3db` são usados para isso e **são necessárias as extensões PHP `pdo` e `pdo_sqlite`.** - -Você pode alterar o journal na configuração: - -```neon -services: - cache.journal: MyJournal -``` - - -Serviços DI -=========== - -Estes serviços são adicionados ao contêiner DI: - -| Nome | Tipo | Descrição -|-----------------|------------------------------------------|--------------------------------------------------- -| `cache.storage` | `Nette\Caching\Storage` | O serviço de armazenamento de cache padrão (geralmente FileStorage). -| `cache.journal` | `Nette\Caching\Storages\Journal` | O journal padrão (geralmente SQLiteJournal). - - -Desativar a cache -================= - -Uma das opções para desativar a cache na aplicação é definir o armazenamento como [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Esta configuração não afeta o armazenamento em cache de templates no Latte ou no contêiner DI, pois essas bibliotecas não usam os serviços nette/caching e gerenciam sua própria cache de forma independente. Afinal, a cache delas [não precisa ser desativada |nette:troubleshooting#Como desativar o cache durante o desenvolvimento] no modo de desenvolvimento. diff --git a/caching/pt/@meta.texy b/caching/pt/@meta.texy deleted file mode 100644 index e2566bcb44..0000000000 --- a/caching/pt/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentação Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/ro/@home.texy b/caching/ro/@home.texy deleted file mode 100644 index 2bff23a31e..0000000000 --- a/caching/ro/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Cache-ul accelerează aplicația dvs. salvând datele obținute cu efort pentru utilizare ulterioară. Vom arăta: - -- cum să utilizați cache-ul -- cum să schimbați stocarea -- cum să invalidați corect cache-ul - -</div> - -Utilizarea cache-ului în Nette este foarte ușoară, acoperind în același timp și nevoi foarte avansate. Este proiectat pentru performanță și rezistență 100%. În mod implicit, veți găsi adaptoare pentru cele mai comune stocări backend. Permite invalidarea bazată pe tag-uri, expirarea în timp, are protecție împotriva cache stampede etc. - - -Instalare -========= - -Descărcați și instalați biblioteca folosind [Composer |best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Utilizare de bază -================= - -Centrul lucrului cu cache-ul este obiectul [Cache |api:Nette\Caching\Cache]. Creăm o instanță a acestuia și îi transmitem constructorului așa-numita stocare (storage). Acesta este un obiect care reprezintă locul unde datele vor fi stocate fizic (bază de date, Memcached, fișiere pe disc, ...). Ajungem la stocare lăsându-ne să o primim prin [dependency injection |dependency-injection:passing-dependencies] cu tipul `Nette\Caching\Storage`. Veți afla tot ce este esențial în [secțiunea Stocări |#Stocări]. - -.[warning] -În versiunea 3.0, interfața avea încă prefixul `I`, deci numele era `Nette\Caching\IStorage`. De asemenea, constantele clasei `Cache` erau scrise cu majuscule, deci, de exemplu, `Cache::EXPIRE` în loc de `Cache::Expire`. - -Pentru următoarele exemple, presupunem că avem un alias `Cache` creat și stocarea în variabila `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instanță de Nette\Caching\Storage -``` - -Cache-ul este de fapt un *key–value store*, adică citim și scriem date sub chei la fel ca în array-urile asociative. Aplicațiile sunt compuse din mai multe părți independente și dacă toate ar folosi o singură stocare (imaginați-vă un singur director pe disc), mai devreme sau mai târziu ar apărea o coliziune de chei. Nette Framework rezolvă problema împărțind întregul spațiu în spații de nume (subdirectoare). Fiecare parte a programului folosește apoi propriul spațiu cu un nume unic și nu mai poate apărea nicio coliziune. - -Numele spațiului îl specificăm ca al doilea parametru al constructorului clasei Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Acum putem folosi obiectul `$cache` pentru a citi și scrie în cache. Pentru ambele servește metoda `load()`. Primul argument este cheia și al doilea este un callback PHP, care este apelat atunci când cheia nu este găsită în cache. Callback-ul generează valoarea, o returnează și aceasta este salvată în cache: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // calcul costisitor - return $computedValue; -}); -``` - -Dacă nu specificăm al doilea parametru `$value = $cache->load($key)`, se va returna `null` dacă elementul nu este în cache. - -.[tip] -Este grozav că în cache pot fi stocate orice structuri serializabile, nu trebuie să fie doar șiruri de caractere. Și același lucru este valabil chiar și pentru chei. - -Elementul din cache îl ștergem cu metoda `remove()`: - -```php -$cache->remove($key); -``` - -Salvarea unui element în cache se poate face și cu metoda `$cache->save($key, $value, array $dependencies = [])`. Cu toate acestea, metoda preferată este cea menționată mai sus, folosind `load()`, deoarece gestionează atomic generarea și salvarea datelor. - - -Memoizare -========= - -Memoizarea înseamnă stocarea în cache a rezultatului apelării unei funcții sau metode, astfel încât să îl puteți utiliza data viitoare fără a calcula același lucru din nou și din nou. - -Metodele și funcțiile pot fi apelate memoizat folosind `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Funcția `gethostbyaddr()` va fi astfel apelată pentru fiecare parametru `$ip` o singură dată, iar data viitoare se va returna valoarea din cache. - -De asemenea, este posibil să creați un wrapper memoizat peste o metodă sau funcție, care poate fi apelat ulterior: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // calculează prima dată -$result = $memoizedFactorial(5); // a doua oară din cache -``` - - -Expirare & invalidare -===================== - -Cu stocarea în cache, trebuie rezolvată problema când datele stocate anterior devin invalide. Nette Framework oferă un mecanism pentru a limita validitatea datelor sau pentru a le șterge controlat (în terminologia framework-ului „a invalida”). - -Validitatea datelor se setează în momentul salvării, folosind al treilea parametru al metodei `save()`, de exemplu: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Sau folosind parametrul `$dependencies` transmis prin referință la callback-ul metodei `load()`, de exemplu: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Sau folosind al treilea parametru în metoda `load()`, de exemplu: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -În următoarele exemple, vom presupune a doua variantă și, prin urmare, existența variabilei `$dependencies`. - - -Expirare --------- - -Cea mai simplă expirare este limita de timp. Astfel stocăm date în cache cu o valabilitate de 20 de minute: - -```php -// acceptă și numărul de secunde sau timestamp UNIX -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Dacă am dori să prelungim perioada de valabilitate la fiecare citire, se poate realiza astfel, dar atenție, costul cache-ului va crește: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Este utilă posibilitatea de a lăsa datele să expire în momentul în care se modifică un fișier sau unul dintre mai multe fișiere. Acest lucru poate fi utilizat, de exemplu, la stocarea în cache a datelor rezultate din procesarea acestor fișiere. Utilizați căi absolute. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// sau -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Putem lăsa un element din cache să expire în momentul în care expiră un alt element (sau unul dintre mai multe altele). Acest lucru poate fi utilizat atunci când stocăm în cache, de exemplu, o întreagă pagină HTML și sub alte chei fragmentele sale. De îndată ce un fragment se modifică, întreaga pagină este invalidată. Dacă fragmentele sunt stocate sub cheile, de exemplu, `frag1` și `frag2`, folosim: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Expirarea poate fi controlată și prin funcții proprii sau metode statice, care decid întotdeauna la citire dacă elementul este încă valid. Astfel, de exemplu, putem lăsa un element să expire ori de câte ori se schimbă versiunea PHP. Creăm o funcție care compară versiunea curentă cu parametrul și, la salvare, adăugăm între dependențe un array de forma `[nume functie, ...argumente]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // expiră când checkPhpVersion(...) === false -]; -``` - -Toate criteriile pot fi, desigur, combinate. Cache-ul va expira atunci când cel puțin un criteriu nu este îndeplinit. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Invalidare prin tag-uri ------------------------ - -Un instrument de invalidare foarte util sunt așa-numitele tag-uri. Fiecărui element din cache îi putem atribui la salvare o listă de tag-uri, care sunt șiruri de caractere arbitrare. Să avem, de exemplu, o pagină HTML cu un articol și comentarii, pe care o vom stoca în cache. La salvare, specificăm tag-urile: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Să ne mutăm în administrare. Aici găsim un formular pentru editarea articolului. Împreună cu salvarea articolului în baza de date, vom apela comanda `clean()`, care va șterge din cache elementele conform tag-ului: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -La fel, în locul adăugării unui nou comentariu (sau editării unui comentariu), nu vom omite invalidarea tag-ului corespunzător: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Ce am obținut prin asta? Că cache-ul nostru HTML se va invalida (șterge) ori de câte ori se modifică articolul sau comentariile. Când se editează articolul cu ID = 10, se va forța invalidarea tag-ului `article/10` și pagina HTML care poartă tag-ul menționat se va șterge din cache. Același lucru se întâmplă la inserarea unui nou comentariu sub articolul respectiv. - -.[note] -Tag-urile necesită așa-numitul [#Journal]. - - -Invalidare prin prioritate --------------------------- - -Fiecărui element din cache îi putem seta o prioritate, cu ajutorul căreia va fi posibil să le ștergem, de exemplu, când cache-ul depășește o anumită dimensiune: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Ștergem toate elementele cu prioritate egală sau mai mică de 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Prioritățile necesită așa-numitul [#Journal]. - - -Ștergerea cache-ului --------------------- - -Parametrul `Cache::All` șterge tot: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Citire în masă -============== - -Pentru citirea și scrierea în masă în cache servește metoda `bulkLoad()`, căreia îi transmitem un array de chei și obținem un array de valori: - -```php -$values = $cache->bulkLoad($keys); -``` - -Metoda `bulkLoad()` funcționează similar cu `load()`, inclusiv cu al doilea parametru callback, căruia i se transmite cheia elementului generat: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // calcul costisitor - return $computedValue; -}); -``` - - -Utilizare cu PSR-16 .{data-version:3.3.1} -========================================= - -Pentru a utiliza Nette Cache cu interfața PSR-16 (Simple Cache), puteți folosi adaptorul `Nette\Bridges\Psr\PsrCacheAdapter`. Acesta permite integrarea fără probleme între Nette Cache (`Nette\Caching\Storage`) și orice cod sau bibliotecă care așteaptă un cache compatibil PSR-16. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Acum puteți utiliza `$psrCache` ca un cache PSR-16: - -```php -$psrCache->set('key', 'value', 3600); // salvează valoarea pentru 1 oră -$value = $psrCache->get('key', 'default'); -``` - -Adaptorul suportă toate metodele definite în PSR-16, inclusiv `getMultiple()`, `setMultiple()` și `deleteMultiple()`. - - -Stocarea în cache a ieșirii -=========================== - -Se poate captura și stoca în cache ieșirea foarte elegant: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // afișăm date - - $capture->end(); // salvăm ieșirea în cache -} -``` - -În cazul în care ieșirea este deja stocată în cache, metoda `capture()` o va afișa și va returna `null`, deci condiția nu se va executa. În caz contrar, va începe să captureze ieșirea și va returna obiectul `$capture`, cu ajutorul căruia vom salva în final datele afișate în cache. - -.[note] -În versiunea 3.0, metoda se numea `$cache->start()`. - - -Stocarea în cache în Latte -========================== - -Stocarea în cache în șabloanele [Latte |latte:] este foarte ușoară, este suficient să încadrați o parte a șablonului cu tag-urile `{cache}...{/cache}`. Cache-ul se invalidează automat în momentul în care se modifică șablonul sursă (inclusiv eventualele șabloane incluse în interiorul blocului `{cache}`). Tag-urile `{cache}` pot fi imbricate, iar când un bloc imbricat devine invalid (de exemplu, printr-un tag), blocul părinte devine și el invalid. - -În tag se pot specifica chei suplimentare de care va depinde cache-ul (aici variabila `$id`), se poate seta expirarea și [tag-urile pentru invalidare |#Invalidare prin tag-uri]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Toate elementele sunt opționale, deci nu trebuie să specificăm nici expirarea, nici tag-urile, și nici măcar cheile. - -Utilizarea cache-ului poate fi, de asemenea, condiționată folosind `if` - conținutul va fi stocat în cache doar dacă condiția este îndeplinită: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Stocări -======= - -Stocarea este un obiect care reprezintă locul unde datele sunt stocate fizic. Putem folosi o bază de date, un server Memcached sau cea mai accesibilă stocare, care sunt fișierele pe disc. - -|----------------- -| Stocare | Descriere -|----------------- -| [#FileStorage] | stocare implicită cu salvare în fișiere pe disc -| [#MemcachedStorage] | utilizează serverul `Memcached` -| [#MemoryStorage] | datele sunt temporar în memorie -| [#SQLiteStorage] | datele se salvează într-o bază de date SQLite -| [#DevNullStorage] | datele nu se salvează, potrivit pentru testare - -La obiectul de stocare ajungeți lăsându-vă să vi-l transmită prin [dependency injection |dependency-injection:passing-dependencies] cu tipul `Nette\Caching\Storage`. Ca stocare implicită, Nette oferă obiectul `FileStorage` care salvează datele în subdirectorul `cache` din directorul pentru [fișiere temporare |application:bootstrapping#Fișiere temporare]. - -Puteți schimba stocarea implicită în configurația `services.neon`: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Scrie cache-ul în fișiere pe disc. Stocarea `Nette\Caching\Storages\FileStorage` este foarte bine optimizată pentru performanță și, mai presus de toate, asigură atomicitatea completă a operațiunilor. Ce înseamnă asta? Că la utilizarea cache-ului nu se poate întâmpla să citim un fișier care nu a fost încă scris complet de un alt fir de execuție, sau ca cineva să ni-l șteargă „sub nas”. Utilizarea cache-ului este, prin urmare, complet sigură. - -Această stocare are, de asemenea, o funcție importantă încorporată, care previne creșterea extremă a utilizării CPU în momentul în care cache-ul este șters sau nu este încă încălzit (adică creat) - fenomen cunoscut sub numele de "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Se întâmplă ca, la un moment dat, să apară un număr mai mare de cereri concurente care doresc același lucru din cache (de exemplu, rezultatul unei interogări SQL costisitoare) și, deoarece nu se află în cache, toate procesele încep să execute aceeași interogare SQL. Sarcina se multiplică astfel și se poate chiar întâmpla ca niciun fir de execuție să nu reușească să răspundă în limita de timp, cache-ul să nu se creeze și aplicația să se prăbușească. Din fericire, cache-ul din Nette (cu `FileStorage`) funcționează astfel încât, în cazul mai multor cereri concurente pentru un singur element, acesta este generat doar de primul fir de execuție, celelalte așteaptă și apoi utilizează rezultatul generat. - -Exemplu de creare a FileStorage: - -```php -// stocarea va fi directorul '/path/to/temp' pe disc -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Serverul [Memcached |https://memcached.org] este un sistem de înaltă performanță pentru stocarea în memorie distribuită, al cărui adaptor este `Nette\Caching\Storages\MemcachedStorage`. În configurație specificăm adresa IP și portul, dacă diferă de cel standard 11211. - -.[caution] -Necesită extensia PHP `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` este o stocare care salvează datele într-un array PHP și, prin urmare, se pierd la terminarea cererii. - - -SQLiteStorage -------------- - -Baza de date SQLite și adaptorul `Nette\Caching\Storages\SQLiteStorage` oferă o modalitate de a stoca cache-ul într-un singur fișier pe disc. În configurație specificăm calea către acest fișier. - -.[caution] -Necesită extensiile PHP `pdo` și `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -O implementare specială a stocării este `Nette\Caching\Storages\DevNullStorage`, care de fapt nu stochează deloc datele. Este astfel potrivită pentru testare, când dorim să eliminăm influența cache-ului. - - -Utilizarea cache-ului în cod -============================ - -La utilizarea cache-ului în cod, avem două moduri de a proceda. Primul este să ne lăsăm să primim stocarea prin [dependency injection |dependency-injection:passing-dependencies] și să creăm obiectul `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -A doua opțiune este să ne lăsăm să primim direct obiectul `Cache`: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Obiectul `Cache` este apoi creat direct în configurație în acest mod: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette stochează tag-urile și prioritățile în așa-numitul journal. În mod standard, se utilizează SQLite și fișierul `journal.s3db` și **sunt necesare extensiile PHP `pdo` și `pdo_sqlite`.** - -Puteți schimba journal-ul în configurație: - -```neon -services: - cache.journal: MyJournal -``` - - -Servicii DI -=========== - -Aceste servicii sunt adăugate implicit în containerul DI de către extensia `nette/caching`: - -| Nume | Tip | Descriere -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | journal -| `cache.storage` | [api:Nette\Caching\Storage] | stocare - - -Dezactivarea cache-ului -======================= - -Una dintre opțiunile pentru a dezactiva *efectiv* cache-ul gestionat de `nette/caching` în aplicație este să setați ca stocare implicită [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Această setare nu afectează stocarea în cache a șabloanelor în Latte sau a containerului DI, deoarece aceste biblioteci nu utilizează serviciile nette/caching și își gestionează cache-ul independent. Cache-ul lor, de altfel, [nu trebuie dezactivat |nette:troubleshooting#Cum să dezactivați cache-ul în timpul dezvoltării] în modul dezvoltator. diff --git a/caching/ro/@meta.texy b/caching/ro/@meta.texy deleted file mode 100644 index 6554692600..0000000000 --- a/caching/ro/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentație Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/sl/@home.texy b/caching/sl/@home.texy deleted file mode 100644 index f87e7a5f6e..0000000000 --- a/caching/sl/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Predpomnilnik pospeši vašo aplikacijo tako, da enkrat težko pridobljene podatke shrani za naslednjo uporabo. Pokazali bomo: - -- kako uporabljati predpomnilnik -- kako spremeniti shrambo -- kako pravilno invalidirati predpomnilnik - -</div> - -Uporaba predpomnilnika je v Nette zelo enostavna, hkrati pa pokriva tudi zelo napredne potrebe. Zasnovan je za zmogljivost in 100% odpornost. V osnovi najdete adapterje za najpogostejše zaledne shrambe. Omogoča invalidacijo, temelječo na značkah, časovni potek, ima zaščito pred cache stampede itd. - - -Namestitev -========== - -Knjižnico prenesete in namestite z orodjem [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Osnovna uporaba -=============== - -Središče dela s predpomnilnikom predstavlja objekt [api:Nette\Caching\Cache]. Ustvarimo si njegovo instanco in kot parameter konstruktorju posredujemo t.i. shrambo. To je objekt, ki predstavlja mesto, kamor se bodo podatki fizično shranjevali (podatkovna baza, Memcached, datoteke na disku, ...). Do shrambe pridemo tako, da si jo pustimo posredovati s pomočjo [dependency injection |dependency-injection:passing-dependencies] s tipom `Nette\Caching\Storage`. Vse bistveno boste izvedeli v [odseku Shrambe |#Shrambe]. - -.[warning] -V različici 3.0 je imel vmesnik še predpono `I`, zato je bilo ime `Nette\Caching\IStorage`. Poleg tega so bile konstante razreda `Cache` zapisane z velikimi črkami, torej na primer `Cache::EXPIRE` namesto `Cache::Expire`. - -Za naslednje primere predpostavimo, da imamo ustvarjen alias `Cache` in v spremenljivki `$storage` shrambo. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // instance of Nette\Caching\Storage -``` - -Predpomnilnik je pravzaprav *key–value store*, torej podatke beremo in zapisujemo pod ključi enako kot pri asociativnih poljih. Aplikacije so sestavljene iz vrste neodvisnih delov in če bi vsi uporabljali eno shrambo (predstavljajte si en imenik na disku), bi prej ali slej prišlo do kolizije ključev. Nette Framework problem rešuje tako, da celoten prostor deli na imenske prostore (podimenike). Vsak del programa nato uporablja svoj prostor z edinstvenim imenom in do nobene kolizije več ne more priti. - -Ime prostora navedemo kot drugi parameter konstruktorja razreda Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Zdaj lahko s pomočjo objekta `$cache` iz predpomnilnika beremo in vanj zapisujemo. Za oboje služi metoda `load()`. Prvi argument je ključ in drugi PHP povratni klic (callback), ki se pokliče, ko ključ ni najden v predpomnilniku. Povratni klic vrednost generira, vrne in ta se shrani v predpomnilnik: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // zahteven izračun - return $computedValue; -}); -``` - -Če drugega parametra ne navedemo `$value = $cache->load($key)`, se vrne `null`, če elementa v predpomnilniku ni. - -.[tip] -Odlično je, da lahko v predpomnilnik shranjujemo kakršnekoli serializabilne strukture, ni nujno, da so to samo nizi. In enako velja celo za ključe. - -Element iz predpomnilnika izbrišemo z metodo `remove()`: - -```php -$cache->remove($key); -``` - -Shranjevanje elementa v predpomnilnik je mogoče tudi z metodo `$cache->save($key, $value, array $dependencies = [])`. Vendar je prednostni zgoraj navedeni način s pomočjo `load()`. - - -Memoizacija -=========== - -Memoizacija pomeni predpomnjenje rezultata klica funkcije ali metode, da ga lahko uporabite naslednjič brez ponovnega izračunavanja iste stvari. - -Memoizirano lahko kličemo metode in funkcije s pomočjo `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Funkcija `gethostbyaddr()` se tako pokliče za vsak parameter `$ip` samo enkrat in naslednjič se že vrne vrednost iz predpomnilnika. - -Prav tako je mogoče ustvariti memoiziran ovoj nad metodo ali funkcijo, ki ga lahko kličemo kasneje: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // prvič izračuna -$result = $memoizedFactorial(5); // drugič iz predpomnilnika -``` - - -Potek & invalidacija -==================== - -Pri shranjevanju v predpomnilnik je treba rešiti vprašanje, kdaj prej shranjeni podatki postanejo neveljavni. Nette Framework ponuja mehanizem, kako omejiti veljavnost podatkov ali jih nadzorovano brisati (v terminologiji ogrodja "invalidirati"). - -Veljavnost podatkov se nastavi v trenutku shranjevanja in sicer s pomočjo tretjega parametra metode `save()`, npr.: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Ali s pomočjo parametra `$dependencies`, posredovanega z referenco v povratni klic metode `load()`, npr.: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Ali s pomočjo 3. parametra v metodi `load()`, npr: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -V nadaljnjih primerih bomo predpostavljali drugo varianto in torej obstoj spremenljivke `$dependencies`. - - -Potek ------ - -Najenostavnejši potek predstavlja časovna omejitev. Tako shranimo v predpomnilnik podatke z veljavnostjo 20 minut: - -```php -// sprejema tudi število sekund ali UNIX časovni žig -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Če bi želeli podaljšati dobo veljavnosti z vsakim branjem, lahko to dosežemo na naslednji način, vendar pozor, režija predpomnilnika se s tem poveča: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Priročna je možnost, da podatki potečejo v trenutku, ko se spremeni datoteka ali katera od več datotek. To lahko izkoristimo na primer pri shranjevanju podatkov, nastalih z obdelavo teh datotek, v predpomnilnik. Uporabljajte absolutne poti. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// ali -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Element v predpomnilniku lahko pustimo poteči v trenutku, ko poteče drug element (ali kateri od več drugih). To lahko izkoristimo takrat, ko v predpomnilnik shranjujemo na primer celotno HTML stran in pod drugimi ključi njene fragmente. Takoj ko se fragment spremeni, se invalidira celotna stran. Če imamo fragmente shranjene pod ključi npr. `frag1` in `frag2`, uporabimo: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Potek lahko nadzorujemo tudi s pomočjo lastnih funkcij ali statičnih metod, ki vedno ob branju odločijo, ali je element še veljaven. Tako lahko na primer pustimo element poteči vedno, ko se spremeni različica PHP. Ustvarimo funkcijo, ki primerja trenutno različico s parametrom, in pri shranjevanju dodamo med odvisnosti polje v obliki `[ime funkcije, ...argumenti]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // poteče, ko checkPhpVersion(...) === false -]; -``` - -Vsa merila je seveda mogoče kombinirati. Predpomnilnik potem poteče, ko vsaj eno merilo ni izpolnjeno. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Invalidacija s pomočjo značk ----------------------------- - -Zelo uporabno orodje za invalidacijo so t.i. značke. Vsakemu elementu v predpomnilniku lahko ob shranjevanju dodelimo seznam značk, ki so poljubni nizi. Imejmo na primer HTML stran s člankom in komentarji, ki jo bomo predpomnili. Pri shranjevanju specificiramo značke: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Premaknimo se v administracijo. Tu najdemo obrazec za urejanje članka. Skupaj s shranjevanjem članka v podatkovno bazo pokličemo ukaz `clean()`, ki izbriše iz predpomnilnika elemente glede na značko: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Enako tako na mestu dodajanja novega komentarja (ali urejanja komentarja) ne pozabimo invalidirati ustrezne značke: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Kaj smo s tem dosegli? Da se nam bo HTML predpomnilnik invalidiral (brisal), kadarkoli se spremeni članek ali komentarji. Ko se ureja članek z ID = 10, pride do prisilne invalidacije značke `article/10` in HTML stran, ki nosi navedeno značko, se izbriše iz predpomnilnika. Enako se zgodi pri vstavljanju novega komentarja pod ustrezen članek. - -.[note] -Značke zahtevajo t.i. [#Dnevnik Journal]. - - -Invalidacija s pomočjo prioritete ---------------------------------- - -Posameznim elementom v predpomnilniku lahko nastavimo prioriteto, s pomočjo katere jih bo mogoče brisati, ko na primer predpomnilnik preseže določeno velikost: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Izbrišemo vse elemente s prioriteto enako ali manjšo od 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Prioritete zahtevajo t.i. [#Dnevnik Journal]. - - -Brisanje predpomnilnika ------------------------ - -Parameter `Cache::All` izbriše vse: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Množično branje -=============== - -Za množično branje in pisanje v predpomnilnik služi metoda `bulkLoad()`, kateri posredujemo polje ključev in dobimo polje vrednosti: - -```php -$values = $cache->bulkLoad($keys); -``` - -Metoda `bulkLoad()` deluje podobno kot `load()` tudi z drugim parametrom povratnim klicem, kateremu se posreduje ključ generiranega elementa: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // zahteven izračun - return $computedValue; -}); -``` - - -Uporaba s PSR-16 .{data-version:3.3.1} -====================================== - -Za uporabo Nette Cache z vmesnikom PSR-16 lahko uporabite adapter `PsrCacheAdapter`. Omogoča brezšivno integracijo med Nette Cache in katerokoli kodo ali knjižnico, ki pričakuje PSR-16 združljiv predpomnilnik. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Zdaj lahko uporabljate `$psrCache` kot PSR-16 predpomnilnik: - -```php -$psrCache->set('key', 'value', 3600); // shrani vrednost za 1 uro -$value = $psrCache->get('key', 'default'); -``` - -Adapter podpira vse metode, definirane v PSR-16, vključno z `getMultiple()`, `setMultiple()` in `deleteMultiple()`. - - -Predpomnjenje izpisa -==================== - -Zelo elegantno lahko zajamemo in predpomnimo izpis: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // izpisujemo podatke - - $capture->end(); // shranimo izpis v predpomnilnik -} -``` - -V primeru, da je izpis že shranjen v predpomnilniku, ga metoda `capture()` izpiše in vrne `null`, torej se pogoj ne izvede. V nasprotnem primeru začne zajemati izpis in vrne objekt `$capture`, s pomočjo katerega na koncu izpisane podatke shranimo v predpomnilnik. - -.[note] -V različici 3.0 se je metoda imenovala `$cache->start()`. - - -Predpomnjenje v Latte -===================== - -Predpomnjenje v predlogah [Latte|latte:] je zelo enostavno, dovolj je, da del predloge ovijemo z značkami `{cache}...{/cache}`. Predpomnilnik se samodejno invalidira v trenutku, ko se spremeni izvorna predloga (vključno z morebitnimi vključenimi predlogami znotraj bloka cache). Značke `{cache}` lahko gnezdijo ena v drugo in ko se vgnezden blok razveljavi (na primer z značko), se razveljavi tudi nadrejeni blok. - -V znački je mogoče navesti ključe, na katere bo predpomnilnik vezan (tu spremenljivka `$id`) in nastaviti potek ter [značke za razveljavitev |#Invalidacija s pomočjo značk] - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Vsi elementi so neobvezni, zato nam ni treba navajati niti poteka, niti značk, na koncu niti ključev. - -Uporabo predpomnilnika lahko tudi pogojimo s pomočjo `if` - vsebina se bo potem predpomnila samo, če bo pogoj izpolnjen: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Shrambe -======= - -Shramba je objekt, ki predstavlja mesto, kamor se podatki fizično shranjujejo. Lahko uporabimo podatkovno bazo, strežnik Memcached ali najdostopnejšo shrambo, kar so datoteke na disku. - -|----------------- -| Shramba | Opis -|----------------- -| [#FileStorage] | privzeta shramba s shranjevanjem v datoteke na disk -| [#MemcachedStorage] | uporablja `Memcached` strežnik -| [#MemoryStorage] | podatki so začasno v pomnilniku -| [#SQLiteStorage] | podatki se shranjujejo v SQLite podatkovno bazo -| [#DevNullStorage] | podatki se ne shranjujejo, primerno za testiranje - -Do objekta shrambe pridete tako, da si ga pustite posredovati s pomočjo [dependency injection |dependency-injection:passing-dependencies] s tipom `Nette\Caching\Storage`. Kot privzeto shrambo Nette ponuja objekt FileStorage, ki shranjuje podatke v podimenik `cache` v imeniku za [začasne datoteke |application:bootstrapping#Začasne datoteke]. - -Shrambo lahko spremenite v konfiguraciji: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Zapisuje predpomnilnik v datoteke na disku. Shramba `Nette\Caching\Storages\FileStorage` je zelo dobro optimizirana za zmogljivost in predvsem zagotavlja polno atomičnost operacij. Kaj to pomeni? Da se pri uporabi predpomnilnika ne more zgoditi, da bi prebrali datoteko, ki še ni bila popolnoma zapisana s strani druge niti, ali da bi vam jo kdo "pod roko" izbrisal. Uporaba predpomnilnika je torej popolnoma varna. - -Ta shramba ima tudi vgrajeno pomembno funkcijo, ki preprečuje ekstremno povečanje uporabe CPU v trenutku, ko se predpomnilnik izbriše ali še ni ogret (tj. ustvarjen). Gre za preprečevanje "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Zgodi se, da se v enem trenutku zbere večje število sočasnih zahtev, ki želijo iz predpomnilnika isto stvar (npr. rezultat drage SQL poizvedbe) in ker v predpomnilniku ni, začnejo vsi procesi izvajati isto SQL poizvedbo. Obremenitev se tako množi in lahko se celo zgodi, da nobena nit ne uspe odgovoriti v časovni omejitvi, predpomnilnik se ne ustvari in aplikacija propade. Na srečo predpomnilnik v Nette deluje tako, da pri več sočasnih zahtevah za en element ga generira samo prva nit, ostale čakajo in nato uporabijo generirani rezultat. - -Primer ustvarjanja FileStorage: - -```php -// shramba bo imenik '/path/to/temp' na disku -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Strežnik [Memcached|https://memcached.org] je visoko zmogljiv sistem shranjevanja v porazdeljenem pomnilniku, katerega adapter je `Nette\Caching\Storages\MemcachedStorage`. V konfiguraciji navedemo IP naslov in vrata, če se razlikujejo od standardnih 11211. - -.[caution] -Zahteva PHP razširitev `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` je shramba, ki podatke shranjuje v PHP polje, in se torej z zaključkom zahteve izgubijo. - - -SQLiteStorage -------------- - -Podatkovna baza SQLite in adapter `Nette\Caching\Storages\SQLiteStorage` ponujata način, kako shranjevati predpomnilnik v eno datoteko na disku. V konfiguraciji navedemo pot do te datoteke. - -.[caution] -Zahteva PHP razširitvi `pdo` in `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Posebna implementacija shrambe je `Nette\Caching\Storages\DevNullStorage`, ki dejansko podatkov sploh ne shranjuje. Je tako primerna za testiranje, ko želimo eliminirati vpliv predpomnilnika. - - -Uporaba predpomnilnika v kodi -============================= - -Pri uporabi predpomnilnika v kodi imamo dva načina, kako to storiti. Prvi je ta, da si pustimo posredovati s pomočjo [dependency injection |dependency-injection:passing-dependencies] shrambo in ustvarimo objekt `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Druga možnost je, da si pustimo neposredno posredovati objekt `Cache`: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Objekt `Cache` se potem ustvari neposredno v konfiguraciji na ta način: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Dnevnik (Journal) -================= - -Nette si značke in prioritete shranjuje v t.i. dnevnik (journal). Standardno se za to uporablja SQLite in datoteka `journal.s3db` ter **zahtevata se PHP razširitvi `pdo` in `pdo_sqlite`.** - -Dnevnik lahko spremenite v konfiguraciji: - -```neon -services: - cache.journal: MyJournal -``` - - -Storitve DI -=========== - -Te storitve se dodajo v DI vsebnik: - -| Ime | Tip | Opis -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | dnevnik -| `cache.storage` | [api:Nette\Caching\Storage] | shramba - - -Izklop predpomnilnika -===================== - -Ena od možnosti, kako izklopiti predpomnilnik v aplikaciji, je nastaviti kot shrambo [#DevNullStorage]: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Ta nastavitev nima vpliva na predpomnjenje predlog v Latte ali DI vsebnika, ker te knjižnice ne uporabljajo storitev nette/caching in si upravljajo predpomnilnik samostojno. Njihovega predpomnilnika sicer [ni treba |nette:troubleshooting#Kako izklopiti predpomnilnik med razvojem] v razvojnem načinu izklapljati. diff --git a/caching/sl/@meta.texy b/caching/sl/@meta.texy deleted file mode 100644 index 282883a3d6..0000000000 --- a/caching/sl/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Dokumentacija}} -{{leftbar: nette:@menu-topics}} diff --git a/caching/uk/@home.texy b/caching/uk/@home.texy deleted file mode 100644 index 14e19a212f..0000000000 --- a/caching/uk/@home.texy +++ /dev/null @@ -1,484 +0,0 @@ -Nette Caching -************* - -<div class=perex> - -Кеш прискорить ваш застосунок, зберігаючи дані, отримані з великими витратами, для майбутнього використання. Ми покажемо: - -- як використовувати кеш -- як змінити сховище -- як правильно інвалідувати кеш - -</div> - -Використання кешу в Nette дуже просте, водночас воно покриває навіть дуже складні потреби. Він розроблений для продуктивності та 100% стійкості. В основі ви знайдете адаптери для найпоширеніших бекенд-сховищ. Дозволяє інвалідацію на основі тегів, часову експірацію, має захист від cache stampede тощо. - - -Встановлення -============ - -Бібліотеку можна завантажити та встановити за допомогою інструменту [Composer|best-practices:composer]: - -```shell -composer require nette/caching -``` - - -Базове використання -=================== - -Центром роботи з кешем є об'єкт [api:Nette\Caching\Cache]. Створимо його екземпляр і передамо конструктору так зване сховище. Це об'єкт, що представляє місце, де дані будуть фізично зберігатися (база даних, Memcached, файли на диску, ...). До сховища можна отримати доступ, попросивши передати його за допомогою [dependency injection |dependency-injection:passing-dependencies] з типом `Nette\Caching\Storage`. Все важливе ви дізнаєтеся в [розділі Сховища |#Сховища]. - -.[warning] -У версії 3.0 інтерфейс ще мав префікс `I`, тому назва була `Nette\Caching\IStorage`. Також константи класу `Cache` були написані великими літерами, наприклад, `Cache::EXPIRE` замість `Cache::Expire`. - -Для наступних прикладів припустимо, що ми створили псевдонім `Cache` і маємо сховище у змінній `$storage`. - -```php -use Nette\Caching\Cache; - -$storage = /* ... */; // екземпляр Nette\Caching\Storage -``` - -Кеш — це, по суті, *key–value store*, тобто ми читаємо та записуємо дані за ключами так само, як у асоціативних масивах. Застосунки складаються з низки незалежних частин, і якщо всі вони будуть використовувати одне сховище (уявіть собі один каталог на диску), рано чи пізно виникне колізія ключів. Nette Framework вирішує цю проблему, розділяючи весь простір на простори імен (підкаталоги). Кожна частина програми використовує свій простір з унікальною назвою, і колізій більше не виникає. - -Назву простору вказуємо як другий параметр конструктора класу Cache: - -```php -$cache = new Cache($storage, 'Full Html Pages'); -``` - -Тепер за допомогою об'єкта `$cache` ми можемо читати з кешу та записувати в нього. Для обох дій служить метод `load()`. Першим аргументом є ключ, а другим — PHP callback, який викликається, якщо ключ не знайдено в кеші. Callback генерує значення, повертає його, і воно зберігається в кеші: - -```php -$value = $cache->load($key, function () use ($key) { - $computedValue = /* ... */; // складне обчислення - return $computedValue; -}); -``` - -Якщо другий параметр не вказано `$value = $cache->load($key)`, повернеться `null`, якщо елемент відсутній у кеші. - -.[tip] -Чудово те, що в кеш можна зберігати будь-які серіалізовані структури, не обов'язково лише рядки. Те саме стосується навіть ключів. - -Елемент з кешу видаляємо методом `remove()`: - -```php -$cache->remove($key); -``` - -Зберегти елемент у кеші можна також методом `$cache->save($key, $value, array $dependencies = [])`. Однак перевага надається вищезгаданому способу за допомогою `load()`. - - -Мемоізація -========== - -Мемоізація означає кешування результату виклику функції або методу, щоб ви могли використовувати його наступного разу без повторного обчислення того самого. - -Мемоізовано можна викликати методи та функції за допомогою `call(callable $callback, ...$args)`: - -```php -$result = $cache->call('gethostbyaddr', $ip); -``` - -Функція `gethostbyaddr()` таким чином викликається для кожного параметра `$ip` лише один раз, а наступного разу повертається значення з кешу. - -Також можна створити мемоізовану обгортку над методом або функцією, яку можна викликати пізніше: - -```php -function factorial($num) -{ - return /* ... */; -} - -$memoizedFactorial = $cache->wrap('factorial'); - -$result = $memoizedFactorial(5); // обчислює вперше -$result = $memoizedFactorial(5); // вдруге з кешу -``` - - -Експірація та інвалідація -========================= - -При зберіганні даних у кеші необхідно вирішувати питання, коли раніше збережені дані стануть недійсними. Nette Framework пропонує механізм для обмеження терміну дії даних або їх керованого видалення (в термінології фреймворку — «інвалідації»). - -Термін дії даних встановлюється в момент збереження за допомогою третього параметра методу `save()`, наприклад: - -```php -$cache->save($key, $value, [ - $cache::Expire => '20 minutes', -]); -``` - -Або за допомогою параметра `$dependencies`, переданого за посиланням до callback-функції методу `load()`, наприклад: - -```php -$value = $cache->load($key, function (&$dependencies) { - $dependencies[Cache::Expire] = '20 minutes'; - return /* ... */; -}); -``` - -Або за допомогою 3-го параметра в методі `load()`, наприклад: - -```php -$value = $cache->load($key, function () { - return ...; -}, [Cache::Expire => '20 minutes']); -``` - -У наступних прикладах ми будемо припускати другий варіант і, отже, існування змінної `$dependencies`. - - -Експірація ----------- - -Найпростіша експірація — це часовий ліміт. Таким чином ми зберігаємо дані в кеші з терміном дії 20 хвилин: - -```php -// приймає також кількість секунд або UNIX timestamp -$dependencies[Cache::Expire] = '20 minutes'; -``` - -Якщо ми хочемо продовжити термін дії при кожному читанні, це можна зробити наступним чином, але будьте обережні, накладні витрати кешу при цьому зростуть: - -```php -$dependencies[Cache::Sliding] = true; -``` - -Зручною є можливість дозволити даним закінчитися в момент зміни файлу або одного з кількох файлів. Це можна використовувати, наприклад, при зберіганні в кеші даних, отриманих в результаті обробки цих файлів. Використовуйте абсолютні шляхи. - -```php -$dependencies[Cache::Files] = '/path/to/data.yaml'; -// або -$dependencies[Cache::Files] = ['/path/to/data1.yaml', '/path/to/data2.yaml']; -``` - -Ми можемо дозволити елементу в кеші закінчитися в момент, коли закінчується інший елемент (або один з кількох інших). Це можна використовувати, наприклад, коли ми зберігаємо в кеші цілу HTML-сторінку, а під іншими ключами — її фрагменти. Як тільки фрагмент змінюється, вся сторінка інвалідується. Якщо фрагменти збережені під ключами, наприклад, `frag1` та `frag2`, використовуємо: - -```php -$dependencies[Cache::Items] = ['frag1', 'frag2']; -``` - -Експірацію можна контролювати також за допомогою власних функцій або статичних методів, які завжди при читанні вирішують, чи є елемент ще дійсним. Таким чином, наприклад, ми можемо дозволити елементу закінчитися щоразу, коли змінюється версія PHP. Створимо функцію, яка порівнює поточну версію з параметром, і при збереженні додамо серед залежностей масив у форматі `[назва функції, ...аргументи]`: - -```php -function checkPhpVersion($ver): bool -{ - return $ver === PHP_VERSION_ID; -} - -$dependencies[Cache::Callbacks] = [ - ['checkPhpVersion', PHP_VERSION_ID] // закінчити, коли checkPhpVersion(...) === false -]; -``` - -Всі критерії, звичайно, можна комбінувати. Кеш тоді закінчується, коли принаймні один критерій не виконується. - -```php -$dependencies[Cache::Expire] = '20 minutes'; -$dependencies[Cache::Files] = '/path/to/data.yaml'; -``` - - -Інвалідація за допомогою тегів ------------------------------- - -Дуже корисним інструментом інвалідації є так звані теги. Кожному елементу в кеші ми можемо призначити список тегів, які є довільними рядками. Наприклад, маємо HTML-сторінку зі статтею та коментарями, яку будемо кешувати. При збереженні вказуємо теги: - -```php -$dependencies[Cache::Tags] = ["article/$articleId", "comments/$articleId"]; -``` - -Перейдемо до адміністративної частини. Тут знайдемо форму для редагування статті. Разом зі збереженням статті в базу даних викличемо команду `clean()`, яка видалить з кешу елементи за тегом: - -```php -$cache->clean([ - $cache::Tags => ["article/$articleId"], -]); -``` - -Так само в місці додавання нового коментаря (або редагування коментаря) не забудемо інвалідувати відповідний тег: - -```php -$cache->clean([ - $cache::Tags => ["comments/$articleId"], -]); -``` - -Чого ми цим досягли? Що наш HTML-кеш буде інвалідуватися (видалятися), коли змінюється стаття або коментарі. При редагуванні статті з ID = 10 відбувається примусова інвалідація тегу `article/10`, і HTML-сторінка, яка несе цей тег, видаляється з кешу. Те саме відбувається при додаванні нового коментаря до відповідної статті. - -.[note] -Теги вимагають так званого [#Journal]. - - -Інвалідація за допомогою пріоритету ------------------------------------ - -Окремим елементам у кеші ми можемо встановити пріоритет, за допомогою якого їх можна буде видаляти, наприклад, коли кеш перевищить певний розмір: - -```php -$dependencies[Cache::Priority] = 50; -``` - -Видалимо всі елементи з пріоритетом, рівним або меншим за 100: - -```php -$cache->clean([ - $cache::Priority => 100, -]); -``` - -.[note] -Пріоритети вимагають так званого [#Journal]. - - -Видалення кешу --------------- - -Параметр `Cache::All` видаляє все: - -```php -$cache->clean([ - $cache::All => true, -]); -``` - - -Масове читання -============== - -Для масового читання та запису в кеш служить метод `bulkLoad()`, якому ми передаємо масив ключів і отримуємо масив значень: - -```php -$values = $cache->bulkLoad($keys); -``` - -Метод `bulkLoad()` працює подібно до `load()` і з другим параметром callback, якому передається ключ генерованого елемента: - -```php -$values = $cache->bulkLoad($keys, function ($key, &$dependencies) { - $computedValue = /* ... */; // складне обчислення - return $computedValue; -}); -``` - - -Використання з PSR-16 .{data-version:3.3.1} -=========================================== - -Для використання Nette Cache з інтерфейсом PSR-16 ви можете скористатися адаптером `PsrCacheAdapter`. Він дозволяє безшовну інтеграцію між Nette Cache та будь-яким кодом або бібліотекою, яка очікує PSR-16 сумісний кеш. - -```php -$psrCache = new Nette\Bridges\Psr\PsrCacheAdapter($storage); -``` - -Тепер ви можете використовувати `$psrCache` як PSR-16 кеш: - -```php -$psrCache->set('key', 'value', 3600); // зберігає значення на 1 годину -$value = $psrCache->get('key', 'default'); -``` - -Адаптер підтримує всі методи, визначені в PSR-16, включаючи `getMultiple()`, `setMultiple()` та `deleteMultiple()`. - - -Кешування виводу -================ - -Дуже елегантно можна перехоплювати та кешувати вивід: - -```php -if ($capture = $cache->capture($key)) { - - echo ... // виводимо дані - - $capture->end(); // зберігаємо вивід у кеш -} -``` - -У випадку, якщо вивід вже збережено в кеші, метод `capture()` виведе його і поверне `null`, отже умова не виконається. В іншому випадку він почне перехоплювати вивід і поверне об'єкт `$capture`, за допомогою якого ми врешті-решт збережемо виведені дані в кеш. - -.[note] -У версії 3.0 метод називався `$cache->start()`. - - -Кешування в Latte -================= - -Кешування в шаблонах [Latte|latte:] дуже просте, достатньо частину шаблону обернути тегами `{cache}...{/cache}`. Кеш автоматично інвалідується в момент, коли змінюється вихідний шаблон (включаючи можливі включені шаблони всередині блоку кешу). Теги `{cache}` можна вкладати один в одного, і коли вкладений блок стає недійсним (наприклад, за допомогою тегу), батьківський блок також стає недійсним. - -У тегу можна вказати ключі, до яких буде прив'язаний кеш (тут змінна `$id`), і встановити термін дії та [теги для інвалідації |#Інвалідація за допомогою тегів]. - -```latte -{cache $id, expire: '20 minutes', tags: [tag1, tag2]} - ... -{/cache} -``` - -Усі параметри є необов'язковими, тому ми не повинні вказувати ні термін дії, ні теги, ні навіть ключі. - -Використання кешу також можна обумовити за допомогою `if` - вміст тоді буде кешуватися лише за умови виконання умови: - -```latte -{cache $id, if: !$form->isSubmitted()} - {$form} -{/cache} -``` - - -Сховища -======= - -Сховище — це об'єкт, що представляє місце, де дані фізично зберігаються. Ми можемо використовувати базу даних, сервер Memcached або найдоступніше сховище — файли на диску. - -|----------------- -| Сховище | Опис -|----------------- -| [#FileStorage] | сховище за замовчуванням зі збереженням у файли на диску -| [#MemcachedStorage] | використовує сервер `Memcached` -| [#MemoryStorage] | дані тимчасово зберігаються в пам'яті -| [#SQLiteStorage] | дані зберігаються в базі даних SQLite -| [#DevNullStorage] | дані не зберігаються, підходить для тестування - -До об'єкта сховища можна отримати доступ, попросивши передати його за допомогою [dependency injection |dependency-injection:passing-dependencies] з типом `Nette\Caching\Storage`. Як сховище за замовчуванням Nette надає об'єкт FileStorage, що зберігає дані в підкаталозі `cache` в каталозі для [тимчасових файлів |application:bootstrapping#Тимчасові файли]. - -Змінити сховище можна в конфігурації: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - - -FileStorage ------------ - -Записує кеш у файли на диску. Сховище `Nette\Caching\Storages\FileStorage` дуже добре оптимізоване для продуктивності і, перш за все, забезпечує повну атомарність операцій. Що це означає? Що при використанні кешу не може статися так, що ми прочитаємо файл, який ще не повністю записаний іншим потоком, або що хтось його "під руками" видалить. Використання кешу, таким чином, є абсолютно безпечним. - -Це сховище також має вбудовану важливу функцію, яка запобігає екстремальному зростанню використання ЦП у момент, коли кеш видаляється або ще не прогрітий (тобто створений). Це запобігання "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Стається так, що в один момент збігається велика кількість одночасних запитів, які хочуть отримати з кешу одну й ту саму річ (наприклад, результат дорогого SQL-запиту), і оскільки в кеші її немає, всі процеси починають виконувати той самий SQL-запит. Навантаження таким чином множиться, і може навіть статися, що жоден потік не встигне відповісти в часовому ліміті, кеш не створиться, і застосунок звалиться. На щастя, кеш у Nette працює так, що при кількох одночасних запитах на один елемент його генерує лише перший потік, інші чекають і потім використовують згенерований результат. - -Приклад створення FileStorage: - -```php -// сховищем буде каталог '/path/to/temp' на диску -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp'); -``` - - -MemcachedStorage ----------------- - -Сервер [Memcached|https://memcached.org] — це високопродуктивна система зберігання в розподіленій пам'яті, адаптером якої є `Nette\Caching\Storages\MemcachedStorage`. У конфігурації вказуємо IP-адресу та порт, якщо він відрізняється від стандартного 11211. - -.[caution] -Вимагає PHP-розширення `memcached`. - -```neon -services: - cache.storage: Nette\Caching\Storages\MemcachedStorage('10.0.0.5') -``` - - -MemoryStorage -------------- - -`Nette\Caching\Storages\MemoryStorage` — це сховище, яке зберігає дані в масиві PHP, і тому вони втрачаються після завершення запиту. - - -SQLiteStorage -------------- - -База даних SQLite та адаптер `Nette\Caching\Storages\SQLiteStorage` пропонують спосіб зберігання кешу в одному файлі на диску. У конфігурації вказуємо шлях до цього файлу. - -.[caution] -Вимагає PHP-розширень `pdo` та `pdo_sqlite`. - -```neon -services: - cache.storage: Nette\Caching\Storages\SQLiteStorage('%tempDir%/cache.db') -``` - - -DevNullStorage --------------- - -Спеціальною реалізацією сховища є `Nette\Caching\Storages\DevNullStorage`, яке насправді взагалі не зберігає дані. Тому воно підходить для тестування, коли ми хочемо усунути вплив кешу. - - -Використання кешу в коді -======================== - -При використанні кешу в коді є два способи це зробити. Перший полягає в тому, що ми просимо передати сховище за допомогою [dependency injection |dependency-injection:passing-dependencies] і створюємо об'єкт `Cache`: - -```php -use Nette; - -class ClassOne -{ - private Nette\Caching\Cache $cache; - - public function __construct(Nette\Caching\Storage $storage) - { - $this->cache = new Nette\Caching\Cache($storage, 'my-namespace'); - } -} -``` - -Другий варіант — ми просимо передати об'єкт `Cache` безпосередньо: - -```php -class ClassTwo -{ - public function __construct( - private Nette\Caching\Cache $cache, - ) { - } -} -``` - -Об'єкт `Cache` потім створюється безпосередньо в конфігурації таким чином: - -```neon -services: - - ClassTwo( Nette\Caching\Cache(namespace: 'my-namespace') ) -``` - - -Journal -======= - -Nette зберігає теги та пріоритети у так званому журналі. Стандартно для цього використовується SQLite та файл `journal.s3db`, і **вимагаються PHP-розширення `pdo` та `pdo_sqlite`.** - -Змінити журнал можна в конфігурації: - -```neon -services: - cache.journal: MyJournal -``` - - -Сервіси DI -========== - -Ці сервіси додаються до DI-контейнера: - -| Назва | Тип | Опис -|---------------------------------------------------------- -| `cache.journal` | [api:Nette\Caching\Storages\Journal] | журнал -| `cache.storage` | [api:Nette\Caching\Storage] | сховище - - -Вимкнення кешу -============== - -Одним із способів вимкнути кеш у застосунку є встановлення [#DevNullStorage] як сховища: - -```neon -services: - cache.storage: Nette\Caching\Storages\DevNullStorage -``` - -Це налаштування не впливає на кешування шаблонів у Latte або DI-контейнера, оскільки ці бібліотеки не використовують сервіси nette/caching і керують своїм кешем самостійно. Їхній кеш, до речі, [не потрібно |nette:troubleshooting#Як вимкнути кеш під час розробки] вимикати в режимі розробки. diff --git a/caching/uk/@meta.texy b/caching/uk/@meta.texy deleted file mode 100644 index 083a8ab9f7..0000000000 --- a/caching/uk/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документація Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/code-checker/bg/@home.texy b/code-checker/bg/@home.texy deleted file mode 100644 index ab284bfe7e..0000000000 --- a/code-checker/bg/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Инструментът [Code Checker |https://github.com/nette/code-checker] проверява и евентуално коригира някои от формалните грешки във вашия изходен код. - - -Инсталация -========== - -Code Checker не трябва да се добавя към зависимостите, а да се инсталира като проект. - -```shell -composer create-project nette/code-checker -``` - -Или го инсталирайте глобално с помощта на: - -```shell -composer global require nette/code-checker -``` - -и се уверете, че вашата глобална директория `vendor/bin` е в [променливата на средата $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Употреба -======== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> 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 -``` - -Без параметри проверява текущата директория в режим само за четене, с параметъра `-f` коригира файловете. - -Преди да се запознаете с него, определено първо архивирайте файловете си. - -За по-лесно стартиране можем да създадем файл `code.bat`: - -```shell -php path_to_Nette_tools\Code-Checker\code-checker %* -``` - - -Какво прави всичко това? -======================== - -- премахва [BOM |nette:glossary#BOM] -- проверява валидността на [Latte |latte:] шаблони -- проверява валидността на файлове `.neon`, `.php` и `.json` -- проверява за наличието на [контролни знаци |nette:glossary#Контролни знаци] -- проверява дали файлът е кодиран в UTF-8 -- проверява неправилно записани `/* @anotace */` (липсва звездичка) -- премахва завършващия `?>` при PHP файлове -- премахва десните интервали и излишните редове в края на файла -- нормализира разделителите на редове до системните (ако посочите опцията `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/el/@home.texy b/code-checker/el/@home.texy deleted file mode 100644 index de1f6401a0..0000000000 --- a/code-checker/el/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Το εργαλείο [Code Checker |https://github.com/nette/code-checker] ελέγχει και ενδεχομένως διορθώνει ορισμένα από τα τυπικά σφάλματα στους πηγαίους κώδικές σας. - - -Εγκατάσταση -=========== - -Δεν πρέπει να προσθέσετε το Code Checker στις εξαρτήσεις, αλλά να το εγκαταστήσετε ως έργο. - -```shell -composer create-project nette/code-checker -``` - -Ή εγκαταστήστε το καθολικά χρησιμοποιώντας: - -```shell -composer global require nette/code-checker -``` - -και βεβαιωθείτε ότι ο καθολικός σας κατάλογος `vendor/bin` βρίσκεται στη [μεταβλητή περιβάλλοντος $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Χρήση -===== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> 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 -``` - -Χωρίς παραμέτρους ελέγχει τον τρέχοντα κατάλογο σε κατάσταση μόνο ανάγνωσης, με την παράμετρο `-f` διορθώνει τα αρχεία. - -Πριν εξοικειωθείτε μαζί του, φροντίστε να δημιουργήσετε αντίγραφα ασφαλείας των αρχείων σας πρώτα. - -Για ευκολότερη εκτέλεση, μπορούμε να δημιουργήσουμε ένα αρχείο `code.bat`: - -```shell -php path_to_Nette_tools\Code-Checker\code-checker %* -``` - - -Τι κάνει; -========= - -- αφαιρεί το [BOM |nette:glossary#BOM] -- ελέγχει την εγκυρότητα των templates [Latte |latte:] -- ελέγχει την εγκυρότητα των αρχείων `.neon`, `.php` και `.json` -- ελέγχει την παρουσία [χαρακτήρων ελέγχου |nette:glossary#Control characters] -- ελέγχει εάν το αρχείο είναι κωδικοποιημένο σε UTF-8 -- ελέγχει για λανθασμένα γραμμένα `/* @anotace */` (λείπει ο αστερίσκος) -- αφαιρεί το τελικό `?>` από τα αρχεία PHP -- αφαιρεί τα δεξιά κενά και τις περιττές γραμμές στο τέλος του αρχείου -- κανονικοποιεί τους διαχωριστές γραμμών σε συστήματος (εάν δώσετε την επιλογή `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/hu/@home.texy b/code-checker/hu/@home.texy deleted file mode 100644 index e676665b2f..0000000000 --- a/code-checker/hu/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -A [Code Checker |https://github.com/nette/code-checker] eszköz ellenőrzi és szükség esetén kijavítja a forráskódjaiban található néhány formai hibát. - - -Telepítés -========= - -A Code Checkert nem szabad a függőségekhez hozzáadni, hanem projektként kell telepíteni. - -```shell -composer create-project nette/code-checker -``` - -Vagy telepítse globálisan a következővel: - -```shell -composer global require nette/code-checker -``` - -és győződjön meg róla, hogy a globális `vendor/bin` könyvtára benne van a [$PATH környezeti változóban |https://getcomposer.org/doc/03-cli.md#global]. - - -Használat -========= - -``` -Usage: php code-checker [options] - -Options: - -d <path> Szkennelendő mappa vagy fájl (alapértelmezett: aktuális könyvtár) - -i | --ignore <mask> Figyelmen kívül hagyandó fájlok - -f | --fix Javítja a fájlokat - -l | --eol Újsor karakterek konvertálása - --no-progress Ne jelenítse meg a folyamatjelző pontokat - --strict-types Ellenőrzi, hogy a PHP 7.0 strict_types direktíva engedélyezve van-e -``` - -Paraméterek nélkül az aktuális könyvtárat ellenőrzi read-only módban, a `-f` paraméterrel javítja a fájlokat. - -Mielőtt megismerkedne vele, mindenképpen készítsen biztonsági másolatot a fájlokról. - -A könnyebb indítás érdekében létrehozhatunk egy `code.bat` fájlt: - -```shell -php path_to_Nette_tools\Code-Checker\code-checker %* -``` - - -Mit csinál pontosan? -==================== - -- eltávolítja a [BOM |nette:glossary#BOM]-ot -- ellenőrzi a [Latte |latte:] sablonok érvényességét -- ellenőrzi a `.neon`, `.php` és `.json` fájlok érvényességét -- ellenőrzi a [vezérlőkarakterek |nette:glossary#Vezérlő karakterek] előfordulását -- ellenőrzi, hogy a fájl UTF-8 kódolású-e -- ellenőrzi a hibásan írt `/* @anotace */` (hiányzik a csillag) -- eltávolítja a záró `?>` taget a PHP fájlokból -- eltávolítja a jobb oldali szóközöket és a felesleges sorokat a fájl végéről -- normalizálja a sorelválasztókat a rendszer alapértelmezettjére (ha megadja a `-l` opciót) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/pt/@home.texy b/code-checker/pt/@home.texy deleted file mode 100644 index c50e0603c1..0000000000 --- a/code-checker/pt/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -A ferramenta [Code Checker |https://github.com/nette/code-checker] verifica e, opcionalmente, corrige alguns dos erros formais nos seus códigos-fonte. - - -Instalação -========== - -Você não deve adicionar o Code Checker às suas dependências, mas instalá-lo como um projeto. - -```shell -composer create-project nette/code-checker -``` - -Ou instale-o globalmente usando: - -```shell -composer global require nette/code-checker -``` - -e certifique-se de que seu diretório global `vendor/bin` esteja na [variável de ambiente $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Uso -=== - -``` -Usage: php code-checker [options] - -Options: - -d <path> Pasta ou arquivo para escanear (padrão: diretório atual) - -i | --ignore <mask> Arquivos a ignorar - -f | --fix Corrige arquivos - -l | --eol Converte caracteres de nova linha - --no-progress Não mostrar pontos de progresso - --strict-types Verifica se a diretiva strict_types do PHP 7.0 está habilitada -``` - -Sem parâmetros, verifica o diretório atual no modo somente leitura; com o parâmetro `-f`, corrige os arquivos. - -Antes de se familiarizar com ele, certifique-se de fazer backup dos seus arquivos primeiro. - -Para facilitar a execução, podemos criar um arquivo `code.bat`: - -```shell -php caminho_para_Nette_tools\Code-Checker\code-checker %* -``` - - -O que ele faz? -============== - -- remove o [BOM |nette:glossary#BOM] -- verifica a validade dos templates [Latte |latte:] -- verifica a validade dos arquivos `.neon`, `.php` e `.json` -- verifica a ocorrência de [caracteres de controle |nette:glossary#Caracteres de controle] -- verifica se o arquivo está codificado em UTF-8 -- verifica `/* @anotações */` mal escritas (falta asterisco) -- remove `?>` de fechamento em arquivos PHP -- remove espaços em branco à direita e linhas desnecessárias no final do arquivo -- normaliza os separadores de linha para o padrão do sistema (se você usar a opção `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/ro/@home.texy b/code-checker/ro/@home.texy deleted file mode 100644 index c73570354d..0000000000 --- a/code-checker/ro/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Instrumentul [Code Checker |https://github.com/nette/code-checker] verifică și, eventual, corectează unele dintre erorile formale din codurile dvs. sursă. - - -Instalare -========= - -Code Checker nu ar trebui adăugat la dependențe, ci instalat ca proiect. - -```shell -composer create-project nette/code-checker -``` - -Sau instalați-l global folosind: - -```shell -composer global require nette/code-checker -``` - -și asigurați-vă că directorul dvs. global `vendor/bin` se află în [variabila de mediu $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Utilizare -========= - -``` -Usage: php code-checker [options] - -Options: - -d <path> Director sau fișier de scanat (implicit: directorul curent) - -i | --ignore <mask> Fișiere de ignorat - -f | --fix Corectează fișierele - -l | --eol Convertește caracterele de sfârșit de linie - --no-progress Nu afișa punctele de progres - --strict-types Verifică dacă directiva PHP 7.0 strict_types este activată -``` - -Fără parametri, verifică directorul curent în modul read-only, cu parametrul `-f` corectează fișierele. - -Înainte de a vă familiariza cu el, asigurați-vă că faceți mai întâi o copie de rezervă a fișierelor. - -Pentru o rulare mai ușoară, putem crea un fișier `code.bat`: - -```shell -php cale_catre_Nette_tools\Code-Checker\code-checker %* -``` - - -Ce face? -======== - -- elimină [BOM |nette:glossary#BOM] -- verifică validitatea șabloanelor [Latte |latte:] -- verifică validitatea fișierelor `.neon`, `.php` și `.json` -- verifică prezența [caracterelor de control |nette:glossary#Caractere de control] -- verifică dacă fișierul este codificat în UTF-8 -- verifică `/* @adnotari */` scrise incorect (lipsește asteriscul) -- elimină `?>` de la sfârșitul fișierelor PHP -- elimină spațiile de la sfârșitul rândului și rândurile goale inutile de la sfârșitul fișierului -- normalizează separatorii de rând la cei de sistem (dacă specificați opțiunea `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/sl/@home.texy b/code-checker/sl/@home.texy deleted file mode 100644 index 27cbc98ca0..0000000000 --- a/code-checker/sl/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Orodje [Code Checker |https://github.com/nette/code-checker] preveri in po potrebi popravi nekatere formalne napake v vaših izvornih kodah. - - -Namestitev -========== - -Code Checkerja ne bi smeli dodajati med odvisnosti, ampak ga namestiti kot projekt. - -```shell -composer create-project nette/code-checker -``` - -Ali pa ga namestite globalno s pomočjo: - -```shell -composer global require nette/code-checker -``` - -in se prepričajte, da je vaš globalni imenik `vendor/bin` v [okoljski spremenljivki $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Uporaba -======= - -``` -Usage: php code-checker [options] - -Options: - -d <path> Folder or file to scan (default: current directory) - -i | --ignore <mask> 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 -``` - -Brez parametrov preveri trenutni imenik v načinu samo za branje, s parametrom `-f` popravlja datoteke. - -Preden se z njim seznanite, si vsekakor najprej varnostno kopirajte datoteke. - -Za lažje zaganjanje si lahko ustvarimo datoteko `code.bat`: - -```shell -php pot_do_Nette_tools\Code-Checker\code-checker %* -``` - - -Kaj vse počne? -============== - -- odstranjuje [BOM |nette:glossary#BOM] -- preverja veljavnost [Latte |latte:] predlog -- preverja veljavnost datotek `.neon`, `.php` in `.json` -- preverja pojav [kontrolnih znakov |nette:glossary#Kontrolni znaki] -- preverja, ali je datoteka kodirana v UTF-8 -- preverja napačno zapisane `/* @anotace */` (manjka zvezdica) -- odstranjuje zaključne `?>` pri PHP datotekah -- odstranjuje desne presledke in nepotrebne vrstice na koncu datoteke -- normalizira ločila vrstic na sistemske (če navedete opcijo `-l`) - -{{leftbar: www:@menu-common}} diff --git a/code-checker/uk/@home.texy b/code-checker/uk/@home.texy deleted file mode 100644 index 261ea7550c..0000000000 --- a/code-checker/uk/@home.texy +++ /dev/null @@ -1,65 +0,0 @@ -Nette Code Checker -****************** - -.[perex] -Інструмент [Code Checker |https://github.com/nette/code-checker] перевіряє та, за потреби, виправляє деякі формальні помилки у ваших вихідних кодах. - - -Встановлення -============ - -Code Checker не слід додавати до залежностей, а встановлювати як проект. - -```shell -composer create-project nette/code-checker -``` - -Або встановіть його глобально за допомогою: - -```shell -composer global require nette/code-checker -``` - -і переконайтеся, що ваш глобальний каталог `vendor/bin` знаходиться у [змінній середовища $PATH |https://getcomposer.org/doc/03-cli.md#global]. - - -Використання -============ - -``` -Usage: php code-checker [options] - -Options: - -d <path> Папка або файл для сканування (за замовчуванням: поточний каталог) - -i | --ignore <mask> Файли, які слід ігнорувати - -f | --fix Виправляє файли - -l | --eol Перетворює символи нового рядка - --no-progress Не показувати точки прогресу - --strict-types Перевіряє, чи увімкнена директива PHP 7.0 strict_types -``` - -Без параметрів перевіряє поточний каталог у режимі лише для читання, з параметром `-f` виправляє файли. - -Перш ніж ознайомитися з ним, обов'язково зробіть резервну копію файлів. - -Для полегшення запуску можна створити файл `code.bat`: - -```shell -php шлях_до_Nette_tools\Code-Checker\code-checker %* -``` - - -Що він робить? -============== - -- видаляє [BOM |nette:glossary#BOM] -- перевіряє валідність [Latte |latte:] шаблонів -- перевіряє валідність файлів `.neon`, `.php` та `.json` -- перевіряє наявність [керуючих символів |nette:glossary#Керуючі символи] -- перевіряє, чи файл закодований у UTF-8 -- перевіряє неправильно записані `/* @anotace */` (відсутня зірочка) -- видаляє завершальний `?>` у PHP файлах -- видаляє пробіли в кінці рядка та зайві рядки в кінці файлу -- нормалізує роздільники рядків до системних (якщо вказано опцію `-l`) - -{{leftbar: www:@menu-common}} diff --git a/component-model/bg/@home.texy b/component-model/bg/@home.texy deleted file mode 100644 index 2209fed3e8..0000000000 --- a/component-model/bg/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Компонентен модел -***************** - -.[perex] -Важно понятие в Nette е компонентът. В страниците вмъкваме [визуални интерактивни компоненти |application:components], компоненти са и формите или всички техни елементи. Основните два класа, от които всички тези компоненти наследяват, са част от пакета `nette/component-model` и имат за цел да създават дървовидна йерархия на компоненти. - - -Component -========= -[api:Nette\ComponentModel\Component] е общият предтеча на всички компоненти. Съдържа методи `getName()`, връщащ името на компонента, и метод `getParent()`, връщащ неговия родител. И двете могат да бъдат зададени с метода `setParent()` - първият параметър е родителят, а вторият - името на компонента. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Търси в йерархията нагоре обект от желания клас или интерфейс. Например `$component->lookup(Nette\Application\UI\Presenter::class)` връща презентер, ако компонентът е свързан с него, дори през няколко нива. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Връща така наречения път, който е низ, получен чрез свързване на имената на всички компоненти по пътя между текущия и търсения компонент. Така например `$component->lookupPath(Nette\Application\UI\Presenter::class)` връща уникален идентификатор на компонента спрямо презентера. - - -Container -========= -[api:Nette\ComponentModel\Container] е родителският компонент, т.е. компонент, съдържащ наследници и така образуващ дървовидна структура. Разполага с методи за лесно добавяне, получаване и премахване на обекти. Той е предтеча например на формата или на класовете `Control` и `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Връща компонент. При опит за получаване на недефиниран наследник се извиква фабриката `createComponent($name)`. Методът `createComponent($name)` извиква в текущия компонент метода `createComponent<име на компонента>` и като параметър му предава името на компонента. Създаденият компонент след това се добавя към текущия компонент като негов наследник. Тези методи наричаме фабрики за компоненти и могат да бъдат имплементирани от наследниците на класа `Container`. - - -getComponents(): array .[method] --------------------------------- -Връща преките наследници като масив. Ключовете съдържат имената на тези компоненти. Забележка: във версия 3.0.x методът връщаше итератор вместо масив и първият му параметър определяше дали компонентите да се обхождат в дълбочина, а вторият представляваше типов филтър. Тези параметри са deprecated. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Получава цялата йерархия на компоненти, включително всички вложени подчинени компоненти, като индексиран масив. Търсенето се извършва първо в дълбочина. - - -Наблюдение на предците -====================== - -Компонентният модел на Nette позволява много динамична работа с дървото (можем да премахваме, преместваме, добавяме компоненти), затова би било грешка да се разчита, че след създаването на компонента веднага (в конструктора) е известен родителят, родителят на родителя и т.н. Най-често родителят изобщо не е известен при създаването. - -Как да разберем кога компонентът е бил прикрепен към дървото на презентера? Наблюдението на промяната на родителя не е достатъчно, тъй като към презентера може да е бил прикрепен например родителят на родителя. Помага методът [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Всеки компонент може да наблюдава произволен брой класове и интерфейси. Прикрепването или откачането се съобщава чрез извикване на callback `$attached`, респ. `$detached`, и предаване на обекта на наблюдавания клас. - -За по-добро разбиране, пример: класът `UploadControl`, представляващ формулярен елемент за качване на файлове в Nette Forms, трябва да зададе на формата атрибут `enctype` на стойност `multipart/form-data`. В момента на създаване на обекта обаче той може да не е прикрепен към никаква форма. В кой момент тогава да се модифицира формата? Решението е просто - в конструктора се иска наблюдение: - -```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'); - }); - // ... - } - - // ... -} -``` - -и щом формата е налична, се извиква callback. (Преди това вместо него се използваха общите методи `attached`, респ. `detached`). diff --git a/component-model/bg/@meta.texy b/component-model/bg/@meta.texy deleted file mode 100644 index 794cbc8522..0000000000 --- a/component-model/bg/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документация на Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/el/@home.texy b/component-model/el/@home.texy deleted file mode 100644 index b38ccc1262..0000000000 --- a/component-model/el/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Μοντέλο Component -***************** - -.[perex] -Ένας σημαντικός όρος στο Nette είναι το component. Στις σελίδες εισάγουμε [οπτικά διαδραστικά components |application:components], τα components είναι επίσης φόρμες ή όλα τα στοιχεία τους. Οι δύο βασικές κλάσεις από τις οποίες κληρονομούν όλα αυτά τα components αποτελούν μέρος του πακέτου `nette/component-model` και έχουν ως αποστολή τη δημιουργία μιας ιεραρχικής δενδροειδούς δομής components. - - -Component -========= -Η [api:Nette\ComponentModel\Component] είναι ο κοινός πρόγονος όλων των components. Περιέχει τις μεθόδους `getName()` που επιστρέφει το όνομα του component και τη μέθοδο `getParent()` που επιστρέφει τον γονέα του. Και τα δύο μπορούν να οριστούν με τη μέθοδο `setParent()` - η πρώτη παράμετρος είναι ο γονέας και η δεύτερη το όνομα του component. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Αναζητά στην ιεραρχία προς τα πάνω ένα αντικείμενο της ζητούμενης κλάσης ή interface. Για παράδειγμα, το `$component->lookup(Nette\Application\UI\Presenter::class)` επιστρέφει τον presenter, εάν το component είναι συνδεδεμένο με αυτόν, ακόμη και μέσω πολλών επιπέδων. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Επιστρέφει τη λεγόμενη διαδρομή, η οποία είναι μια συμβολοσειρά που δημιουργείται από τη συνένωση των ονομάτων όλων των components στη διαδρομή μεταξύ του τρέχοντος και του αναζητούμενου component. Έτσι, π.χ., το `$component->lookupPath(Nette\Application\UI\Presenter::class)` επιστρέφει ένα μοναδικό αναγνωριστικό του component σε σχέση με τον presenter. - - -Container -========= -Η [api:Nette\ComponentModel\Container] είναι το γονικό component, δηλ. ένα component που περιέχει απογόνους και σχηματίζει έτσι μια δενδροειδή δομή. Διαθέτει μεθόδους για εύκολη προσθήκη, ανάκτηση και αφαίρεση αντικειμένων. Είναι ο πρόγονος, για παράδειγμα, της φόρμας ή των κλάσεων `Control` και `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Επιστρέφει το component. Κατά την προσπάθεια ανάκτησης ενός μη ορισμένου απογόνου, καλείται το factory `createComponent($name)`. Η μέθοδος `createComponent($name)` καλεί στο τρέχον component τη μέθοδο `createComponent<όνομα_component>` και της περνά ως παράμετρο το όνομα του component. Το δημιουργημένο component προστίθεται στη συνέχεια στο τρέχον component ως απόγονός του. Αυτές οι μέθοδοι ονομάζονται factories component και μπορούν να υλοποιηθούν από απογόνους της κλάσης `Container`. - - -getComponents(): array .[method] --------------------------------- -Επιστρέφει τους άμεσους απογόνους ως πίνακα. Τα κλειδιά περιέχουν τα ονόματα αυτών των components. Σημείωση: στην έκδοση 3.0.x η μέθοδος επέστρεφε έναν iterator αντί για πίνακα και η πρώτη της παράμετρος καθόριζε αν τα components έπρεπε να διασχιστούν σε βάθος, και η δεύτερη αντιπροσώπευε ένα φίλτρο τύπου. Αυτές οι παράμετροι είναι deprecated. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Ανακτά ολόκληρη την ιεραρχία των components, συμπεριλαμβανομένων όλων των ενσωματωμένων θυγατρικών components, ως ευρετηριασμένο πίνακα. Η αναζήτηση γίνεται πρώτα σε βάθος. - - -Παρακολούθηση προγόνων -====================== - -Το μοντέλο component του Nette επιτρέπει πολύ δυναμική εργασία με το δέντρο (μπορούμε να αφαιρούμε, να μετακινούμε, να προσθέτουμε components), επομένως θα ήταν λάθος να βασιζόμαστε στο γεγονός ότι μετά τη δημιουργία ενός component είναι αμέσως γνωστός ο γονέας, ο γονέας του γονέα κ.λπ. (στον κατασκευαστή). Τις περισσότερες φορές, ο γονέας δεν είναι καθόλου γνωστός κατά τη δημιουργία. - -Πώς να αναγνωρίσετε πότε ένα component συνδέθηκε στο δέντρο του presenter; Η παρακολούθηση της αλλαγής του γονέα δεν αρκεί, γιατί μπορεί να έχει συνδεθεί στον presenter ο γονέας του γονέα, για παράδειγμα. Η μέθοδος [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] βοηθάει. Κάθε component μπορεί να παρακολουθεί οποιονδήποτε αριθμό κλάσεων και interfaces. Η σύνδεση ή η αποσύνδεση ανακοινώνεται με την κλήση του callback `$attached` ή `$detached` αντίστοιχα, και την παράδοση του αντικειμένου της παρακολουθούμενης κλάσης. - -Για καλύτερη κατανόηση, ένα παράδειγμα: η κλάση `UploadControl`, που αντιπροσωπεύει ένα στοιχείο φόρμας για την αποστολή αρχείων στο Nette Forms, πρέπει να ορίσει το attribute `enctype` της φόρμας στην τιμή `multipart/form-data`. Κατά τη στιγμή της δημιουργίας του αντικειμένου, όμως, μπορεί να μην είναι συνδεδεμένο με καμία φόρμα. Πότε λοιπόν πρέπει να τροποποιηθεί η φόρμα; Η λύση είναι απλή - στον κατασκευαστή ζητείται η παρακολούθηση: - -```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'); - }); - // ... - } - - // ... -} -``` - -και μόλις η φόρμα είναι διαθέσιμη, καλείται το callback. (Παλαιότερα, χρησιμοποιούνταν αντί αυτού η κοινή μέθοδος `attached` ή `detached`). diff --git a/component-model/el/@meta.texy b/component-model/el/@meta.texy deleted file mode 100644 index a09ce5fe0d..0000000000 --- a/component-model/el/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/hu/@home.texy b/component-model/hu/@home.texy deleted file mode 100644 index 5ea05ef999..0000000000 --- a/component-model/hu/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Komponens modell -**************** - -.[perex] -A Nette fontos fogalma a komponens. Az oldalakra [vizuális interaktív komponenseket |application:components] illesztünk be, komponensek az űrlapok vagy azok összes eleme is. A két alapvető osztály, amelyektől ezek a komponensek öröklődnek, a `nette/component-model` csomag részét képezik, és feladatuk a komponensek fa hierarchiájának létrehozása. - - -Component -========= -Az [api:Nette\ComponentModel\Component] az összes komponens közös őse. Tartalmazza a `getName()` metódust, amely visszaadja a komponens nevét, és a `getParent()` metódust, amely visszaadja a szülőjét. Mindkettőt a `setParent()` metódussal lehet beállítani - az első paraméter a szülő, a második a komponens neve. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Felkeresi a hierarchiában felfelé a kívánt osztály vagy interfész objektumát. Például a `$component->lookup(Nette\Application\UI\Presenter::class)` visszaadja a presentert, ha a komponens hozzá van csatolva, akár több szinten keresztül is. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Visszaadja az úgynevezett utat, amely egy string, ami az aktuális és a keresett komponens közötti útvonalon lévő összes komponens nevének összekapcsolásával jön létre. Tehát pl. a `$component->lookupPath(Nette\Application\UI\Presenter::class)` visszaadja a komponens egyedi azonosítóját a presenterhez képest. - - -Container -========= -Az [api:Nette\ComponentModel\Container] a szülő komponens, azaz a leszármazottakat tartalmazó komponens, amely fa struktúrát alkot. Metódusokkal rendelkezik az objektumok egyszerű hozzáadásához, lekéréséhez és eltávolításához. Például az űrlap vagy a `Control` és `Presenter` osztályok őse. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Visszaadja a komponenst. Egy nem definiált leszármazott lekérésekor a `createComponent($name)` factory hívódik meg. A `createComponent($name)` metódus meghívja az aktuális komponensben a `createComponent<komponens neve>` metódust, és paraméterként átadja neki a komponens nevét. A létrehozott komponens ezután hozzáadódik az aktuális komponenshez annak leszármazottjaként. Ezeket a metódusokat komponens factory-knak nevezzük, és a `Container` osztály leszármazottai implementálhatják őket. - - -getComponents(): array .[method] --------------------------------- -Visszaadja a közvetlen leszármazottakat tömbként. A kulcsok ezeknek a komponenseknek a neveit tartalmazzák. Megjegyzés: a 3.0.x verzióban a metódus tömb helyett iterátort adott vissza, és az első paramétere határozta meg, hogy a komponenseket mélységében kell-e bejárni, a második pedig egy típus szűrőt jelentett. Ezek a paraméterek elavultak. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Lekéri a teljes komponens hierarchiát, beleértve az összes beágyazott alárendelt komponenst is, indexelt tömbként. A keresés először mélységében történik. - - -Ősök monitorozása -================= - -A Nette komponens modellje nagyon dinamikus munkát tesz lehetővé a fával (komponenseket kivehetünk, áthelyezhetünk, hozzáadhatunk), ezért hiba lenne arra támaszkodni, hogy a komponens létrehozása után azonnal (a konstruktorban) ismert a szülő, a szülő szülője stb. Legtöbbször ugyanis a szülő a létrehozáskor egyáltalán nem ismert. - -Hogyan lehet tudni, mikor csatlakozott a komponens a presenter fájához? A szülő változásának figyelése nem elegendő, mert a presenterhez például a szülő szülője is csatlakozhatott. Segít a [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] metódus. Minden komponens tetszőleges számú osztályt és interfészt monitorozhat. A csatlakozást vagy leválasztást a `$attached`, illetve `$detached` callback meghívása jelzi, átadva a figyelt osztály objektumát. - -A jobb megértés érdekében egy példa: az `UploadControl` osztály, amely a Nette Forms fájlfeltöltési űrlap elemét képviseli, be kell állítania az űrlap `enctype` attribútumát `multipart/form-data` értékre. Az objektum létrehozásakor azonban nem feltétlenül kell csatlakoznia semmilyen űrlaphoz. Melyik pillanatban kell tehát módosítani az űrlapot? A megoldás egyszerű - a konstruktorban kérjük a monitorozást: - -```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'); - }); - // ... - } - - // ... -} -``` - -és amint az űrlap elérhetővé válik, a callback meghívódik. (Korábban helyette a közös `attached`, illetve `detached` metódust használták). diff --git a/component-model/hu/@meta.texy b/component-model/hu/@meta.texy deleted file mode 100644 index c00a2158aa..0000000000 --- a/component-model/hu/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette dokumentáció}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/pt/@home.texy b/component-model/pt/@home.texy deleted file mode 100644 index 2e57ace611..0000000000 --- a/component-model/pt/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Modelo de Componente -******************** - -.[perex] -Um conceito importante no Nette é o componente. Inserimos [componentes interativos visuais |application:components] nas páginas, formulários são componentes, assim como todos os seus elementos. As duas classes base das quais todos esses componentes herdam fazem parte do pacote `nette/component-model` e têm a tarefa de criar uma hierarquia de componentes em árvore. - - -Component -========= -[api:Nette\ComponentModel\Component] é o ancestral comum de todos os componentes. Contém os métodos `getName()` que retorna o nome do componente e `getParent()` que retorna seu pai. Ambos podem ser definidos usando o método `setParent()` - o primeiro parâmetro é o pai e o segundo é o nome do componente. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Procura na hierarquia para cima um objeto da classe ou interface solicitada. Por exemplo, `$component->lookup(Nette\Application\UI\Presenter::class)` retorna o presenter, se o componente estiver anexado a ele, mesmo através de vários níveis. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Retorna o chamado caminho, que é uma string formada pela concatenação dos nomes de todos os componentes no caminho entre o componente atual e o componente procurado. Assim, por exemplo, `$component->lookupPath(Nette\Application\UI\Presenter::class)` retorna um identificador único do componente em relação ao presenter. - - -Container -========= -[api:Nette\ComponentModel\Container] é o componente pai, ou seja, um componente que contém descendentes e forma assim uma estrutura em árvore. Possui métodos para fácil adição, obtenção e remoção de objetos. É o ancestral, por exemplo, do formulário ou das classes `Control` e `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Retorna um componente. Ao tentar obter um descendente indefinido, a fábrica `createComponent($name)` é chamada. O método `createComponent($name)` chama o método `createComponent<nome do componente>` no componente atual e passa o nome do componente como parâmetro. O componente criado é então adicionado ao componente atual como seu descendente. Chamamos esses métodos de fábricas de componentes e eles podem ser implementados por descendentes da classe `Container`. - - -getComponents(): array .[method] --------------------------------- -Retorna os descendentes diretos como um array. As chaves contêm os nomes desses componentes. Nota: na versão 3.0.x, o método retornava um iterador em vez de um array, e seu primeiro parâmetro determinava se os componentes deveriam ser percorridos em profundidade, e o segundo representava um filtro de tipo. Esses parâmetros estão obsoletos. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Obtém toda a hierarquia de componentes, incluindo todos os componentes filhos aninhados, como um array indexado. A busca é feita primeiro em profundidade. - - -Monitoramento de Ancestrais -=========================== - -O modelo de componente Nette permite um trabalho muito dinâmico com a árvore (podemos remover, mover, adicionar componentes), portanto seria um erro confiar que, após a criação de um componente, o pai, o pai do pai, etc., sejam imediatamente conhecidos (no construtor). Na maioria das vezes, o pai não é conhecido durante a criação. - -Como saber quando um componente foi anexado à árvore do presenter? Observar a mudança do pai não é suficiente, porque o pai do pai pode ter sido anexado ao presenter, por exemplo. O método [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()] ajuda. Cada componente pode monitorar qualquer número de classes e interfaces. A anexação ou desanexação é sinalizada chamando o callback `$attached` ou `$detached`, respectivamente, e passando o objeto da classe monitorada. - -Para melhor compreensão, um exemplo: a classe `UploadControl`, que representa o elemento de formulário para upload de arquivos no Nette Forms, precisa definir o atributo `enctype` do formulário para o valor `multipart/form-data`. No entanto, no momento da criação do objeto, ele pode não estar anexado a nenhum formulário. Em que momento, então, modificar o formulário? A solução é simple - no construtor, solicita-se o monitoramento: - -```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'); - }); - // ... - } - - // ... -} -``` - -e assim que o formulário estiver disponível, o callback é chamado. (Anteriormente, os métodos comuns `attached` e `detached` eram usados em seu lugar). diff --git a/component-model/pt/@meta.texy b/component-model/pt/@meta.texy deleted file mode 100644 index e2566bcb44..0000000000 --- a/component-model/pt/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentação Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/ro/@home.texy b/component-model/ro/@home.texy deleted file mode 100644 index 6bf15157b2..0000000000 --- a/component-model/ro/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Modelul de componente -********************* - -.[perex] -Un concept important în Nette este componenta. În pagini inserăm [componente vizuale interactive |application:components], componente sunt și formularele sau toate elementele lor. Cele două clase de bază, din care moștenesc toate aceste componente, fac parte din pachetul `nette/component-model` și au rolul de a crea o ierarhie arborescentă de componente. - - -Component -========= -[api:Nette\ComponentModel\Component] este strămoșul comun al tuturor componentelor. Conține metodele `getName()` care returnează numele componentei și metoda `getParent()` care returnează părintele său. Ambele pot fi setate cu metoda `setParent()` - primul parametru este părintele și al doilea este numele componentei. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Caută în ierarhie în sus un obiect de clasa sau interfața dorită. De exemplu, `$component->lookup(Nette\Application\UI\Presenter::class)` returnează presenter-ul, dacă componenta este atașată la acesta, chiar și prin mai multe niveluri. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Returnează așa-numita cale, care este un șir de caractere format prin concatenarea numelor tuturor componentelor de pe calea dintre componenta curentă și cea căutată. Deci, de exemplu, `$component->lookupPath(Nette\Application\UI\Presenter::class)` returnează un identificator unic al componentei față de presenter. - - -Container -========= -[api:Nette\ComponentModel\Container] este componenta părinte, adică o componentă care conține descendenți și formează astfel o structură arborescentă. Dispune de metode pentru adăugarea, obținerea și eliminarea ușoară a obiectelor. Este strămoșul, de exemplu, al formularului sau al claselor `Control` și `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Returnează componenta. La încercarea de a obține un descendent nedefinit, este apelată fabrica `createComponent($name)`. Metoda `createComponent($name)` apelează în componenta curentă metoda `createComponent<nume componenta>` și îi transmite ca parametru numele componentei. Componenta creată este apoi adăugată la componenta curentă ca descendent al acesteia. Aceste metode le numim fabrici de componente și pot fi implementate de descendenții clasei `Container`. - - -getComponents(): array .[method] --------------------------------- -Returnează descendenții direcți ca array. Cheile conțin numele acestor componente. Notă: în versiunea 3.0.x, metoda returna un iterator în loc de array, iar primul său parametru specifica dacă componentele trebuie parcurse în adâncime, iar al doilea reprezenta un filtru de tip. Acești parametri sunt depreciați. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Obține întreaga ierarhie de componente, inclusiv toate componentele subordonate imbricate, ca un array indexat. Căutarea se face mai întâi în adâncime. - - -Monitorizarea strămoșilor -========================= - -Modelul de componente Nette permite o muncă foarte dinamică cu arborele (putem elimina, muta, adăuga componente), de aceea ar fi o greșeală să ne bazăm pe faptul că, după crearea componentei, părintele, părintele părintelui etc. sunt imediat cunoscuți (în constructor). De obicei, părintele nu este deloc cunoscut la creare. - -Cum să aflăm când a fost componenta atașată la arborele presenter-ului? Urmărirea schimbării părintelui nu este suficientă, deoarece la presenter ar fi putut fi atașat, de exemplu, părintele părintelui. Ajută metoda [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Fiecare componentă poate monitoriza orice număr de clase și interfețe. Atașarea sau detașarea este anunțată prin apelarea callback-ului `$attached` respectiv `$detached`, și transmiterea obiectului clasei monitorizate. - -Pentru o mai bună înțelegere, un exemplu: clasa `UploadControl`, reprezentând elementul de formular pentru încărcarea fișierelor în Nette Forms, trebuie să seteze atributul `enctype` al formularului la valoarea `multipart/form-data`. Dar în momentul creării obiectului, este posibil să nu fie atașată la niciun formular. În ce moment, deci, să modificăm formularul? Soluția este simplă - în constructor se solicită monitorizarea: - -```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'); - }); - // ... - } - - // ... -} -``` - -și de îndată ce formularul este disponibil, se apelează callback-ul. (Anterior, în locul său se folosea metoda comună `attached` respectiv `detached`). diff --git a/component-model/ro/@meta.texy b/component-model/ro/@meta.texy deleted file mode 100644 index 6554692600..0000000000 --- a/component-model/ro/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Documentație Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/sl/@home.texy b/component-model/sl/@home.texy deleted file mode 100644 index cc2bd1f48c..0000000000 --- a/component-model/sl/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Komponentni model -***************** - -.[perex] -Pomemben pojem v Nette je komponenta. V strani vstavljamo [vizualne interaktivne komponente |application:components], komponente so tudi obrazci ali vsi njihovi elementi. Osnovna dva razreda, od katerih vse te komponente dedujejo, sta del paketa `nette/component-model` in imata nalogo ustvarjati drevesno hierarhijo komponent. - - -Component -========= -[api:Nette\ComponentModel\Component] je skupni prednik vseh komponent. Vsebuje metodi `getName()`, ki vrača ime komponente, in metodo `getParent()`, ki vrača njenega starša. Oboje lahko nastavimo z metodo `setParent()` - prvi parameter je starš in drugi ime komponente. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -V hierarhiji navzgor poišče objekt zahtevanega razreda ali vmesnika. Na primer `$component->lookup(Nette\Application\UI\Presenter::class)` vrne presenter, če je komponenta nanj, tudi preko več nivojev, priključena. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Vrača t.i. pot, kar je niz, nastal s spajanjem imen vseh komponent na poti med trenutno in iskano komponento. Torej npr. `$component->lookupPath(Nette\Application\UI\Presenter::class)` vrača edinstven identifikator komponente glede na presenter. - - -Container -========= -[api:Nette\ComponentModel\Container] je starševska komponenta, tj. komponenta, ki vsebuje potomce in tako tvori drevesno strukturo. Ima metode za enostavno dodajanje, pridobivanje in odstranjevanje objektov. Je prednik na primer obrazca ali razredov `Control` in `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Vrača komponento. Pri poskusu pridobivanja nedefiniranega potomca se pokliče tovarna `createComponent($name)`. Metoda `createComponent($name)` v trenutni komponenti pokliče metodo `createComponent<ime komponente>` in ji kot parameter posreduje ime komponente. Ustvarjena komponenta se nato doda v trenutno komponento kot njen potomec. Tem metodam rečemo tovarne komponent in jih lahko implementirajo potomci razreda `Container`. - - -getComponents(): array .[method] --------------------------------- -Vrača neposredne potomce kot polje. Ključi vsebujejo imena teh komponent. Opomba: v različici 3.0.x je metoda namesto polja vračala iterator in njen prvi parameter je določal, ali naj se komponente prehajajo v globino, drugi pa je predstavljal tipski filter. Ti parametri so zastareli. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Pridobi celotno hierarhijo komponent, vključno z vsemi gnezdenimi podrejenimi komponentami, kot indeksirano polje. Iskanje gre najprej v globino. - - -Spremljanje prednikov -===================== - -Komponentni model Nette omogoča zelo dinamično delo z drevesom (komponente lahko odstranjujemo, premikamo, dodajamo), zato bi bila napaka zanašati se na to, da je po ustvarjanju komponente takoj (v konstruktorju) znan starš, starš starša itd. Večinoma namreč starš ob ustvarjanju sploh ni znan. - -Kako ugotoviti, kdaj je bila komponenta priključena v drevo presenterja? Spremljanje spremembe starša ni dovolj, saj je bil lahko k presenterju priključen na primer starš starša. Pomaga metoda [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Vsaka komponenta lahko spremlja poljubno število razredov in vmesnikov. Priključitev ali odklop je sporočen s klicem povratnega klica `$attached` oz. `$detached`, in posredovanjem objekta spremljanega razreda. - -Za boljše razumevanje primer: razred `UploadControl`, ki predstavlja obrazčevni element za nalaganje datotek v Nette Forms, mora obrazcu nastaviti atribut `enctype` na vrednost `multipart/form-data`. V času ustvarjanja objekta pa ni nujno, da je priključen na kakršenkoli obrazec. V katerem trenutku torej modificirati obrazec? Rešitev je enostavna - v konstruktorju se zahteva spremljanje: - -```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'); - }); - // ... - } - - // ... -} -``` - -in takoj ko je obrazec na voljo, se pokliče povratni klic. (Prej se je namesto njega uporabljala skupna metoda `attached` oz. `detached`). diff --git a/component-model/sl/@meta.texy b/component-model/sl/@meta.texy deleted file mode 100644 index 282883a3d6..0000000000 --- a/component-model/sl/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Nette Dokumentacija}} -{{leftbar: nette:@menu-topics}} diff --git a/component-model/uk/@home.texy b/component-model/uk/@home.texy deleted file mode 100644 index 34b27ba85e..0000000000 --- a/component-model/uk/@home.texy +++ /dev/null @@ -1,67 +0,0 @@ -Компонентна модель -****************** - -.[perex] -Важливим поняттям у Nette є компонент. На сторінки ми вставляємо [візуальні інтерактивні компоненти |application:components], компонентами є також форми або всі їхні елементи. Основні два класи, від яких успадковуються всі ці компоненти, є частиною пакету `nette/component-model` і мають на меті створення ієрархії компонентів у вигляді дерева. - - -Component -========= -[api:Nette\ComponentModel\Component] є спільним предком усіх компонентів. Він містить методи `getName()`, що повертає назву компонента, та метод `getParent()`, що повертає його батька. Обидва можна встановити методом `setParent()` - перший параметр - батько, а другий - назва компонента. - - -lookup(string $type): ?Component .[method] ------------------------------------------- -Шукає в ієрархії вгору об'єкт потрібного класу або інтерфейсу. Наприклад, `$component->lookup(Nette\Application\UI\Presenter::class)` повертає presenter, якщо компонент приєднаний до нього, навіть через кілька рівнів. - - -lookupPath(string $type): ?string .[method] -------------------------------------------- -Повертає так званий шлях, який є рядком, утвореним з'єднанням імен усіх компонентів на шляху між поточним та шуканим компонентом. Так, наприклад, `$component->lookupPath(Nette\Application\UI\Presenter::class)` повертає унікальний ідентифікатор компонента відносно presenter. - - -Container -========= -[api:Nette\ComponentModel\Container] є батьківським компонентом, тобто компонентом, що містить нащадків і таким чином утворює деревоподібну структуру. Він має методи для легкого додавання, отримання та видалення об'єктів. Він є предком, наприклад, форми або класів `Control` та `Presenter`. - - -getComponent(string $name): ?Component .[method] ------------------------------------------------- -Повертає компонент. При спробі отримати невизначеного нащадка викликається фабрика `createComponent($name)`. Метод `createComponent($name)` викликає в поточному компоненті метод `createComponent<назва компонента>` і передає йому як параметр назву компонента. Створений компонент потім додається до поточного компонента як його нащадок. Ці методи називаються фабриками компонентів і можуть бути реалізовані нащадками класу `Container`. - - -getComponents(): array .[method] --------------------------------- -Повертає прямих нащадків у вигляді масиву. Ключі містять назви цих компонентів. Примітка: у версії 3.0.x метод повертав ітератор замість масиву, а його перший параметр визначав, чи слід проходити компоненти в глибину, а другий представляв фільтр типів. Ці параметри є застарілими. - - -getComponentTree(): array .[method]{data-version:3.1.0} -------------------------------------------------------- -Отримує всю ієрархію компонентів, включаючи всі вкладені дочірні компоненти, у вигляді індексованого масиву. Пошук спочатку йде в глибину. - - -Моніторинг предків -================== - -Компонентна модель Nette дозволяє дуже динамічно працювати з деревом (компоненти можна видаляти, переміщати, додавати), тому було б помилкою покладатися на те, що після створення компонента відразу (в конструкторі) відомий батько, батько батька і т.д. Зазвичай батько при створенні взагалі не відомий. - -Як дізнатися, коли компонент був приєднаний до дерева presenter? Спостерігати за зміною батька недостатньо, оскільки до presenter міг бути приєднаний, наприклад, батько батька. Допоможе метод [monitor($type, $attached, $detached)|api:Nette\ComponentModel\Component::monitor()]. Кожен компонент може моніторити будь-яку кількість класів та інтерфейсів. Приєднання або від'єднання повідомляється викликом callback-функції `$attached` або `$detached` відповідно, і передачею об'єкта відстежуваного класу. - -Для кращого розуміння приклад: клас `UploadControl`, що представляє елемент форми для завантаження файлів у Nette Forms, повинен встановити атрибут `enctype` форми на значення `multipart/form-data`. Однак у момент створення об'єкта він може не бути приєднаним до жодної форми. В який момент тоді модифікувати форму? Рішення просте - в конструкторі запитується моніторинг: - -```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'); - }); - // ... - } - - // ... -} -``` - -і як тільки форма стає доступною, викликається callback. (Раніше замість нього використовувалися спільні методи `attached` або `detached`). diff --git a/component-model/uk/@meta.texy b/component-model/uk/@meta.texy deleted file mode 100644 index 083a8ab9f7..0000000000 --- a/component-model/uk/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документація Nette}} -{{leftbar: nette:@menu-topics}} diff --git a/contributing/bg/@home.texy b/contributing/bg/@home.texy deleted file mode 100644 index 628390697b..0000000000 --- a/contributing/bg/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Станете сътрудник на Nette -************************** - -.[perex] -Научете как можете да се включите в нашия open source проект. Овладейте процедурите за принос към изходния код и документацията и станете част от общността на разработчиците, които активно участват в подобряването на Nette. - - -**Код** - -- [Как да допринесем към кода? |code] -- [Стандарт за кодиране |coding-standard] - -**Документация** - -- [Как да допринесем към документацията? |documentation] -- [Синтаксис на документацията |syntax] -- "Редактор за предварителен преглед":https://editor.nette.org diff --git a/contributing/bg/@left-menu.texy b/contributing/bg/@left-menu.texy deleted file mode 100644 index 111ad4339d..0000000000 --- a/contributing/bg/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Код -*** -- [Как да допринесем към кода? |code] -- [Стандарт за кодиране |coding-standard] - -Документация -************ -- [Как да допринесем към документацията? |documentation] -- [Синтаксис на документацията |syntax] -- "Редактор за предварителен преглед":https://editor.nette.org diff --git a/contributing/bg/code.texy b/contributing/bg/code.texy deleted file mode 100644 index 9e609453a3..0000000000 --- a/contributing/bg/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Как да допринесете към кода -*************************** - -.[perex] -Подготвяте се да допринесете към Nette Framework и трябва да се ориентирате в правилата и процедурите? Този наръчник за начинаещи ще ви покаже стъпка по стъпка как ефективно да допринасяте към кода, да работите с хранилища и да внедрявате промени. - - -Процедура -========= - -За да допринесете към кода, е необходимо да имате акаунт в [GitHub|https://github.com] и да сте запознати с основите на работа със системата за контрол на версиите Git. Ако не владеете работата с Git, можете да разгледате ръководството [git - the simple guide |https://rogerdudler.github.io/git-guide/] и евентуално да използвате някой от многото [графични клиенти |https://git-scm.com/downloads/guis]. - - -Подготовка на средата и хранилището ------------------------------------ - -1) В GitHub си създайте [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] на хранилището на [пакета |www:packages], който се готвите да промените -2) [Клонирайте |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] това хранилище на своя компютър -3) Инсталирайте зависимостите, включително [Nette Tester |tester:], с командата `composer install` -4) Проверете дали тестовете работят, като стартирате `composer tester` -5) Създайте си [#нов branch] базиран на последната издадена версия - - -Внедряване на собствени промени -------------------------------- - -Сега можете да направите своите собствени промени в кода: - -1) програмирайте желаните промени и не забравяйте тестовете -2) уверете се, че тестовете преминават успешно, с помощта на `composer tester` -3) проверете дали кодът отговаря на [стандарта за кодиране |#Стандарти за кодиране] -4) запазете промените (commit) с описание в [този формат |#Описание на commit] - -Можете да създадете няколко commit-а, по един за всяка логическа стъпка. Всеки commit трябва да бъде смислен сам по себе си. - - -Изпращане на промените ----------------------- - -След като сте доволни от промените, можете да ги изпратите: - -1) изпратете (push) промените в GitHub към вашия fork -2) оттам ги изпратете към Nette хранилището, като създадете [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) посочете в описанието [достатъчно информация |#Описание на pull request] - - -Обработване на забележките --------------------------- - -Вашите commit-и сега ще бъдат видени и от други. Обичайно е да получите коментари със забележки: - -1) следете предложените корекции -2) обработете ги като нови commit-и или ги [обединете с предишните |https://help.github.com/en/github/using-git/about-git-rebase] -3) отново изпратете commit-ите в GitHub и те автоматично ще се появят в pull request-а - -Никога не създавайте нов pull request за корекция на съществуващ. - - -Документация ------------- - -Ако сте променили функционалност или сте добавили нова, не забравяйте да я [добавите и в документацията |documentation]. - - -Нов branch -========== - -Ако е възможно, правете промените спрямо последната издадена версия, т.е. последния таг в дадения branch. За таг `v3.2.1` ще създадете branch с тази команда: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Стандарти за кодиране -===================== - -Вашият код трябва да отговаря на [стандарта за кодиране |coding-standard], използван в Nette Framework. За проверка и корекция на кода е наличен автоматичен инструмент. Може да бъде инсталиран чрез Composer **глобално** във ваша избрана папка: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Сега трябва да можете да стартирате инструмента в терминала. С първата команда ще проверите, а с втората и ще коригирате кода в папките `src` и `tests` в текущата директория: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Описание на commit -================== - -В Nette темите на commit-ите имат формат: `Presenter: fixed AJAX detection [Closes #69]` - -- област, последвана от двоеточие -- целта на commit-а в минало време; ако е възможно, започнете с думата: "added" (добавена нова функционалност), "fixed" (корекция), "refactored" (промяна в кода без промяна на поведението), "changed", "removed" -- ако commit-ът нарушава обратната съвместимост, добавете "BC break" -- евентуална връзка към issue tracker като `(#123)` или `[Closes #69]` -- след темата може да последва един празен ред и след това по-подробно описание, включително например връзки към форума - - -Описание на pull request -======================== - -При създаване на pull request интерфейсът на GitHub ще ви позволи да въведете заглавие и описание. Посочете описателно заглавие и в описанието предоставете колкото се може повече информация за причините за вашата промяна. - -Ще се покаже и заглавие, където да посочите дали става въпрос за нова функция или корекция на грешка и дали може да настъпи нарушаване на обратната съвместимост (BC break). Ако има свързан проблем (issue), посочете го, за да бъде затворен след одобрение на pull request-а. - -``` -- bug fix / new feature? <!-- #issue номера, ако има --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- силно приветствано, вижте https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/bg/coding-standard.texy b/contributing/bg/coding-standard.texy deleted file mode 100644 index a6371c01dd..0000000000 --- a/contributing/bg/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Стандарт за кодиране -******************** - -.[perex] -Този документ описва правилата и препоръките за разработка на Nette. При допринасяне на код към Nette трябва да ги спазвате. Най-лесният начин да го направите е да имитирате съществуващия код. Целта е целият код да изглежда така, сякаш е написан от един човек. - -Стандартът за кодиране на Nette отговаря на [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/], с две основни изключения: за отстъп използва [#табулатори вместо интервали] и за [константи на класове използва PascalCase|https://blog.nette.org/bg/for-less-screaming-in-the-code]. - - -Общи правила -============ - -- Всеки PHP файл трябва да съдържа `declare(strict_types=1)` -- Два празни реда се използват за разделяне на методи за по-добра четливост. -- Причината за използване на shut-up оператора трябва да бъде документирана: `@mkdir($dir); // @ - директорията може да съществува`. -- Ако се използва оператор за сравнение със слабо типизиране (т.е. `==`, `!=`, ...), намерението трябва да бъде документирано: `// == приема null` -- В един файл `exceptions.php` можете да запишете няколко изключения. -- При интерфейсите не се специфицира видимостта на методите, тъй като те винаги са публични. -- Всяко свойство, върната стойност и параметър трябва да имат посочен тип. Обратно, при `final` константи никога не посочваме тип, тъй като той е очевиден. -- За ограждане на низ трябва да се използват единични кавички, с изключение на случаите, когато самият литерал съдържа апострофи. - - -Конвенции за именуване -====================== - -- Не използвайте съкращения, освен ако цялото име не е твърде дълго. -- При двубуквени съкращения използвайте главни букви, при по-дълги съкращения PascalCase/camelCase. -- За име на клас използвайте съществително име или словосъчетание. -- Имената на класовете трябва да съдържат не само спецификата (`Array`), но и общността (`ArrayIterator`). Изключение са PHP атрибутите. -- "Константите на класове и енумите трябва да използват PascalCaps":https://blog.nette.org/bg/for-less-screaming-in-the-code. -- "Интерфейсите и абстрактните класове не трябва да съдържат префикси или суфикси":https://blog.nette.org/bg/prefixes-and-suffixes-do-not-belong-in-interface-names като `Abstract`, `Interface` или `I`. - - -Обвиване и скоби -================ - -Стандартът за кодиране на Nette отговаря на PSR-12 (респ. PER Coding Style), в някои точки го допълва или променя: - -- arrow функциите се пишат без интервал преди скобата, т.е. `fn($a) => $b`. -- не се изисква празен ред между различните типове `use` импортиращи изрази. -- типът на връщаната стойност на функция/метод и началната фигурна скоба винаги са на отделни редове: - -```php - public function find( - string $dir, - array $options, - ): array - { - // тяло на метода - } -``` - -Началната фигурна скоба на отделен ред е важна за визуалното разделяне на сигнатурата на функцията/метода от тялото. Ако сигнатурата е на един ред, разделянето е ясно (изображение вляво), ако е на няколко реда, в PSR сигнатурата и тялото се сливат (в средата), докато в стандарта на Nette те продължават да бъдат разделени (вдясно): - -[* new-line-after.webp *] - - -Документационни блокове (phpDoc) -================================ - -Основно правило: Никога не дублирайте никаква информация в сигнатурата, като тип на параметър или тип на връщаната стойност, без добавена стойност. - -Документационен блок за дефиниция на клас: - -- Започва с описание на класа. -- Следва празен ред. -- Следват анотации `@property` (или `@property-read`, `@property-write`), една след друга. Синтаксисът е: анотация, интервал, тип, интервал, `$име`. -- Следват анотации `@method`, една след друга. Синтаксисът е: анотация, интервал, тип на връщаната стойност, интервал, име(тип $param, ...). -- Анотацията `@author` се пропуска. Авторството се съхранява в историята на изходния код. -- Могат да се използват анотации `@internal` или `@deprecated`. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Документационен блок за свойство, който съдържа само анотация `@var`, трябва да бъде едноредов: - -```php -/** @var string[] */ -private array $name; -``` - -Документационен блок за дефиниция на метод: - -- Започва с кратко описание на метода. -- Без празен ред. -- Анотации `@param` на отделни редове. -- Анотация `@return`. -- Анотации `@throws`, една след друга. -- Могат да се използват анотации `@internal` или `@deprecated`. - -След всяка анотация следва един интервал, с изключение на `@param`, след която за по-добра четливост следват два интервала. - -```php -/** - * Намира файл в директория. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Табулатори вместо интервали -=========================== - -Табулаторите имат няколко предимства пред интервалите: - -- размерът на отстъпа може да се персонализира в редакторите и в "уеб":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- не налагат на кода предпочитанията на потребителя за размера на отстъпа, така че кодът е по-преносим -- могат да се напишат с едно натискане на клавиш (навсякъде, не само в редактори, които превръщат табулаторите в интервали) -- отстъпването е тяхната цел -- уважават нуждите на колегите със зрителни увреждания и незрящите - -Чрез използването на табулатори в нашите проекти позволяваме персонализиране на ширината, което може да изглежда като излишно за повечето хора, но за хората със зрителни увреждания е необходимо. - -За незрящите програмисти, които използват брайлови дисплеи, всеки интервал представлява една брайлова клетка. Така че, ако отстъпът по подразбиране е 4 интервала, отстъпът от 3-то ниво губи 12 ценни брайлови клетки още преди началото на кода. На 40-клетъчен дисплей, който се използва най-често при лаптопи, това е повече от една четвърт от наличните клетки, които са пропилени без никаква информация. - - -{{priority: -1}} diff --git a/contributing/bg/documentation.texy b/contributing/bg/documentation.texy deleted file mode 100644 index 4f03aca70a..0000000000 --- a/contributing/bg/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Как да допринесете към документацията -************************************* - -.[perex] -Допринасянето към документацията е една от най-полезните дейности, тъй като помагате на другите да разберат framework-а. - - -Как да пишем? -------------- - -Документацията е предназначена предимно за хора, които се запознават с темата. Затова трябва да отговаря на няколко важни точки: - -- Започнете от простото и общото. Към по-напредналите теми преминете едва накрая -- Опитайте се да обясните нещата възможно най-добре. Опитайте например първо да обясните темата на колега -- Посочвайте само тази информация, която потребителят действително трябва да знае по дадената тема -- Проверете дали вашата информация е наистина вярна. Тествайте всеки код -- Бъдете кратки - това, което напишете, съкратете наполовина. А след това спокойно още веднъж -- Пестете всякакви видове подчертавания, от удебелен шрифт до рамки като `.[note]` -- В кодовете спазвайте [Стандарта за кодиране |coding-standard] - -Освойте също [синтаксиса |syntax]. За преглед на статията по време на писането й можете да използвате [редактор с преглед |https://editor.nette.org/]. - - -Езикови версии --------------- - -Основният език е английският, така че вашите промени трябва да бъдат на чешки и английски. Ако английският не е вашата силна страна, използвайте [DeepL Translator |https://www.deepl.com/translator] и другите ще проверят текста ви. - -Преводът на други езици ще бъде извършен автоматично след одобрение и финализиране на вашата корекция. - - -Тривиални корекции ------------------- - -За да допринесете към документацията, е необходимо да имате акаунт в [GitHub|https://github.com]. - -Най-лесният начин да направите дребна промяна в документацията е да използвате връзките в края на всяка страница: - -- *Покажи в GitHub* отваря изходния вид на дадената страница в GitHub. След това е достатъчно да натиснете бутона `E` и можете да започнете да редактирате (необходимо е да сте влезли в GitHub) -- *Отвори преглед* отваря редактор, където веднага виждате и крайния визуален вид - -Тъй като [редакторът с преглед |https://editor.nette.org/] няма възможност да запазва промените директно в GitHub, е необходимо след завършване на корекциите да копирате изходния текст в клипборда (с бутона *Copy to clipboard*) и след това да го поставите в редактора в GitHub. Под полето за редактиране има формуляр за изпращане. Тук не забравяйте да обобщите накратко и да обясните причината за вашата корекция. След изпращане се създава т.нар. pull request (PR), който може да бъде редактиран допълнително. - - -По-големи корекции ------------------- - -По-подходящо, отколкото да използвате интерфейса на GitHub, е да сте запознати с основите на работа със системата за контрол на версиите Git. Ако не владеете работата с Git, можете да разгледате ръководството [git - the simple guide |https://rogerdudler.github.io/git-guide/] и евентуално да използвате някой от многото [графични клиенти |https://git-scm.com/downloads/guis]. - -Редактирайте документацията по този начин: - -1) В GitHub си създайте [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] на хранилището [nette/docs |https://github.com/nette/docs] -2) [Клонирайте |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] това хранилище на своя компютър -3) След това в [съответния branch |#Структура на документацията] направете промените -4) Проверете за излишни интервали в текста с помощта на инструмента [Code-Checker |code-checker:] -4) Запазете промените (commit) -6) Ако сте доволни от промените, изпратете ги (push) в GitHub към вашия fork -7) Оттам ги изпратете към хранилището `nette/docs`, като създадете [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) - -Обичайно е да получавате коментари със забележки. Следете предложените промени и ги обработете. Добавете предложените промени като нови commit-и и отново ги изпратете в GitHub. Никога не създавайте нов pull request заради корекция на съществуващ pull request. - - -Структура на документацията ---------------------------- - -Цялата документация е разположена в GitHub в хранилището [nette/docs |https://github.com/nette/docs]. Текущата версия е в `master`, по-старите версии са разположени в branch-ове като `doc-3.x`, `doc-2.x`. - -Съдържанието на всеки branch се разделя на основни папки, представляващи отделните области на документацията. Например `application/` отговаря на https://doc.nette.org/bg/application, `latte/` отговаря на https://latte.nette.org и т.н. Всяка такава папка съдържа подпапки, представляващи езиковите версии (`cs`, `en`, `bg`, ...) и евентуално подпапка `files` с изображения, които могат да бъдат вмъквани в страниците на документацията. diff --git a/contributing/bg/syntax.texy b/contributing/bg/syntax.texy deleted file mode 100644 index 94bbb02a7a..0000000000 --- a/contributing/bg/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Синтаксис на документацията -*************************** - -Документацията използва Markdown & [синтаксис на Texy |https://texy.nette.org/syntax] с някои разширения. - - -Връзки -====== - -За вътрешни връзки се използва запис в квадратни скоби `[връзка |odkaz]`. И това е или във формата с вертикална черта `[текст на връзката |цел на връзката]`, или съкратено `[текст на връзката]`, ако целта е същата като текста (след трансформация в малки букви и тирета): - -- `[Page name]` -> `<a href="/bg/page-name">Page name</a>` -- `[текст на връзка |Page name]` -> `<a href="/bg/page-name">текст на връзка</a>` - -Можем да правим връзки към друга езикова версия или към друга секция. Под секция се разбира Nette библиотека (напр. `forms`, `latte` и др.) или специални секции като `best-practices`, `quickstart` и т.н.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (същата секция, друг език) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/bg/page-name">Page name</a>` (друга секция, същия език) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (друга секция и език) - -С помощта на `#` е възможно също така да се насочи към конкретно заглавие на страницата. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (заглавие на текущата страница) -- `[Page name#Heading]` -> `<a href="/bg/page-name#toc-heading">Page name</a>` - -Връзка към началната страница на секцията: (`@home` е специален израз за началната страница на секцията) - -- `[текст на връзка |@home]` -> `<a href="/bg/">текст на връзка</a>` -- `[текст на връзка |tracy:]` -> `<a href="//tracy.nette.org/bg/">текст на връзка</a>` - - -Връзки към API документацията ------------------------------ - -Винаги посочвайте само с този запис: - -- `[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] - -Използвайте напълно квалифицирани имена само при първото споменаване. За следващи връзки използвайте опростено име: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Връзки към PHP документацията ------------------------------ - -- `[php:substr]` -> [php:substr] - - -Изходен код -=========== - -Блокът с код започва с <code>```lang</code> и завършва с <code>```</code>. Поддържаните езици са `php`, `latte`, `neon`, `html`, `css`, `js` и `sql`. За отстъп винаги използвайте табулатори. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Можете също така да посочите името на файла като <code>```php .{file: ArrayTest.php}</code> и блокът с код ще се рендира по този начин: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Заглавия -======== - -Най-високото заглавие (т.е. името на страницата) подчертайте със звездички (`***`). За разделяне на секции използвайте знаци за равенство (`===`). Заглавията от по-ниско ниво подчертавайте със знаци за равенство (`===`) и след това с тирета (`---`): - -``` -MVC Приложения & презентери -*************************** -... - - -Създаване на връзки -=================== -... - - -Връзки в шаблоните ------------------- -... -``` - - -Рамки и стилове -=============== - -Perex обозначаваме с клас `.[perex]` .[perex] - -Бележка обозначаваме с клас `.[note]` .[note] - -Съвет обозначаваме с клас `.[tip]` .[tip] - -Предупреждение обозначаваме с клас `.[caution]` .[caution] - -По-силно предупреждение обозначаваме с клас `.[warning]` .[warning] - -Номер на версия `.{data-version:2.4.10}` .{data-version:2.4.10} - -Записвайте класовете преди реда: - -``` -.[perex] -Това е perex. -``` - -Моля, имайте предвид, че рамки като `.[tip]` "привличат" очите, следователно се използват за подчертаване, а не за по-малко съществена информация. Затова използвайте ги максимално пестеливо. - - -Съдържание -========== - -Съдържанието (връзките в дясното меню) се генерира автоматично за всички страници, чийто размер надхвърля 4 000 байта, като това поведение по подразбиране може да бъде променено с помощта на [мета таг |#Мета тагове] `{{toc}}`. Текстът, формиращ съдържанието, се взема стандартно директно от текста на заглавията, но с помощта на модификатора `.{toc}` е възможно да се покаже в съдържанието друг текст, което е полезно главно за по-дълги заглавия. - -``` - - -Дълго и интелигентно заглавие .{toc: Произволен друг текст, показан в съдържанието} -=================================================================================== -``` - - -Мета тагове -=========== - -- настройка на собствено име на страницата (в `<title>` и навигацията тип "хлебни трохи") `{{title: Друго име}}` -- пренасочване `{{redirect: pla:cs}}` - виж [#връзки] -- принудително `{{toc}}` или забрана `{{toc: no}}` на автоматичното съдържание (кутийка с връзки към отделните заглавия) - -{{priority: -1}} diff --git a/contributing/el/@home.texy b/contributing/el/@home.texy deleted file mode 100644 index a3a78869e2..0000000000 --- a/contributing/el/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Γίνετε συνεισφέρων στο Nette -**************************** - -.[perex] -Μάθετε πώς μπορείτε να συμμετάσχετε στο open source έργο μας. Εξοικειωθείτε με τις διαδικασίες συνεισφοράς στον πηγαίο κώδικα και την τεκμηρίωση και γίνετε μέλος της κοινότητας των προγραμματιστών που συμμετέχουν ενεργά στη βελτίωση του Nette. - - -**Κώδικας** - -- [Πώς να συνεισφέρετε στον κώδικα; |code] -- [Πρότυπο κωδικοποίησης |coding-standard] - -**Τεκμηρίωση** - -- [Πώς να συνεισφέρετε στην τεκμηρίωση; |documentation] -- [Σύνταξη τεκμηρίωσης |syntax] -- "Επεξεργαστής προεπισκόπησης":https://editor.nette.org diff --git a/contributing/el/@left-menu.texy b/contributing/el/@left-menu.texy deleted file mode 100644 index 8c9742c5a3..0000000000 --- a/contributing/el/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Κώδικας -******* -- [Πώς να συνεισφέρετε στον κώδικα; |code] -- [Πρότυπο κωδικοποίησης |coding-standard] - -Τεκμηρίωση -********** -- [Πώς να συνεισφέρετε στην τεκμηρίωση; |documentation] -- [Σύνταξη τεκμηρίωσης |syntax] -- "Επεξεργαστής προεπισκόπησης":https://editor.nette.org diff --git a/contributing/el/code.texy b/contributing/el/code.texy deleted file mode 100644 index e6c6cc8513..0000000000 --- a/contributing/el/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Πώς να συνεισφέρετε στον κώδικα -******************************* - -.[perex] -Ετοιμάζεστε να συνεισφέρετε στο Nette Framework και χρειάζεστε καθοδήγηση σχετικά με τους κανόνες και τις διαδικασίες; Αυτός ο οδηγός για αρχάριους θα σας δείξει βήμα προς βήμα πώς να συνεισφέρετε αποτελεσματικά στον κώδικα, να εργάζεστε με αποθετήρια (repositories) και να υλοποιείτε αλλαγές. - - -Διαδικασία -========== - -Για να συνεισφέρετε στον κώδικα, είναι απαραίτητο να έχετε λογαριασμό στο [GitHub |https://github.com] και να είστε εξοικειωμένοι με τα βασικά του συστήματος ελέγχου εκδόσεων Git. Αν δεν γνωρίζετε πώς να χρησιμοποιείτε το Git, μπορείτε να ανατρέξετε στον οδηγό [git - the simple guide |https://rogerdudler.github.io/git-guide/] και ενδεχομένως να χρησιμοποιήσετε έναν από τους πολλούς [γραφικούς clients |https://git-scm.com/downloads/guis]. - - -Προετοιμασία περιβάλλοντος και αποθετηρίου ------------------------------------------- - -1) Στο GitHub, δημιουργήστε ένα [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] του αποθετηρίου του [πακέτου |www:packages] που πρόκειται να τροποποιήσετε. -2) [Κλωνοποιήστε |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] αυτό το αποθετήριο στον υπολογιστή σας. -3) Εγκαταστήστε τις εξαρτήσεις, συμπεριλαμβανομένου του [Nette Tester |tester:], χρησιμοποιώντας την εντολή `composer install`. -4) Ελέγξτε ότι οι δοκιμές λειτουργούν, εκτελώντας το `composer tester`. -5) Δημιουργήστε έναν [νέο κλάδο |#Νέος Κλάδος] βασισμένο στην τελευταία δημοσιευμένη έκδοση. - - -Υλοποίηση των δικών σας αλλαγών -------------------------------- - -Τώρα μπορείτε να πραγματοποιήσετε τις δικές σας τροποποιήσεις στον κώδικα: - -1) Προγραμματίστε τις απαιτούμενες αλλαγές και μην ξεχάσετε τις δοκιμές. -2) Βεβαιωθείτε ότι οι δοκιμές εκτελούνται επιτυχώς, χρησιμοποιώντας το `composer tester`. -3) Ελέγξτε αν ο κώδικας πληροί τα [#πρότυπα κωδικοποίησης]. -4) Αποθηκεύστε τις αλλαγές (commit) με περιγραφή σε [αυτή τη μορφή |#Περιγραφή του Commit]. - -Μπορείτε να δημιουργήσετε πολλαπλά commits, ένα για κάθε λογικό βήμα. Κάθε commit θα πρέπει να έχει νόημα από μόνο του. - - -Υποβολή των αλλαγών -------------------- - -Μόλις είστε ικανοποιημένοι με τις αλλαγές, μπορείτε να τις υποβάλετε: - -1) Στείλτε (push) τις αλλαγές στο GitHub στο δικό σας fork. -2) Από εκεί, υποβάλετέ τις στο αποθετήριο του Nette δημιουργώντας ένα [pull request |https://help.github.com/articles/creating-a-pull-request] (PR). -3) Παρέχετε [επαρκείς πληροφορίες |#Περιγραφή του Pull Request] στην περιγραφή. - - -Ενσωμάτωση σχολίων ------------------- - -Τα commits σας θα είναι πλέον ορατά και σε άλλους. Είναι σύνηθες να λαμβάνετε σχόλια με παρατηρήσεις: - -1) Παρακολουθήστε τις προτεινόμενες τροποποιήσεις. -2) Ενσωματώστε τις ως νέα commits ή [συγχωνεύστε τα με τα προηγούμενα |https://help.github.com/en/github/using-git/about-git-rebase]. -3) Στείλτε ξανά τα commits στο GitHub, και θα εμφανιστούν αυτόματα στο pull request. - -Ποτέ μην δημιουργείτε νέο pull request για την τροποποίηση ενός υπάρχοντος. - - -Τεκμηρίωση ----------- - -Αν αλλάξατε τη λειτουργικότητα ή προσθέσατε νέα, μην ξεχάσετε να την [προσθέσετε και στην τεκμηρίωση |documentation]. - - -Νέος Κλάδος -=========== - -Αν είναι δυνατόν, πραγματοποιήστε τις αλλαγές έναντι της τελευταίας δημοσιευμένης έκδοσης, δηλαδή του τελευταίου tag στον συγκεκριμένο κλάδο. Για το tag `v3.2.1`, δημιουργείτε έναν κλάδο με αυτή την εντολή: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Πρότυπα Κωδικοποίησης -===================== - -Ο κώδικάς σας πρέπει να πληροί τα [πρότυπα κωδικοποίησης |coding-standard] που χρησιμοποιούνται στο Nette Framework. Για τον έλεγχο και τη διόρθωση του κώδικα είναι διαθέσιμο ένα αυτόματο εργαλείο. Μπορεί να εγκατασταθεί μέσω Composer **global** στον φάκελο της επιλογής σας: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Τώρα θα πρέπει να μπορείτε να εκτελέσετε το εργαλείο στο τερματικό. Με την πρώτη εντολή ελέγχετε και με τη δεύτερη διορθώνετε τον κώδικα στους φακέλους `src` και `tests` στον τρέχοντα κατάλογο: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Περιγραφή του Commit -==================== - -Στο Nette, τα θέματα των commits έχουν τη μορφή: `Presenter: fixed AJAX detection [Closes #69]` - -- Περιοχή ακολουθούμενη από άνω και κάτω τελεία. -- Σκοπός του commit σε παρελθοντικό χρόνο, αν είναι δυνατόν, ξεκινήστε με τη λέξη: "added (προστέθηκε νέα δυνατότητα)", "fixed (διόρθωση)", "refactored (αλλαγή στον κώδικα χωρίς αλλαγή συμπεριφοράς)", "changed", "removed". -- Αν το commit διακόπτει την προς τα πίσω συμβατότητα, προσθέστε "BC break". -- Πιθανή σύνδεση με το issue tracker όπως `(#123)` ή `[Closes #69]`. -- Μετά το θέμα μπορεί να ακολουθεί μία κενή γραμμή και στη συνέχεια λεπτομερέστερη περιγραφή, συμπεριλαμβανομένων, για παράδειγμα, συνδέσμων στο φόρουμ. - - -Περιγραφή του Pull Request -========================== - -Κατά τη δημιουργία ενός pull request, η διεπαφή του GitHub σας επιτρέπει να εισάγετε έναν τίτλο και μια περιγραφή. Δώστε έναν περιεκτικό τίτλο και στην περιγραφή παρέχετε όσο το δυνατόν περισσότερες πληροφορίες σχετικά με τους λόγους της αλλαγής σας. - -Θα εμφανιστεί επίσης μια επικεφαλίδα, όπου θα καθορίσετε αν πρόκειται για νέα λειτουργία ή διόρθωση σφάλματος και αν μπορεί να προκύψει παραβίαση της προς τα πίσω συμβατότητας (BC break). Αν υπάρχει σχετικό πρόβλημα (issue), αναφερθείτε σε αυτό, ώστε να κλείσει μετά την έγκριση του pull request. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/el/coding-standard.texy b/contributing/el/coding-standard.texy deleted file mode 100644 index 6454785629..0000000000 --- a/contributing/el/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Πρότυπο Κωδικοποίησης -********************* - -.[perex] -Αυτό το έγγραφο περιγράφει τους κανόνες και τις συστάσεις για την ανάπτυξη του Nette. Κατά τη συνεισφορά κώδικα στο Nette, πρέπει να τους τηρείτε. Ο ευκολότερος τρόπος για να το κάνετε αυτό είναι να μιμηθείτε τον υπάρχοντα κώδικα. Στόχος είναι όλος ο κώδικας να φαίνεται σαν να τον έγραψε ένα άτομο. - -Το Nette Coding Standard αντιστοιχεί στο [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] με δύο κύριες εξαιρέσεις: για την εσοχή χρησιμοποιεί [tabs αντί για κενά |#Tabulators αντί για Κενά] και για τις [σταθερές κλάσεων χρησιμοποιεί PascalCase |https://blog.nette.org/el/for-less-screaming-in-the-code]. - - -Γενικοί Κανόνες -=============== - -- Κάθε αρχείο PHP πρέπει να περιέχει `declare(strict_types=1)`. -- Δύο κενές γραμμές χρησιμοποιούνται για τον διαχωρισμό μεθόδων για καλύτερη αναγνωσιμότητα. -- Ο λόγος χρήσης του τελεστή σίγασης (@) πρέπει να τεκμηριώνεται: `@mkdir($dir); // @ - ο κατάλογος μπορεί ήδη να υπάρχει`. -- Αν χρησιμοποιείται τελεστής σύγκρισης με αδύναμη τυποποίηση (δηλ. `==`, `!=`, ...), πρέπει να τεκμηριώνεται η πρόθεση: `// == αποδοχή null`. -- Σε ένα αρχείο `exceptions.php` μπορείτε να γράψετε πολλαπλές εξαιρέσεις. -- Στα interfaces δεν καθορίζεται η ορατότητα των μεθόδων, επειδή είναι πάντα public. -- Κάθε ιδιότητα, τιμή επιστροφής και παράμετρος πρέπει να έχει δηλωμένο τύπο. Αντίθετα, στις τελικές σταθερές δεν δηλώνουμε ποτέ τον τύπο, επειδή είναι προφανής. -- Για τον οριοθέτηση ενός string θα πρέπει να χρησιμοποιούνται απλά εισαγωγικά, εκτός από τις περιπτώσεις όπου το ίδιο το literal περιέχει αποστρόφους. - - -Συμβάσεις Ονοματοδοσίας -======================= - -- Μην χρησιμοποιείτε συντομογραφίες, εκτός αν το πλήρες όνομα είναι πολύ μεγάλο. -- Για διγράμματες συντομογραφίες χρησιμοποιήστε κεφαλαία γράμματα, για μεγαλύτερες συντομογραφίες PascalCase/camelCase. -- Για το όνομα της κλάσης χρησιμοποιήστε ουσιαστικό ή φράση ουσιαστικού. -- Τα ονόματα των κλάσεων πρέπει να περιέχουν όχι μόνο την εξειδίκευση (`Array`), αλλά και τη γενικότητα (`ArrayIterator`). Εξαίρεση αποτελούν τα attributes της γλώσσας PHP. -- "Οι σταθερές κλάσεων και τα enums πρέπει να χρησιμοποιούν PascalCase":https://blog.nette.org/el/for-less-screaming-in-the-code. -- "Τα Interfaces και οι abstract κλάσεις δεν πρέπει να περιέχουν προθέματα ή επιθήματα":https://blog.nette.org/el/prefixes-and-suffixes-do-not-belong-in-interface-names όπως `Abstract`, `Interface` ή `I`. - - -Αναδίπλωση και Άγκιστρα -======================= - -Το Nette Coding Standard αντιστοιχεί στο PSR-12 (ή PER Coding Style), σε ορισμένα σημεία το συμπληρώνει ή το τροποποιεί: - -- Οι arrow functions γράφονται χωρίς κενό πριν την παρένθεση, δηλ. `fn($a) => $b`. -- Δεν απαιτείται κενή γραμμή μεταξύ διαφορετικών τύπων `use` import statements. -- Ο τύπος επιστροφής της συνάρτησης/μεθόδου και το αρχικό άγκιστρο `{` είναι πάντα σε ξεχωριστές γραμμές: - -```php - public function find( - string $dir, - array $options, - ): array - { - // σώμα της μεθόδου - } -``` - -Το αρχικό άγκιστρο σε ξεχωριστή γραμμή είναι σημαντικό για τον οπτικό διαχωρισμό της υπογραφής της συνάρτησης/μεθόδου από το σώμα. Αν η υπογραφή είναι σε μία γραμμή, ο διαχωρισμός είναι σαφής (εικόνα αριστερά). Αν είναι σε πολλές γραμμές, στο PSR οι υπογραφές και το σώμα συγχωνεύονται (μέση), ενώ στο πρότυπο Nette παραμένουν διαχωρισμένα (δεξιά): - -[* new-line-after.webp *] - - -Μπλοκ Τεκμηρίωσης (phpDoc) -========================== - -Κύριος κανόνας: Ποτέ μην επαναλαμβάνετε καμία πληροφορία που υπάρχει ήδη στην υπογραφή, όπως τον τύπο παραμέτρου ή τον τύπο επιστροφής, χωρίς να προσθέτετε αξία. - -Μπλοκ τεκμηρίωσης για ορισμό κλάσης: - -- Ξεκινά με την περιγραφή της κλάσης. -- Ακολουθεί μια κενή γραμμή. -- Ακολουθούν οι annotations `@property` (ή `@property-read`, `@property-write`), μία μετά την άλλη. Η σύνταξη είναι: annotation, κενό, τύπος, κενό, `$name`. -- Ακολουθούν οι annotations `@method`, μία μετά την άλλη. Η σύνταξη είναι: annotation, κενό, τύπος επιστροφής, κενό, `name(type $param, ...)` . -- Η annotation `@author` παραλείπεται. Η πατρότητα διατηρείται στην ιστορία του πηγαίου κώδικα. -- Μπορούν να χρησιμοποιηθούν οι annotations `@internal` ή `@deprecated`. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Το μπλοκ τεκμηρίωσης για μια ιδιότητα, που περιέχει μόνο την annotation `@var`, θα πρέπει να είναι σε μία γραμμή: - -```php -/** @var string[] */ -private array $name; -``` - -Μπλοκ τεκμηρίωσης για ορισμό μεθόδου: - -- Ξεκινά με μια σύντομη περιγραφή της μεθόδου. -- Καμία κενή γραμμή. -- Annotations `@param` σε ξεχωριστές γραμμές. -- Annotation `@return`. -- Annotations `@throws`, μία μετά την άλλη. -- Μπορούν να χρησιμοποιηθούν οι annotations `@internal` ή `@deprecated`. - -Μετά από κάθε annotation ακολουθεί ένα κενό, εκτός από το `@param`, μετά το οποίο για καλύτερη αναγνωσιμότητα ακολουθούν δύο κενά. - -```php -/** - * Finds a file in directory. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Tabulators αντί για Κενά -======================== - -Οι tabulators έχουν αρκετά πλεονεκτήματα έναντι των κενών: - -- Το μέγεθος της εσοχής μπορεί να προσαρμοστεί στους επεξεργαστές κειμένου και στον "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size. -- Δεν επιβάλλουν στον κώδικα την προτίμηση του χρήστη για το μέγεθος της εσοχής, οπότε ο κώδικας είναι πιο φορητός. -- Μπορούν να γραφτούν με ένα πάτημα πλήκτρου (οπουδήποτε, όχι μόνο σε επεξεργαστές που μετατρέπουν τους tabulators σε κενά). -- Η εσοχή είναι ο σκοπός τους. -- Σέβονται τις ανάγκες των συναδέλφων με προβλήματα όρασης και των τυφλών. - -Χρησιμοποιώντας tabulators στα έργα μας, επιτρέπουμε την προσαρμογή του πλάτους, η οποία μπορεί να φαίνεται περιττή στους περισσότερους ανθρώπους, αλλά είναι απαραίτητη για άτομα με προβλήματα όρασης. - -Για τους τυφλούς προγραμματιστές που χρησιμοποιούν οθόνες Braille, κάθε κενό αντιπροσωπεύει ένα κελί Braille. Αν λοιπόν η προεπιλεγμένη εσοχή είναι 4 κενά, η εσοχή 3ου επιπέδου σπαταλά 12 πολύτιμα κελιά Braille πριν καν αρχίσει ο κώδικας. Σε μια οθόνη 40 κελιών, η οποία χρησιμοποιείται συχνότερα σε φορητούς υπολογιστές, αυτό είναι περισσότερο από το ένα τέταρτο των διαθέσιμων κελιών που σπαταλούνται χωρίς καμία πληροφορία. - - -{{priority: -1}} diff --git a/contributing/el/documentation.texy b/contributing/el/documentation.texy deleted file mode 100644 index 28e360e165..0000000000 --- a/contributing/el/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Πώς να Συνεισφέρετε στην Τεκμηρίωση -*********************************** - -.[perex] -Η συνεισφορά στην τεκμηρίωση είναι μία από τις πιο ωφέλιμες δραστηριότητες, καθώς βοηθάτε άλλους να κατανοήσουν το framework. - - -Πώς να Γράφετε; ---------------- - -Η τεκμηρίωση προορίζεται κυρίως για άτομα που εξοικειώνονται με το θέμα. Επομένως, θα πρέπει να πληροί αρκετά σημαντικά σημεία: - -- Ξεκινήστε από το απλό και το γενικό. Προχωρήστε σε πιο προχωρημένα θέματα μόνο στο τέλος. -- Προσπαθήστε να εξηγήσετε το θέμα όσο το δυνατόν καλύτερα. Δοκιμάστε, για παράδειγμα, να εξηγήσετε πρώτα το θέμα σε έναν συνάδελφο. -- Αναφέρετε μόνο τις πληροφορίες που ο χρήστης πραγματικά χρειάζεται να γνωρίζει για το συγκεκριμένο θέμα. -- Επαληθεύστε ότι οι πληροφορίες σας είναι όντως αληθείς. Δοκιμάστε κάθε κώδικα. -- Να είστε συνοπτικοί - ό,τι γράψετε, συντομεύστε το στο μισό. Και μετά, αν θέλετε, ξανά. -- Χρησιμοποιήστε με φειδώ τα στοιχεία έμφασης κάθε είδους, από έντονα γράμματα μέχρι πλαίσια όπως `.[note]`. -- Στον κώδικα, τηρήστε τα [Πρότυπα Κωδικοποίησης |coding-standard]. - -Εξοικειωθείτε επίσης με τη [σύνταξη |syntax]. Για προεπισκόπηση του άρθρου κατά τη συγγραφή του, μπορείτε να χρησιμοποιήσετε τον [επεξεργαστή με προεπισκόπηση |https://editor.nette.org/]. - - -Γλωσσικές Εκδόσεις ------------------- - -Η κύρια γλώσσα είναι τα Αγγλικά. Οι αλλαγές σας θα πρέπει ιδανικά να γίνονται και στα Αγγλικά. Αν τα Αγγλικά δεν είναι το δυνατό σας σημείο, χρησιμοποιήστε τον [DeepL Translator |https://www.deepl.com/translator] και οι άλλοι θα ελέγξουν το κείμενό σας. - -Η μετάφραση στις άλλες γλώσσες θα γίνει αυτόματα μετά την έγκριση και την τελειοποίηση της τροποποίησής σας. - - -Ασήμαντες Τροποποιήσεις ------------------------ - -Για να συνεισφέρετε στην τεκμηρίωση, είναι απαραίτητο να έχετε λογαριασμό στο [GitHub |https://github.com]. - -Ο ευκολότερος τρόπος για να κάνετε μια μικρή αλλαγή στην τεκμηρίωση είναι να χρησιμοποιήσετε τους συνδέσμους στο τέλος κάθε σελίδας: - -- Το *Εμφάνιση στο GitHub* ανοίγει την πηγαία μορφή της συγκεκριμένης σελίδας στο GitHub. Στη συνέχεια, αρκεί να πατήσετε το κουμπί `E` και μπορείτε να αρχίσετε την επεξεργασία (πρέπει να είστε συνδεδεμένοι στο GitHub). -- Το *Άνοιγμα προεπισκόπησης* ανοίγει τον επεξεργαστή, όπου βλέπετε αμέσως και την τελική οπτική μορφή. - -Επειδή ο [επεξεργαστής με προεπισκόπηση |https://editor.nette.org/] δεν έχει τη δυνατότητα αποθήκευσης αλλαγών απευθείας στο GitHub, είναι απαραίτητο μετά την ολοκλήρωση των τροποποιήσεων να αντιγράψετε το πηγαίο κείμενο στο πρόχειρο (με το κουμπί *Copy to clipboard*) και στη συνέχεια να το επικολλήσετε στον επεξεργαστή στο GitHub. Κάτω από το πεδίο επεξεργασίας υπάρχει μια φόρμα για την υποβολή. Εδώ μην ξεχάσετε να συνοψίσετε και να εξηγήσετε σύντομα τον λόγο της τροποποίησής σας. Μετά την υποβολή δημιουργείται ένα λεγόμενο pull request (PR), το οποίο μπορεί να επεξεργαστεί περαιτέρω. - - -Μεγαλύτερες Τροποποιήσεις -------------------------- - -Πιο κατάλληλο από τη χρήση της διεπαφής του GitHub, είναι να είστε εξοικειωμένοι με τα βασικά της εργασίας με το σύστημα ελέγχου εκδόσεων Git. Αν δεν γνωρίζετε πώς να χρησιμοποιείτε το Git, μπορείτε να δείτε τον οδηγό [git - the simple guide |https://rogerdudler.github.io/git-guide/] και ενδεχομένως να χρησιμοποιήσετε έναν από τους πολλούς [γραφικούς clients |https://git-scm.com/downloads/guis]. - -Τροποποιήστε την τεκμηρίωση με αυτόν τον τρόπο: - -1) Στο GitHub, δημιουργήστε ένα [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] του αποθετηρίου [nette/docs |https://github.com/nette/docs]. -2) [Κλωνοποιήστε |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] αυτό το αποθετήριο στον υπολογιστή σας. -3) Στη συνέχεια, στον [κατάλληλο κλάδο |#Δομή της Τεκμηρίωσης] πραγματοποιήστε τις αλλαγές. -4) Ελέγξτε για περιττά κενά στο κείμενο χρησιμοποιώντας το εργαλείο [Code-Checker |code-checker:]. -5) Αποθηκεύστε τις αλλαγές (commit). -6) Αν είστε ικανοποιημένοι με τις αλλαγές, στείλτε (push) τις στο GitHub στο δικό σας fork. -7) Από εκεί, υποβάλετέ τις στο αποθετήριο `nette/docs` δημιουργώντας ένα [pull request |https://help.github.com/articles/creating-a-pull-request] (PR). - -Είναι σύνηθες να λαμβάνετε σχόλια με παρατηρήσεις. Παρακολουθήστε τις προτεινόμενες αλλαγές και ενσωματώστε τις. Προσθέστε τις προτεινόμενες αλλαγές ως νέα commits και στείλτε τις ξανά στο GitHub. Ποτέ μην δημιουργείτε νέο pull request για την τροποποίηση ενός υπάρχοντος pull request. - - -Δομή της Τεκμηρίωσης --------------------- - -Ολόκληρη η τεκμηρίωση βρίσκεται στο GitHub στο αποθετήριο [nette/docs |https://github.com/nette/docs]. Η τρέχουσα έκδοση βρίσκεται στον κλάδο `master`, ενώ οι παλαιότερες εκδόσεις βρίσκονται σε κλάδους όπως `doc-3.x`, `doc-2.x`. - -Το περιεχόμενο κάθε κλάδου χωρίζεται σε κύριους φακέλους που αντιπροσωπεύουν τις επιμέρους ενότητες της τεκμηρίωσης. Για παράδειγμα, το `application/` αντιστοιχεί στο https://doc.nette.org/cs/application, το `latte/` αντιστοιχεί στο https://latte.nette.org κ.λπ. Κάθε τέτοιος φάκελος περιέχει υποφακέλους που αντιπροσωπεύουν τις γλωσσικές εκδόσεις (`cs`, `en`, ...) και ενδεχομένως τον υποφάκελο `files` με εικόνες, τις οποίες είναι δυνατόν να εισαγάγετε στις σελίδες της τεκμηρίωσης. diff --git a/contributing/el/syntax.texy b/contributing/el/syntax.texy deleted file mode 100644 index 2f46730e28..0000000000 --- a/contributing/el/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Σύνταξη Τεκμηρίωσης -******************* - -Η τεκμηρίωση χρησιμοποιεί Markdown & [σύνταξη Texy |https://texy.nette.org/syntax] με ορισμένες επεκτάσεις. - - -Σύνδεσμοι -========= - -Για εσωτερικούς συνδέσμους χρησιμοποιείται η γραφή σε αγκύλες `[σύνδεσμος]`. Αυτό μπορεί να γίνει είτε με κάθετη γραμμή `[κείμενο συνδέσμου |στόχος συνδέσμου]`, είτε συντομευμένα `[κείμενο συνδέσμου]`, αν ο στόχος είναι ίδιος με το κείμενο (μετά από μετατροπή σε πεζά γράμματα και παύλες): - -- `[Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[link text |Page name]` -> `<a href="/en/page-name">link text</a>` - -Μπορούμε να συνδέσουμε σε άλλη γλωσσική έκδοση ή σε άλλη ενότητα. Ενότητα νοείται η βιβλιοθήκη Nette (π.χ. `forms`, `latte`, κ.λπ.) ή ειδικές ενότητες όπως `best-practices`, `quickstart` κ.λπ.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (ίδια ενότητα, άλλη γλώσσα) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (άλλη ενότητα, ίδια γλώσσα) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (άλλη ενότητα και γλώσσα) - -Με τη χρήση του `#` είναι επίσης δυνατό να στοχεύσουμε σε μια συγκεκριμένη επικεφαλίδα στη σελίδα. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (επικεφαλίδα στην τρέχουσα σελίδα) -- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` - -Σύνδεσμος στην αρχική σελίδα της ενότητας: (`@home` είναι μια ειδική έκφραση για την αρχική σελίδα της ενότητας) - -- `[link text |@home]` -> `<a href="/en/">link text</a>` -- `[link text |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` - - -Σύνδεσμοι στην Τεκμηρίωση API ------------------------------ - -Πάντα να τους αναφέρετε μόνο χρησιμοποιώντας αυτή τη γραφή: - -- `[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] - -Χρησιμοποιήστε πλήρως προσδιορισμένα ονόματα μόνο στην πρώτη αναφορά. Για επόμενους συνδέσμους χρησιμοποιήστε το απλοποιημένο όνομα: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Σύνδεσμοι στην Τεκμηρίωση PHP ------------------------------ - -- `[php:substr]` -> [php:substr] - - -Πηγαίος Κώδικας -=============== - -Ένα μπλοκ κώδικα ξεκινά με <code>```lang</code> και τελειώνει με <code>```</code>. Οι υποστηριζόμενες γλώσσες είναι `php`, `latte`, `neon`, `html`, `css`, `js` και `sql`. Για την εσοχή χρησιμοποιείτε πάντα tabulators. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Μπορείτε επίσης να αναφέρετε το όνομα του αρχείου ως <code>```php .{file: ArrayTest.php}</code> και το μπλοκ κώδικα θα αποδοθεί με αυτόν τον τρόπο: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Επικεφαλίδες -============ - -Την υψηλότερη επικεφαλίδα (δηλαδή τον τίτλο της σελίδας) υπογραμμίστε την με αστερίσκους (`***`). Για τον διαχωρισμό ενοτήτων χρησιμοποιήστε ίσον (`===`). Τις υπόλοιπες επικεφαλίδες υπογραμμίστε τις με ίσον (`===`) και στη συνέχεια με παύλες (`---`): - -``` -Εφαρμογές MVC & Presenters -************************** -... - - -Δημιουργία Συνδέσμων -==================== -... - - -Σύνδεσμοι στα Templates ------------------------ -... -``` - - -Πλαίσια και Στυλ -================ - -Το perex το επισημαίνουμε με την κλάση `.[perex]` .[perex] - -Τη σημείωση την επισημαίνουμε με την κλάση `.[note]` .[note] - -Τη συμβουλή την επισημαίνουμε με την κλάση `.[tip]` .[tip] - -Την προειδοποίηση την επισημαίνουμε με την κλάση `.[caution]` .[caution] - -Μια πιο έντονη προειδοποίηση την επισημαίνουμε με την κλάση `.[warning]` .[warning] - -Αριθμός έκδοσης `.{data-version:2.4.10}` .{data-version:2.4.10} - -Γράψτε τις κλάσεις πριν από τη γραμμή: - -``` -.[perex] -Αυτό είναι το perex. -``` - -Παρακαλούμε λάβετε υπόψη ότι τα πλαίσια όπως το `.[tip]` "τραβούν" τα μάτια, επομένως χρησιμοποιούνται για έμφαση, όχι για λιγότερο σημαντικές πληροφορίες. Γι' αυτό χρησιμοποιήστε τα με τη μέγιστη φειδώ. - - -Πίνακας Περιεχομένων -==================== - -Ο πίνακας περιεχομένων (σύνδεσμοι στο δεξί μενού) δημιουργείται αυτόματα για όλες τις σελίδες των οποίων το μέγεθος υπερβαίνει τα 4.000 bytes. Αυτή η προεπιλεγμένη συμπεριφορά μπορεί να τροποποιηθεί χρησιμοποιώντας τα [#meta tags] `{{toc}}`. Το κείμενο που αποτελεί τα περιεχόμενα λαμβάνεται συνήθως απευθείας από το κείμενο των επικεφαλίδων, αλλά με τον τροποποιητή `.{toc}` είναι δυνατό να εμφανιστεί στα περιεχόμενα διαφορετικό κείμενο, πράγμα που είναι χρήσιμο κυρίως για μακροσκελείς επικεφαλίδες. - -``` - - -Μακροσκελής και Έξυπνη Επικεφαλίδα .{toc: Οποιοδήποτε άλλο κείμενο εμφανίζεται στα περιεχόμενα} -=============================================================================================== -``` - - -Meta Tags -========= - -- Ορισμός προσαρμοσμένου τίτλου σελίδας (στο `<title>` και στην πλοήγηση breadcrumb) `{{title: Άλλος τίτλος}}` -- Ανακατεύθυνση `{{redirect: pla:cs}}` - βλ. [#Σύνδεσμοι] -- Επιβολή `{{toc}}` ή απενεργοποίηση `{{toc: no}}` του αυτόματου πίνακα περιεχομένων (πλαίσιο με συνδέσμους στις επιμέρους επικεφαλίδες) - -{{priority: -1}} diff --git a/contributing/hu/@home.texy b/contributing/hu/@home.texy deleted file mode 100644 index ea18ed53fa..0000000000 --- a/contributing/hu/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Legyen Ön is Nette hozzájáruló -****************************** - -.[perex] -Tudja meg, hogyan vehet részt nyílt forráskódú projektünkben. Sajátítsa el a forráskódhoz és a dokumentációhoz való hozzájárulás eljárásait, és váljon a Nette fejlesztését aktívan segítő fejlesztői közösség részévé. - - -**Kód** - -- [Hogyan járulhat hozzá a kódhoz? |code] -- [Kódolási szabvány |coding-standard] - -**Dokumentáció** - -- [Hogyan járulhat hozzá a dokumentációhoz? |documentation] -- [Dokumentációs szintaxis |syntax] -- "Előnézeti szerkesztő":https://editor.nette.org diff --git a/contributing/hu/@left-menu.texy b/contributing/hu/@left-menu.texy deleted file mode 100644 index 0775d11f06..0000000000 --- a/contributing/hu/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Kód -*** -- [Hogyan járulhat hozzá a kódhoz? |code] -- [Kódolási szabvány |coding-standard] - -Dokumentáció -************ -- [Hogyan járulhat hozzá a dokumentációhoz? |documentation] -- [Dokumentációs szintaxis |syntax] -- "Előnézeti szerkesztő":https://editor.nette.org diff --git a/contributing/hu/code.texy b/contributing/hu/code.texy deleted file mode 100644 index 97a520de2b..0000000000 --- a/contributing/hu/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Hogyan járuljunk hozzá a kódhoz -******************************* - -.[perex] -Készülsz hozzájárulni a Nette Frameworkhöz, és szükséged van eligazodásra a szabályokban és eljárásokban? Ez a kezdőknek szóló útmutató lépésről lépésre megmutatja, hogyan járulhatsz hozzá hatékonyan a kódhoz, hogyan dolgozz a repository-kkal és hogyan implementáld a változtatásokat. - - -Eljárás -======= - -A kódhoz való hozzájáruláshoz elengedhetetlen egy [GitHub |https://github.com] fiók és a Git verziókezelő rendszer alapjainak ismerete. Ha nem ismered a Git használatát, megnézheted a [git - the simple guide |https://rogerdudler.github.io/git-guide/] útmutatót, és esetleg használhatod a számos [grafikus kliens |https://git-scm.com/downloads/guis] egyikét. - - -Környezet és repository előkészítése ------------------------------------- - -1) a GitHubon hozz létre egy [forkot |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] annak a [csomagnak |www:packages] a repository-jából, amelyet módosítani készülsz -2) ezt a repository-t [klónozd |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] a számítógépedre -3) telepítsd a függőségeket, beleértve a [Nette Testert |tester:] is, a `composer install` paranccsal -4) ellenőrizd, hogy a tesztek működnek-e, a `composer tester` futtatásával -5) hozz létre egy [új ágat |#Új ág] az utolsó kiadott verzió alapján - - -Saját változtatások implementálása ----------------------------------- - -Most végrehajthatod a saját kódmódosításaidat: - -1) programozd le a kívánt változtatásokat, és ne feledkezz meg a tesztekről -2) győződj meg róla, hogy a tesztek sikeresen lefutnak, a `composer tester` segítségével -3) ellenőrizd, hogy a kód megfelel-e a [kódolási szabványnak |#Kódolási szabványok] -4) mentsd el a változtatásokat (commitold) egy leírással [ebben a formátumban |#Commit leírása] - -Létrehozhatsz több commitot, egyet minden logikai lépéshez. Minden commitnak önmagában értelmesnek kell lennie. - - -Változtatások elküldése ------------------------ - -Amint elégedett vagy a változtatásokkal, elküldheted őket: - -1) küldd el (pushold) a változtatásokat a GitHubra a saját forkodba -2) onnan küldd el őket a Nette repository-ba egy [pull request |https://help.github.com/articles/creating-a-pull-request] (PR) létrehozásával -3) adj meg a leírásban [elegendő információt |#Pull request leírása] - - -Észrevételek beépítése ----------------------- - -A commitjaidat most már mások is látni fogják. Gyakori, hogy észrevételeket tartalmazó kommenteket kapsz: - -1) kövesd nyomon a javasolt módosításokat -2) építsd be őket új commitokként, vagy [olvaszd össze őket a korábbiakkal |https://help.github.com/en/github/using-git/about-git-rebase] -3) küldd el újra a commitokat a GitHubra, és automatikusan megjelennek a pull requestben - -Soha ne hozz létre új pull requestet egy meglévő módosítása miatt. - - -Dokumentáció ------------- - -Ha megváltoztattad a funkcionalitást vagy újat adtál hozzá, ne felejtsd el [hozzáadni a dokumentációhoz |documentation] is. - - -Új ág -===== - -Ha lehetséges, a változtatásokat az utolsó kiadott verzióhoz képest végezd, azaz az adott ág utolsó tagjéhez. A `v3.2.1` taghez ezzel a paranccsal hozhatsz létre ágat: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Kódolási szabványok -=================== - -A kódodnak meg kell felelnie a Nette Frameworkben használt [kódolási szabványnak |coding-standard]. A kód ellenőrzésére és javítására rendelkezésre áll egy automatikus eszköz. Telepíthető a Composer segítségével **globálisan** egy általad választott mappába: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Most már képesnek kell lenned futtatni az eszközt a terminálban. Az első parancs ellenőrzi, a második pedig javítja is a kódot az `src` és `tests` mappákban az aktuális könyvtárban: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Commit leírása -============== - -A Nette-ben a commit tárgyak formátuma: `Presenter: fixed AJAX detection [Closes #69]` - -- terület, amelyet kettőspont követ -- a commit célja múlt időben, ha lehetséges, kezdődjön a következő szavakkal: `added` (új funkció hozzáadva), `fixed` (javítás), `refactored` (kódváltozás viselkedésváltozás nélkül), `changed`, `removed` -- ha a commit megszakítja a visszamenőleges kompatibilitást, add hozzá a "BC break" jelzést -- esetleges kapcsolat az issue trackerrel, mint `(#123)` vagy `[Closes #69]` -- a tárgy után következhet egy üres sor, majd részletesebb leírás, beleértve például a fórumra mutató linkeket - - -Pull request leírása -==================== - -Pull request létrehozásakor a GitHub felülete lehetővé teszi egy név és leírás megadását. Adj meg egy kifejező nevet, és a leírásban adj meg minél több információt a változtatásod okairól. - -Megjelenik egy fejléc is, ahol meg kell adnod, hogy új funkcióról vagy hibajavításról van-e szó, és hogy okozhat-e visszamenőleges kompatibilitási törést (BC break). Ha van kapcsolódó probléma (issue), hivatkozz rá, hogy a pull request jóváhagyása után lezárásra kerüljön. - -``` -- bug fix / new feature? <!-- #issue számok, ha vannak --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- nagyon szívesen látjuk, lásd https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/hu/coding-standard.texy b/contributing/hu/coding-standard.texy deleted file mode 100644 index 48c1ec05cb..0000000000 --- a/contributing/hu/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Kódolási szabvány -***************** - -.[perex] -Ez a dokumentum leírja a Nette fejlesztésére vonatkozó szabályokat és ajánlásokat. Amikor kódot járulsz hozzá a Nette-hez, be kell tartanod őket. Ennek legegyszerűbb módja a meglévő kód utánzása. A lényeg az, hogy minden kód úgy nézzen ki, mintha egyetlen ember írta volna. - -A Nette Kódolási Szabvány megfelel a [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/]-nak, két fő kivétellel: a behúzáshoz [tabulátorokat használ szóközök helyett |#Tabulátorok szóközök helyett], és az "osztály konstansokhoz PascalCase-t használ":https://blog.nette.org/hu/for-less-screaming-in-the-code. - - -Általános szabályok -=================== - -- Minden PHP fájlnak tartalmaznia kell a `declare(strict_types=1)` deklarációt. -- Két üres sort használunk a metódusok elválasztására a jobb olvashatóság érdekében. -- A shut-up operátor használatának okát dokumentálni kell: `@mkdir($dir); // @ - a könyvtár létezhet`. -- Ha gyengén típusos összehasonlító operátort használunk (pl. `==`, `!=`, ...), a szándékot dokumentálni kell: `// == elfogadja a null-t` -- Egy `exceptions.php` fájlba több kivételt is írhatsz. -- Az interfészeknél nem adjuk meg a metódusok láthatóságát, mivel azok mindig public-ok. -- Minden property-nek, visszatérési értéknek és paraméternek meg kell adni a típusát. Ezzel szemben a final konstansoknál soha nem adjuk meg a típust, mert az nyilvánvaló. -- A stringek határolására aposztrófokat kell használni, kivéve, ha maga a literál tartalmaz aposztrófokat. - - -Elnevezési konvenciók -===================== - -- Ne használj rövidítéseket, hacsak a teljes név nem túl hosszú. -- Kétbetűs rövidítéseknél használj nagybetűket, hosszabb rövidítéseknél pascal/camel case-t. -- Az osztály nevéhez használj főnevet vagy szókapcsolatot. -- Az osztályneveknek nemcsak a specifikusságot (`Array`), hanem az általánosságot (`ArrayIterator`) is tartalmazniuk kell. Kivételt képeznek a PHP nyelvi attribútumok. -- "Az osztály konstansoknak és enumoknak PascalCaps-t kell használniuk":https://blog.nette.org/hu/for-less-screaming-in-the-code. -- "Az interfészeknek és absztrakt osztályoknak nem szabad előtagokat vagy utótagokat tartalmazniuk":https://blog.nette.org/hu/prefixes-and-suffixes-do-not-belong-in-interface-names, mint például `Abstract`, `Interface` vagy `I`. - - -Tördelés és zárójelek -===================== - -A Nette Kódolási Szabvány megfelel a PSR-12-nek (illetve a PER Coding Style-nak), néhány pontban kiegészíti vagy módosítja azt: - -- az arrow függvényeket szóköz nélkül írjuk a zárójel előtt, azaz `fn($a) => $b` -- nem szükséges üres sor a különböző típusú `use` import utasítások között -- a függvény/metódus visszatérési típusa és a nyitó kapcsos zárójel mindig külön sorokban vannak: - -```php - public function find( - string $dir, - array $options, - ): array - { - // metódus törzse - } -``` - -A nyitó kapcsos zárójel külön sorban fontos a függvény/metódus szignatúrájának és törzsének vizuális elválasztásához. Ha a szignatúra egy sorban van, az elválasztás egyértelmű (bal oldali kép), ha több sorban van, a PSR-ben a szignatúra és a törzs egybefolyik (középen), míg a Nette szabványban továbbra is elkülönülnek (jobbra): - -[* new-line-after.webp *] - - -Dokumentációs blokkok (phpDoc) -============================== - -Fő szabály: Soha ne duplikálj semmilyen információt a szignatúrában, mint például a paraméter típusa vagy a visszatérési típus, hozzáadott érték nélkül. - -Dokumentációs blokk egy osztály definíciójához: - -- Az osztály leírásával kezdődik. -- Üres sor következik. -- Az `@property` (vagy `@property-read`, `@property-write`) annotációk következnek, egymás után. Szintaxis: annotáció, szóköz, típus, szóköz, $név. -- Az `@method` annotációk következnek, egymás után. Szintaxis: annotáció, szóköz, visszatérési típus, szóköz, név(típus $param, ...). -- Az `@author` annotációt kihagyjuk. A szerzőiséget a forráskód története őrzi meg. -- Használhatók az `@internal` vagy `@deprecated` annotációk. - -```php -/** - * MIME üzenet rész. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Egy property dokumentációs blokkja, amely csak az `@var` annotációt tartalmazza, egysoros legyen: - -```php -/** @var string[] */ -private array $name; -``` - -Dokumentációs blokk egy metódus definíciójához: - -- Rövid metódusleírással kezdődik. -- Nincs üres sor. -- Az `@param` annotációk külön sorokban. -- Az `@return` annotáció. -- Az `@throws` annotációk, egymás után. -- Használhatók az `@internal` vagy `@deprecated` annotációk. - -Minden annotációt egy szóköz követ, kivéve az `@param`-ot, amelyet a jobb olvashatóság érdekében két szóköz követ. - -```php -/** - * Fájlt keres egy könyvtárban. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Tabulátorok szóközök helyett -============================ - -A tabulátoroknak számos előnyük van a szóközökkel szemben: - -- a behúzás mérete a szerkesztőkben és a "weben":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size testreszabható -- nem erőltetik a kódra a felhasználó behúzásméret-preferenciáját, így a kód jobban hordozható -- egyetlen billentyűleütéssel írhatók (bárhol, nem csak azokban a szerkesztőkben, amelyek a tabulátorokat szóközökre cserélik) -- a behúzás a céljuk -- tiszteletben tartják a látássérült és vak kollégák igényeit - -A tabulátorok használatával projektjeinkben lehetővé tesszük a szélesség testreszabását, ami a legtöbb ember számára feleslegesnek tűnhet, de a látássérültek számára elengedhetetlen. - -A Braille-kijelzőket használó vak programozók számára minden szóköz egy Braille-cellát jelent. Tehát ha az alapértelmezett behúzás 4 szóköz, a 3. szintű behúzás 12 értékes Braille-cellát pazarol el még a kód kezdete előtt. Egy 40 cellás kijelzőn, amelyet a laptopoknál leggyakrabban használnak, ez a rendelkezésre álló cellák több mint negyede, amelyet információ nélkül pazarolnak el. - - -{{priority: -1}} diff --git a/contributing/hu/documentation.texy b/contributing/hu/documentation.texy deleted file mode 100644 index 7ca946186c..0000000000 --- a/contributing/hu/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Hogyan járuljunk hozzá a dokumentációhoz -**************************************** - -.[perex] -A dokumentációhoz való hozzájárulás az egyik leghasznosabb tevékenység, mivel segít másoknak megérteni a keretrendszert. - - -Hogyan írjunk? --------------- - -A dokumentáció elsősorban azoknak szól, akik most ismerkednek a témával. Ezért több fontos pontnak kell megfelelnie: - -- Kezdje az egyszerűtől és általánostól. Csak a végén térjen át a haladóbb témákra. -- Próbálja meg a lehető legjobban elmagyarázni a dolgot. Például próbálja meg először elmagyarázni a témát egy kollégának. -- Csak azokat az információkat közölje, amelyekre a felhasználónak valóban szüksége van az adott témához. -- Ellenőrizze, hogy az információi valóban igazak-e. Minden kódot teszteljen le. -- Legyen tömör - amit ír, rövidítse le a felére. Aztán nyugodtan még egyszer. -- Takarékoskodjon mindenféle kiemeléssel, a félkövér betűktől az olyan keretekig, mint a `.[note]`. -- A kódokban tartsa be a [Kódolási Szabványt |Coding Standard]. - -Sajátítsa el a [szintaxist |syntax] is. A cikk írása közbeni előnézethez használhatja az [előnézeti szerkesztőt |https://editor.nette.org/]. - - -Nyelvi változatok ------------------ - -Az elsődleges nyelv az angol, tehát a változtatásainak csehül és angolul is meg kell lenniük. Ha az angol nem az erőssége, használja a [DeepL Translator |https://www.deepl.com/translator]-t, és a többiek ellenőrzik a szövegét. - -A többi nyelvre történő fordítás automatikusan megtörténik a módosítás jóváhagyása és finomítása után. - - -Apróbb módosítások ------------------- - -A dokumentációhoz való hozzájáruláshoz elengedhetetlen egy [GitHub |https://github.com] fiók. - -A legegyszerűbb módja egy apróbb változtatás végrehajtásának a dokumentációban az, ha kihasználja az egyes oldalak végén található linkeket: - -- *Megjelenítés GitHubon* megnyitja az adott oldal forráskódját a GitHubon. Ezután elég megnyomni az `E` gombot, és elkezdheti a szerkesztést (szükséges bejelentkezni a GitHubra). -- *Előnézet megnyitása* megnyitja a szerkesztőt, ahol rögtön láthatja a végső vizuális megjelenést is. - -Mivel az [előnézeti szerkesztő |https://editor.nette.org/] nem tudja közvetlenül a GitHubra menteni a változtatásokat, a módosítások befejezése után a forrásszöveget a vágólapra kell másolni (a *Copy to clipboard* gombbal), majd beilleszteni a GitHub szerkesztőjébe. A szerkesztőmező alatt található az elküldési űrlap. Itt ne felejtse el röviden összefoglalni és elmagyarázni a módosítás okát. Az elküldés után létrejön egy úgynevezett pull request (PR), amelyet tovább lehet szerkeszteni. - - -Nagyobb módosítások -------------------- - -A GitHub felületének használata helyett célszerűbb tisztában lenni a Git verziókezelő rendszer alapjaival. Ha nem ismeri a Git használatát, megnézheti a [git - the simple guide |https://rogerdudler.github.io/git-guide/] útmutatót, és esetleg használhatja a számos [grafikus kliens |https://git-scm.com/downloads/guis] egyikét. - -A dokumentációt a következő módon szerkessze: - -1) a GitHubon hozzon létre egy [forkot |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] a [nette/docs |https://github.com/nette/docs] repository-ból -2) ezt a repository-t [klónozza |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] a számítógépére -3) ezután a [megfelelő ágban |#Dokumentáció struktúrája] végezze el a változtatásokat -4) ellenőrizze a felesleges szóközöket a szövegben a [Code-Checker |code-checker:] eszközzel -5) mentse el a változtatásokat (commitolja) -6) ha elégedett a változtatásokkal, küldje el (pusholja) őket a GitHubra a saját forkjába -7) onnan küldje el őket a `nette/docs` repository-ba egy [pull request |https://help.github.com/articles/creating-a-pull-request] (PR) létrehozásával - -Gyakori, hogy észrevételeket tartalmazó kommenteket fog kapni. Kövesse nyomon a javasolt változtatásokat, és építse be őket. A javasolt változtatásokat adja hozzá új commitokként, és küldje el újra a GitHubra. Soha ne hozzon létre új pull requestet egy pull request módosítása miatt. - - -Dokumentáció struktúrája ------------------------- - -Az egész dokumentáció a GitHubon található a [nette/docs |https://github.com/nette/docs] repository-ban. Az aktuális verzió a master ágban van, a régebbi verziók olyan ágakban találhatók, mint a `doc-3.x`, `doc-2.x`. - -Minden ág tartalma fő mappákra oszlik, amelyek a dokumentáció egyes területeit képviselik. Például az `application/` megfelel a https://doc.nette.org/hu/application címnek, a `latte/` megfelel a https://latte.nette.org címnek stb. Minden ilyen mappa tartalmaz almappákat, amelyek a nyelvi változatokat (`hu`, `en`, ...) képviselik, és esetleg egy `files` almappát képekkel, amelyeket be lehet illeszteni a dokumentáció oldalaira. diff --git a/contributing/hu/syntax.texy b/contributing/hu/syntax.texy deleted file mode 100644 index 11e46f6417..0000000000 --- a/contributing/hu/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Dokumentációs szintaxis -*********************** - -A dokumentáció Markdown & [Texy szintaxist |https://texy.nette.org/syntax] használ néhány kiterjesztéssel. - - -Linkek -====== - -Belső linkekhez szögletes zárójelekben `[link |odkaz]` írásmódot használunk. Vagy függőleges vonallal elválasztott formában `[link szövege |link célja]`, vagy rövidítve `[link szövege]`, ha a cél megegyezik a szöveggel (kisbetűssé és kötőjelessé alakítás után): - -- `[Page name]` -> `<a href="/hu/page-name">Page name</a>` -- `[link szövege |Page name]` -> `<a href="/hu/page-name">link szövege</a>` - -Hivatkozhatunk más nyelvi változatra vagy más szekcióra. Szekció alatt Nette könyvtárat értünk (pl. `forms`, `latte`, stb.) vagy speciális szekciókat, mint `best-practices`, `quickstart` stb.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (ugyanaz a szekció, más nyelv) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/hu/page-name">Page name</a>` (más szekció, ugyanaz a nyelv) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (más szekció és más nyelv) - -A `#` segítségével egy adott címsorra is lehet célozni az oldalon. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (címsor az aktuális oldalon) -- `[Page name#Heading]` -> `<a href="/hu/page-name#toc-heading">Page name</a>` - -Link a szekció kezdőoldalára: (`@home` egy speciális kifejezés a szekció kezdőoldalára) - -- `[link szövege |@home]` -> `<a href="/hu/">link szövege</a>` -- `[link szövege |tracy:]` -> `<a href="//tracy.nette.org/hu/">link szövege</a>` - - -Linkek az API dokumentációba ----------------------------- - -Mindig csak ezzel az írásmóddal adjuk meg: - -- `[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] - -Teljesen minősített neveket csak az első említéskor használjunk. További hivatkozásokhoz használjunk egyszerűsített nevet: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Linkek a PHP dokumentációba ---------------------------- - -- `[php:substr]` -> [php:substr] - - -Forráskód -========= - -A kódblokk <code>```lang</code>-gal kezdődik és <code>```</code>-gal végződik. Támogatott nyelvek: `php`, `latte`, `neon`, `html`, `css`, `js` és `sql`. A behúzáshoz mindig tabulátorokat használjunk. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Megadhatja a fájlnevet is, mint <code>```php .{file: ArrayTest.php}</code>, és a kódblokk így fog megjelenni: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Címsorok -======== - -A legfelső címsort (azaz az oldal nevét) csillagokkal húzza alá. A szekciók elválasztásához használjon egyenlőségjeleket. A címsorokat egyenlőségjelekkel, majd kötőjelekkel húzza alá: - -``` -MVC Alkalmazások & presenterek -****************************** -... - - -Linkek létrehozása -================== -... - - -Linkek sablonokban ------------------- -... -``` - - -Keretek és stílusok -=================== - -A perexet a `.[perex]` osztállyal jelöljük. .[perex] - -A megjegyzést a `.[note]` osztállyal jelöljük. .[note] - -A tippet a `.[tip]` osztállyal jelöljük. .[tip] - -A figyelmeztetést a `.[caution]` osztállyal jelöljük. .[caution] - -Az erősebb figyelmeztetést a `.[warning]` osztállyal jelöljük. .[warning] - -Verziószám `.{data-version:2.4.10}` .{data-version:2.4.10} - -Az osztályokat a sor elé írja: - -``` -.[perex] -Ez a perex. -``` - -Kérjük, vegye figyelembe, hogy az olyan keretek, mint a `.[tip]`, "vonzzák" a szemet, ezért kiemelésre használják őket, nem pedig kevésbé fontos információkra. Ezért használatukkal maximálisan takarékoskodjon. - - -Tartalomjegyzék -=============== - -A tartalomjegyzék (linkek a jobb oldali menüben) automatikusan generálódik minden olyan oldalhoz, amelynek mérete meghaladja a 4000 bájtot, de ez az alapértelmezett viselkedés módosítható a [#Meta tagek] `{{toc}}` segítségével. A tartalomjegyzéket alkotó szöveg alapértelmezés szerint közvetlenül a címsorok szövegéből származik, de a `.{toc}` módosítóval lehetőség van más szöveg megjelenítésére a tartalomjegyzékben, ami különösen hosszabb címsorok esetén hasznos. - -``` - - -Hosszú és intelligens címsor .{toc: Tetszőleges más szöveg a tartalomjegyzékben} -================================================================================ -``` - - -Meta tagek -========== - -- saját oldalnév beállítása (a `<title>`-ben és a morzsamenüben) `{{title: Másik név}}` -- átirányítás `{{redirect: pla:cs}}` - lásd [#Linkek] -- az automatikus tartalomjegyzék (a linkeket tartalmazó doboz az egyes címsorokra) kényszerítése `{{toc}}` vagy letiltása `{{toc: no}}` - -{{priority: -1}} diff --git a/contributing/pt/@home.texy b/contributing/pt/@home.texy deleted file mode 100644 index a1e6b6466d..0000000000 --- a/contributing/pt/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Torne-se um contribuidor do Nette -********************************* - -.[perex] -Descubra como você pode se envolver em nosso projeto de código aberto. Aprenda os procedimentos para contribuir com o código-fonte e a documentação e faça parte da comunidade de desenvolvedores que participam ativamente no aprimoramento do Nette. - - -**Código** - -- [Como contribuir para o código? |code] -- [Padrão de codificação |coding-standard] - -**Documentação** - -- [Como contribuir para a documentação? |documentation] -- [Sintaxe da documentação |syntax] -- "Editor de pré-visualização":https://editor.nette.org diff --git a/contributing/pt/@left-menu.texy b/contributing/pt/@left-menu.texy deleted file mode 100644 index bca571a642..0000000000 --- a/contributing/pt/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Código -****** -- [Como contribuir para o código? |code] -- [Padrão de codificação |coding-standard] - -Documentação -************ -- [Como contribuir para a documentação? |documentation] -- [Sintaxe da documentação |syntax] -- "Editor de pré-visualização":https://editor.nette.org diff --git a/contributing/pt/code.texy b/contributing/pt/code.texy deleted file mode 100644 index 2a264be5cf..0000000000 --- a/contributing/pt/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Como contribuir para o código -***************************** - -.[perex] -Você está prestes a contribuir para o Nette Framework e precisa se orientar sobre as regras e procedimentos? Este guia para iniciantes mostrará passo a passo como contribuir eficazmente para o código, trabalhar com repositórios e implementar alterações. - - -Procedimento -============ - -Para contribuir para o código, é essencial ter uma conta no [GitHub|https://github.com] e estar familiarizado com os fundamentos do trabalho com o sistema de controle de versão Git. Se você não domina o trabalho com o Git, pode consultar o guia [git - the simple guide |https://rogerdudler.github.io/git-guide/] e, opcionalmente, usar um dos muitos [clientes gráficos |https://git-scm.com/downloads/guis]. - - -Preparação do ambiente e do repositório ---------------------------------------- - -1) No GitHub, crie um [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] do repositório do [pacote |www:packages] que você pretende modificar. -2) [Clone |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] este repositório para o seu computador. -3) Instale as dependências, incluindo o [Nette Tester |tester:], utilizando o comando `composer install`. -4) Verifique se os testes funcionam executando `composer tester`. -5) Crie um [#novo branch] baseado na última versão lançada. - - -Implementação das suas próprias alterações ------------------------------------------- - -Agora você pode fazer as suas próprias modificações no código: - -1) Implemente as alterações desejadas e não se esqueça dos testes. -2) Certifique-se de que os testes são executados com sucesso, utilizando `composer tester`. -3) Verifique se o código cumpre os [#padrões de codificação]. -4) Salve (commit) as alterações com uma descrição [neste formato |#Descrição do commit]. - -Você pode criar vários commits, um para cada passo lógico. Cada commit deve ser significativo por si só. - - -Envio das alterações --------------------- - -Assim que estiver satisfeito com as alterações, pode enviá-las: - -1) Envie (push) as alterações para o GitHub no seu fork. -2) A partir daí, envie-as para o repositório Nette criando um [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). -3) Forneça [informações suficientes |#Descrição do pull request] na descrição. - - -Incorporação de comentários ---------------------------- - -Os seus commits serão agora vistos por outros. É comum receber comentários com sugestões: - -1) Acompanhe as modificações propostas. -2) Incorpore-as como novos commits ou [faça rebase com os anteriores |https://help.github.com/en/github/using-git/about-git-rebase]. -3) Envie novamente os commits para o GitHub e eles aparecerão automaticamente no pull request. - -Nunca crie um novo pull request para modificar um existente. - - -Documentação ------------- - -Se você alterou a funcionalidade ou adicionou uma nova, não se esqueça de a [adicionar também à documentação |documentation]. - - -Novo branch -=========== - -Se possível, faça as alterações em relação à última versão lançada, ou seja, a última tag no branch correspondente. Para a tag `v3.2.1`, crie um branch com este comando: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Padrões de Codificação -====================== - -O seu código deve cumprir os [padrões de codificação |coding-standard] utilizados no Nette Framework. Existe uma ferramenta automática disponível para verificar e corrigir o código. Pode ser instalada via Composer **globalmente** na pasta da sua escolha: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Agora você deve conseguir executar a ferramenta no terminal. O primeiro comando verifica e o segundo também corrige o código nas pastas `src` e `tests` no diretório atual: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Descrição do commit -=================== - -No Nette, os assuntos dos commits têm o formato: `Presenter: fixed AJAX detection [Closes #69]` - -- área seguida por dois pontos -- propósito do commit no tempo passado; se possível, comece com uma palavra como: "added" (nova funcionalidade adicionada), "fixed" (correção), "refactored" (alteração no código sem alteração de comportamento), "changed", "removed" -- se o commit quebrar a compatibilidade retroativa, adicione "BC break" -- possível vínculo com o gestor de issues como `(#123)` ou `[Closes #69]` -- após o assunto, pode seguir uma linha em branco e depois uma descrição mais detalhada, incluindo, por exemplo, links para o fórum - - -Descrição do pull request -========================= - -Ao criar um pull request, a interface do GitHub permitirá que você insira um título e uma descrição. Forneça um título conciso e, na descrição, forneça o máximo de informações possível sobre os motivos da sua alteração. - -Também será exibido um cabeçalho onde você especifica se é uma nova funcionalidade ou correção de erro e se pode haver quebra de compatibilidade retroativa (BC break). Se houver um problema relacionado (issue), crie um link para ele para que seja fechado após a aprovação do pull request. - -``` -- bug fix / new feature? <!-- #números das issues, se houver --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- altamente bem-vindo, veja https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/pt/coding-standard.texy b/contributing/pt/coding-standard.texy deleted file mode 100644 index fcc737696b..0000000000 --- a/contributing/pt/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Padrões de Codificação -********************** - -.[perex] -Este documento descreve as regras e recomendações para o desenvolvimento do Nette. Ao contribuir com código para o Nette, você deve segui-las. A forma mais fácil de o fazer é imitar o código existente. O objetivo é fazer com que todo o código pareça ter sido escrito por uma única pessoa. - -Os Padrões de Codificação Nette correspondem ao [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] com duas exceções principais: utiliza [#tabulações em vez de espaços] para indentação e utiliza [PascalCase para constantes de classe|https://blog.nette.org/en/less-noise-in-code]. - - -Regras gerais -============= - -- Cada arquivo PHP deve conter `declare(strict_types=1)` -- Duas linhas em branco são usadas para separar métodos para melhor legibilidade. -- O motivo para usar o operador shut-up (@) deve ser documentado: `@mkdir($dir); // @ - o diretório pode já existir`. -- Se for usado um operador de comparação de tipo fraco (ou seja, `==`, `!=`, ...), a intenção deve ser documentada: `// == aceita null` -- Você pode escrever várias exceções num único arquivo `exceptions.php`. -- A visibilidade do método não é especificada para interfaces, pois são sempre públicas. -- Cada propriedade, valor de retorno e parâmetro deve ter um tipo especificado. Por outro lado, nunca especificamos o tipo para constantes finais (`final const`), pois é óbvio. -- Aspas simples devem ser usadas para delimitar strings, exceto quando o próprio literal contém apóstrofos. - - -Convenções de Nomenclatura -========================== - -- Não use abreviações, a menos que o nome completo seja muito longo. -- Use letras maiúsculas para abreviações de duas letras, Pascal/CamelCase para abreviações mais longas. -- Use um substantivo ou frase nominal para o nome da classe. -- Os nomes das classes devem conter não apenas a especificidade (`Array`), mas também a generalidade (`ArrayIterator`). Exceções são atributos da linguagem PHP. -- "Constantes de classe e enums devem usar PascalCaps":https://blog.nette.org/en/less-noise-in-code. -- "Interfaces e classes abstratas não devem conter prefixos ou sufixos":https://blog.nette.org/pt/prefixes-and-suffixes-do-not-belong-in-interface-names como `Abstract`, `Interface` ou `I`. - - -Quebra de Linha e Chaves -======================== - -Os Padrões de Codificação Nette correspondem ao PSR-12 (ou PER Coding Style), em alguns pontos complementam-no ou modificam-no: - -- arrow functions são escritas sem espaço antes do parêntese, ou seja, `fn($a) => $b` -- não é necessária uma linha em branco entre diferentes tipos de declarações de importação `use` -- o tipo de retorno da função/método e a chave de abertura `{` estão sempre em linhas separadas: - -```php - public function find( - string $dir, - array $options, - ): array - { - // corpo do método - } -``` - -A chave de abertura `{` numa linha separada é importante para a separação visual da assinatura da função/método do corpo. Se a assinatura estiver numa única linha, a separação é clara (imagem à esquerda). Se estiver em várias linhas, no PSR as assinaturas e o corpo fundem-se (meio), enquanto no padrão Nette permanecem separados (direita): - -[* new-line-after.webp *] - - -Blocos de Documentação (phpDoc) -=============================== - -Regra principal: Nunca duplique informações da assinatura, como o tipo do parâmetro ou o tipo de retorno, sem adicionar valor (por exemplo, uma descrição). - -Bloco de documentação para definição de classe: - -- Começa com a descrição da classe. -- Seguido por uma linha em branco. -- Seguem-se as anotações `@property` (ou `@property-read`, `@property-write`), uma por linha. A sintaxe é: anotação, espaço, tipo, espaço, `$nome`. -- Seguem-se as anotações `@method`, uma por linha. A sintaxe é: anotação, espaço, tipo de retorno, espaço, `nome(tipo $param, ...)`. -- A anotação `@author` é omitida. A autoria é mantida no histórico do código-fonte. -- Podem ser usadas as anotações `@internal` ou `@deprecated`. - -```php -/** - * Parte da mensagem MIME. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Um bloco de documentação para uma propriedade, que contém apenas a anotação `@var`, deve ser de linha única: - -```php -/** @var string[] */ -private array $name; -``` - -Bloco de documentação para definição de método: - -- Começa com uma breve descrição do método. -- Sem linha em branco entre a descrição e as anotações. -- Anotações `@param`, uma por linha. -- Anotação `@return`. -- Anotações `@throws`, uma por linha. -- Podem ser usadas as anotações `@internal` ou `@deprecated`. - -Cada anotação (`@return`, `@throws`, etc.) é seguida por um espaço. A exceção é `@param`, que é seguida por dois espaços para melhor legibilidade. - -```php -/** - * Encontra um arquivo no diretório. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Tabulações em Vez de Espaços -============================ - -As tabulações têm várias vantagens sobre os espaços: - -- o tamanho do recuo pode ser ajustado em editores e na "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- não impõem a preferência de tamanho de indentação do utilizador ao código, tornando o código mais portátil -- podem ser digitadas com um único toque de tecla (em qualquer lugar, não apenas em editores que convertem tabulações em espaços) -- a indentação é o seu propósito -- respeitam as necessidades de colegas com deficiência visual e cegos - -Ao usar tabulações nos nossos projetos, permitimos o ajuste da largura, o que pode parecer supérfluo para a maioria das pessoas, mas é essencial para pessoas com deficiência visual. - -Para programadores cegos que usam displays Braille, cada espaço representa uma célula Braille. Portanto, se a indentação padrão for de 4 espaços, uma indentação de 3º nível desperdiça 12 valiosas células Braille antes mesmo do início do código. Num display de 40 células, que é o mais comum em portáteis, isso representa mais de um quarto das células disponíveis sendo desperdiçadas sem qualquer informação. - - -{{priority: -1}} diff --git a/contributing/pt/documentation.texy b/contributing/pt/documentation.texy deleted file mode 100644 index 4cf9f50cbb..0000000000 --- a/contributing/pt/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Como Contribuir para a Documentação -*********************************** - -.[perex] -Contribuir para a documentação é uma das atividades mais gratificantes, pois ajuda outros a entender o framework. - - -Como Escrever? --------------- - -A documentação destina-se principalmente a pessoas que estão a familiarizar-se com o tópico. Portanto, deve cumprir vários pontos importantes: - -- Comece pelo simples e geral. Avance para tópicos mais complexos apenas no final. -- Tente explicar o assunto da melhor forma possível. Por exemplo, tente explicar primeiro o tópico a um colega. -- Forneça apenas as informações que o utilizador realmente precisa saber sobre o tópico em questão. -- Verifique se as suas informações são realmente verdadeiras. Teste cada trecho de código. -- Seja conciso - reduza o que escreveu pela metade. E depois, se necessário, novamente. -- Use com moderação todos os tipos de destaque, desde negrito até caixas como `.[note]`. -- No código, siga os [Padrões de Codificação |coding-standard]. - -Aprenda também a [sintaxe |syntax]. Para pré-visualizar o artigo enquanto o escreve, pode usar o [editor com pré-visualização |https://editor.nette.org/]. - - -Versões de Idioma ------------------ - -O idioma principal é o inglês. As suas alterações devem ser, portanto, em inglês. Se o inglês não for o seu forte, use o [DeepL Translator |https://www.deepl.com/translator] e outros irão rever o seu texto. - -A tradução para outros idiomas será feita automaticamente após a aprovação e ajuste da sua modificação. - - -Modificações Triviais ---------------------- - -Para contribuir para a documentação, é essencial ter uma conta no [GitHub|https://github.com]. - -A forma mais fácil de fazer uma pequena alteração na documentação é usar os links no final de cada página: - -- *Mostrar no GitHub* abre a versão do código-fonte da página no GitHub. Depois, basta pressionar o botão `E` para começar a editar (é necessário estar autenticado no GitHub). -- *Abrir pré-visualização* abre o editor, onde pode ver imediatamente a aparência visual resultante. - -Como o [editor com pré-visualização |https://editor.nette.org/] não tem a opção de guardar alterações diretamente no GitHub, é necessário, após concluir as edições, copiar o texto fonte para a área de transferência (botão *Copy to clipboard*) e depois colá-lo no editor do GitHub. Abaixo do campo de edição existe um formulário para envio. Não se esqueça de resumir brevemente e explicar o motivo da sua modificação. Após o envio, é criado um chamado pull request (PR), que pode ser editado posteriormente. - - -Modificações Maiores --------------------- - -Mais adequado do que usar a interface do GitHub é estar familiarizado com os fundamentos do trabalho com o sistema de controlo de versões Git. Se não domina o trabalho com o Git, pode consultar o guia [git - the simple guide |https://rogerdudler.github.io/git-guide/] e, opcionalmente, usar um dos muitos [clientes gráficos |https://git-scm.com/downloads/guis]. - -Edite a documentação desta forma: - -1) No GitHub, crie um [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] do repositório [nette/docs |https://github.com/nette/docs]. -2) [Clone |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] este repositório para o seu computador. -3) Em seguida, no [branch apropriado |#Estrutura da Documentação], faça as alterações. -4) Verifique se há espaços em branco extras no texto usando a ferramenta [Code-Checker |code-checker:]. -5) Salve (commit) as alterações. -6) Se estiver satisfeito com as alterações, envie-as (push) para o GitHub no seu fork. -7) A partir daí, envie-as para o repositório `nette/docs` criando um [pull request|https://help.github.com/articles/creating-a-pull-request] (PR). - -É comum receber comentários com sugestões. Acompanhe as alterações propostas e incorpore-as. Adicione as alterações propostas como novos commits e envie novamente para o GitHub. Nunca crie um novo pull request para modificar um pull request existente. - - -Estrutura da Documentação -------------------------- - -Toda a documentação está localizada no GitHub no repositório [nette/docs |https://github.com/nette/docs]. A versão atual está no branch `master`, versões mais antigas estão localizadas em branches como `doc-3.x`, `doc-2.x`. - -O conteúdo de cada branch é dividido em pastas principais que representam as diferentes áreas da documentação. Por exemplo, `application/` corresponde a `https://doc.nette.org/pt/application`, `latte/` corresponde a `https://latte.nette.org`, etc. Cada uma destas pastas contém subpastas que representam as versões de idioma (`pt`, `en`, `cs`, ...) e, opcionalmente, a subpasta `files` com imagens que podem ser inseridas nas páginas da documentação. diff --git a/contributing/pt/syntax.texy b/contributing/pt/syntax.texy deleted file mode 100644 index e810926f67..0000000000 --- a/contributing/pt/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Sintaxe da Documentação -*********************** - -A documentação usa Markdown e a [sintaxe Texy |https://texy.nette.org/syntax] com algumas extensões. - - -Links -===== - -Para links internos, utiliza-se a notação em colchetes `[...]`. Seja na forma com barra vertical `[texto do link |destino do link]`, ou abreviada `[texto do link]`, se o destino for idêntico ao texto (após transformação para minúsculas e hífens): - -- `[Page name|Page name]` -> `<a href="/en/page-name">Page name</a>` -- `[texto do link |Page name]` -> `<a href="/en/page-name">link text</a>` - -Podemos criar links para uma versão de idioma diferente ou para uma seção diferente. Uma seção significa uma biblioteca Nette (por exemplo, `forms`, `latte`, etc.) ou seções especiais como `best-practices`, `quickstart`, etc.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (mesma seção, idioma diferente) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">Page name</a>` (seção diferente, mesmo idioma) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (seção e idioma diferentes) - -Usando `#`, também é possível direcionar para um título específico na página. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (título na página atual) -- `[Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Page name</a>` - -Link para a página inicial da seção: (`@home` é uma expressão especial para a página inicial da seção) - -- `[texto do link |@home]` -> `<a href="/en/">link text</a>` -- `[texto do link |tracy:]` -> `<a href="//tracy.nette.org/en/">link text</a>` - - -Links para a Documentação da API --------------------------------- - -Utilize sempre apenas esta notação: - -- `[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] - -Use nomes totalmente qualificados apenas na primeira menção. Para links subsequentes, use o nome simplificado: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Links para a Documentação do PHP --------------------------------- - -- `[php:substr]` -> [php:substr] - - -Código-Fonte -============ - -Um bloco de código começa com ` ```lang ` e termina com ` ``` `. Os idiomas suportados são `php`, `latte`, `neon`, `html`, `css`, `js` e `sql`. Use sempre tabulações para a indentação. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Também pode especificar o nome do arquivo como ` ```php .{file: ArrayTest.php} ` e o bloco de código será renderizado desta forma: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Títulos -======= - -Sublinhe o título mais alto (ou seja, o nome da página) com asteriscos (`***`). Use sinais de igual (`===`) para separar secções principais. Sublinhe os títulos de nível inferior com sinais de igual (`===`) e depois com hífens (`---`): - -``` -Aplicações MVC & Presenters -*************************** -... - - -Criação de Links -================ -... - - -Links em Templates ------------------- -... -``` - - -Caixas e Estilos -================ - -Marcamos o perex com a classe `.[perex]` .[perex] - -Marcamos uma nota com a classe `.[note]` .[note] - -Marcamos uma dica com a classe `.[tip]` .[tip] - -Marcamos um aviso com a classe `.[caution]` .[caution] - -Marcamos um aviso mais forte com a classe `.[warning]` .[warning] - -Número da versão `.{data-version:2.4.10}` .{data-version:2.4.10} - -Escreva as classes antes da linha: - -``` -.[perex] -Este é o perex. -``` - -Por favor, esteja ciente de que caixas como `.[tip]` chamam a atenção, portanto, são usadas para enfatizar, e não para informações menos importantes. Use-as com moderação. - - -Sumário -======= - -O sumário (links no menu direito) é gerado automaticamente para todas as páginas cujo tamanho exceda 4 000 bytes. Este comportamento padrão pode ser modificado usando a [meta tag |#Meta Tags] `{{toc}}`. O texto que forma o sumário é retirado por padrão diretamente do texto dos títulos, mas usando o modificador `.{toc}`, é possível exibir um texto diferente no sumário, o que é útil principalmente para títulos mais longos. - -``` - - -Título longo e inteligente .{toc: Qualquer outro texto exibido no sumário} -========================================================================== -``` - - -Meta Tags -========= - -- definir um título de página personalizado (em `<title>` e na navegação breadcrumb) `{{title: Outro título}}` -- redirecionamento `{{redirect: pla:cs}}` - veja [#Links] -- forçar `{{toc}}` ou desabilitar `{{toc: no}}` o sumário automático (caixa com links para títulos individuais) - -{{priority: -1}} diff --git a/contributing/ro/@home.texy b/contributing/ro/@home.texy deleted file mode 100644 index aa47120625..0000000000 --- a/contributing/ro/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Deveniți un contribuitor Nette -****************************** - -.[perex] -Aflați cum vă puteți implica în proiectul nostru open source. Însușiți-vă procedurile pentru contribuția la codul sursă și documentație și deveniți parte a comunității de dezvoltatori care participă activ la îmbunătățirea Nette. - - -**Cod** - -- [Cum să contribuiți la cod? |code] -- [Standard de codificare |coding-standard] - -**Documentație** - -- [Cum să contribuiți la documentație? |documentation] -- [Sintaxa documentației |syntax] -- "Editor de previzualizare":https://editor.nette.org diff --git a/contributing/ro/@left-menu.texy b/contributing/ro/@left-menu.texy deleted file mode 100644 index ddea62842c..0000000000 --- a/contributing/ro/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Cod -*** -- [Cum să contribuiți la cod? |code] -- [Standard de codificare |coding-standard] - -Documentație -************ -- [Cum să contribuiți la documentație? |documentation] -- [Sintaxa documentației |syntax] -- "Editor de previzualizare":https://editor.nette.org diff --git a/contributing/ro/code.texy b/contributing/ro/code.texy deleted file mode 100644 index 16fade07ae..0000000000 --- a/contributing/ro/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Cum să contribuiți la cod -************************* - -.[perex] -Vă pregătiți să contribuiți la Nette Framework și aveți nevoie să vă orientați în reguli și proceduri? Acest ghid pentru începători vă va arăta pas cu pas cum să contribuiți eficient la cod, să lucrați cu depozite și să implementați modificări. - - -Procedura -========= - -Pentru a contribui la cod este necesar să aveți un cont pe [GitHub |https://github.com] și să fiți familiarizat cu elementele de bază ale lucrului cu sistemul de versionare Git. Dacă nu stăpâniți lucrul cu Git, puteți consulta ghidul [git - the simple guide |https://rogerdudler.github.io/git-guide/] și eventual să utilizați unul dintre multele [clienți grafici |https://git-scm.com/downloads/guis]. - - -Pregătirea mediului și a depozitului ------------------------------------- - -1) pe GitHub creați un [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] al depozitului [pachetului |www:packages], pe care urmează să-l modificați -2) [clonați |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] acest depozit pe computerul dvs. -3) instalați dependențele, inclusiv [Nette Tester |tester:], folosind comanda `composer install` -4) verificați dacă testele funcționează, rulând `composer tester` -5) creați o [nouă ramură |#Ramură nouă] bazată pe ultima versiune lansată - - -Implementarea propriilor modificări ------------------------------------ - -Acum puteți efectua propriile modificări de cod: - -1) programați modificările dorite și nu uitați de teste -2) asigurați-vă că testele rulează cu succes, folosind `composer tester` -3) verificați dacă codul respectă [standardul de codificare |#Standarde de codificare] -4) salvați modificările (commit) cu o descriere în [acest format |#Descrierea commit-ului] - -Puteți crea mai multe commit-uri, unul pentru fiecare pas logic. Fiecare commit ar trebui să aibă sens de sine stătător. - - -Trimiterea modificărilor ------------------------- - -Odată ce sunteți mulțumit de modificări, le puteți trimite: - -1) trimiteți (push) modificările pe GitHub în fork-ul dvs. -2) de acolo le trimiteți către depozitul Nette creând un [pull request |https://help.github.com/articles/creating-a-pull-request] (PR) -3) furnizați în descriere [suficiente informații |#Descrierea pull request-ului] - - -Incorporarea comentariilor --------------------------- - -Commit-urile dvs. vor fi acum vizibile și pentru alții. Este obișnuit să primiți comentarii cu observații: - -1) urmăriți modificările propuse -2) încorporați-le ca noi commit-uri sau [combinați-le cu cele anterioare |https://help.github.com/en/github/using-git/about-git-rebase] -3) retrimiteți commit-urile pe GitHub și acestea vor apărea automat în pull request - -Nu creați niciodată un nou pull request pentru a modifica unul existent. - - -Documentație ------------- - -Dacă ați modificat funcționalitatea sau ați adăugat una nouă, nu uitați să o [adăugați și în documentație |documentation]. - - -Ramură nouă -=========== - -Dacă este posibil, efectuați modificările față de ultima versiune lansată, adică ultimul tag din ramura respectivă. Pentru tag-ul `v3.2.1` creați o ramură cu această comandă: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Standarde de codificare -======================= - -Codul dvs. trebuie să respecte [standardul de codificare |coding-standard] utilizat în Nette Framework. Pentru verificarea și corectarea codului este disponibil un instrument automat. Acesta poate fi instalat prin Composer **global** în directorul ales de dvs.: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Acum ar trebui să puteți rula instrumentul în terminal. Prima comandă verifică și a doua corectează codul din directoarele `src` și `tests` din directorul curent: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Descrierea commit-ului -====================== - -În Nette, subiectele commit-urilor au formatul: `Presenter: fixed AJAX detection [Closes #69]` - -- zona urmată de două puncte -- scopul commit-ului la timpul trecut, dacă este posibil, începeți cu cuvântul: "added .(proprietate nouă adăugată)", "fixed .(corecție)", "refactored .(modificare în cod fără schimbarea comportamentului)", changed, removed -- dacă commit-ul întrerupe compatibilitatea inversă, adăugați "BC break" -- eventuală legătură cu issue tracker-ul precum `(#123)` sau `[Closes #69]` -- după subiect poate urma o linie goală și apoi o descriere mai detaliată, inclusiv, de exemplu, linkuri către forum - - -Descrierea pull request-ului -============================ - -La crearea unui pull request, interfața GitHub vă permite să introduceți un titlu și o descriere. Furnizați un titlu descriptiv și în descriere oferiți cât mai multe informații despre motivele modificării dvs. - -Se va afișa și un antet, unde specificați dacă este vorba despre o nouă funcție sau o corecție de eroare și dacă poate apărea o întrerupere a compatibilității inverse (BC break). Dacă există o problemă (issue) asociată, faceți referire la ea, astfel încât să fie închisă după aprobarea pull request-ului. - -``` -- bug fix / new feature? <!-- #issue numbers, if any --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- highly welcome, see https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/ro/coding-standard.texy b/contributing/ro/coding-standard.texy deleted file mode 100644 index a933882b59..0000000000 --- a/contributing/ro/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Standard de codificare -********************** - -.[perex] -Acest document descrie regulile și recomandările pentru dezvoltarea Nette. Atunci când contribuiți cu cod la Nette, trebuie să le respectați. Cea mai simplă modalitate de a face acest lucru este să imitați codul existent. Ideea este ca tot codul să arate ca și cum ar fi fost scris de o singură persoană. - -Standardul de codificare Nette corespunde [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] cu două excepții principale: pentru indentare folosește [#Tabulatori în loc de spații] în loc de spații și pentru [constantele de clasă folosește PascalCase |https://blog.nette.org/en/a-bit-less-screaming-in-code]. - - -Reguli generale -=============== - -- Fiecare fișier PHP trebuie să conțină `declare(strict_types=1)` -- Două rânduri goale sunt folosite pentru a separa metodele pentru o mai bună lizibilitate. -- Motivul utilizării operatorului shut-up trebuie documentat: `@mkdir($dir); // @ - directorul poate exista`. -- Dacă este utilizat un operator de comparație slab tipizat (adică `==`, `!=`, ...), intenția trebuie documentată: `// == acceptă null` -- Într-un singur fișier `exceptions.php` puteți scrie mai multe excepții. -- Pentru interfețe nu se specifică vizibilitatea metodelor, deoarece sunt întotdeauna publice. -- Fiecare proprietate, valoare returnată și parametru trebuie să aibă tipul specificat. În schimb, la constantele finale nu specificăm niciodată tipul, deoarece este evident. -- Pentru delimitarea șirurilor de caractere ar trebui folosite ghilimele simple, cu excepția cazurilor în care literalul însuși conține apostrofuri. - - -Convenții de denumire -===================== - -- Nu utilizați abrevieri, cu excepția cazului în care numele complet este prea lung. -- Pentru abrevierile de două litere utilizați majuscule, pentru abrevierile mai lungi pascal/camel case. -- Pentru numele clasei utilizați un substantiv sau o sintagmă. -- Numele claselor trebuie să conțină nu numai specificitatea (`Array`), ci și generalitatea (`ArrayIterator`). Excepție fac atributele limbajului PHP. -- "Constantele de clasă și enum-urile ar trebui să utilizeze PascalCaps":https://blog.nette.org/en/a-bit-less-screaming-in-code. -- "Interfețele și clasele abstracte nu ar trebui să conțină prefixe sau sufixe":https://blog.nette.org/ro/prefixes-and-suffixes-do-not-belong-in-interface-names precum `Abstract`, `Interface` sau `I`. - - -Wrapping and Braces -=================== - -Standardul de codificare Nette corespunde PSR-12 (respectiv PER Coding Style), în unele puncte îl completează sau îl modifică: - -- funcțiile arrow se scriu fără spațiu înainte de paranteză, adică `fn($a) => $b` -- nu se cere un rând gol între diferite tipuri de `use` import statements -- tipul returnat al funcției/metodei și acolada de deschidere sunt întotdeauna pe rânduri separate: - -```php - public function find( - string $dir, - array $options, - ): array - { - // corpul metodei - } -``` - -Acolada de deschidere pe un rând separat este importantă pentru separarea vizuală a semnăturii funcției/metodei de corp. Dacă semnătura este pe un singur rând, separarea este clară (imaginea din stânga), dacă este pe mai multe rânduri, în PSR semnăturile și corpurile se contopesc (mijloc), în timp ce în standardul Nette sunt în continuare separate (dreapta): - -[* new-line-after.webp *] - - -Blocuri de documentație (phpDoc) -================================ - -Regula principală: Nu duplicați niciodată informații în semnătură, cum ar fi tipul parametrului sau tipul returnat, fără valoare adăugată. - -Blocul de documentație pentru definirea clasei: - -- Începe cu descrierea clasei. -- Urmează un rând gol. -- Urmează adnotările `@property` (sau `@property-read`, `@property-write`), una după alta. Sintaxa este: adnotare, spațiu, tip, spațiu, $nume. -- Urmează adnotările `@method`, una după alta. Sintaxa este: adnotare, spațiu, tip returnat, spațiu, nume(tip $param, ...). -- Adnotarea `@author` se omite. Autoritatea este păstrată în istoricul codului sursă. -- Se pot utiliza adnotările `@internal` sau `@deprecated`. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Blocul de documentație pentru o proprietate, care conține doar adnotarea `@var`, ar trebui să fie pe un singur rând: - -```php -/** @var string[] */ -private array $name; -``` - -Blocul de documentație pentru definirea metodei: - -- Începe cu o scurtă descriere a metodei. -- Niciun rând gol. -- Adnotările `@param` pe rânduri separate. -- Adnotarea `@return`. -- Adnotările `@throws`, una după alta. -- Se pot utiliza adnotările `@internal` sau `@deprecated`. - -După fiecare adnotare urmează un singur spațiu, cu excepția `@param`, după care, pentru o mai bună lizibilitate, urmează două spații. - -```php -/** - * Găsește un fișier în director. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Tabulatori în loc de spații -=========================== - -Tabulatorii au mai multe avantaje față de spații: - -- dimensiunea indentării poate fi personalizată în editoare și pe "web":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- nu impun codului preferința utilizatorului privind dimensiunea indentării, astfel încât codul este mai portabil -- pot fi scrise cu o singură apăsare de tastă (oriunde, nu doar în editoarele care transformă tabulatorii în spații) -- indentarea este scopul lor -- respectă nevoile colegilor cu deficiențe de vedere și nevăzători - -Utilizând tabulatori în proiectele noastre, permitem personalizarea lățimii, ceea ce poate părea o inutilitate pentru majoritatea oamenilor, dar este esențială pentru persoanele cu deficiențe de vedere. - -Pentru programatorii nevăzători care utilizează afișaje Braille, fiecare spațiu reprezintă o celulă Braille. Deci, dacă indentarea implicită este de 4 spații, indentarea de nivel 3 irosește 12 celule Braille valoroase chiar înainte de începutul codului. Pe un afișaj de 40 de celule, care este cel mai frecvent utilizat la laptopuri, aceasta reprezintă mai mult de un sfert din celulele disponibile, care sunt irosite fără nicio informație. - - -{{priority: -1}} diff --git a/contributing/ro/documentation.texy b/contributing/ro/documentation.texy deleted file mode 100644 index c51ce18e62..0000000000 --- a/contributing/ro/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Cum să contribuiți la documentație -********************************** - -.[perex] -Contribuția la documentație este una dintre cele mai benefice activități, deoarece îi ajutați pe alții să înțeleagă framework-ul. - - -Cum să scrieți? ---------------- - -Documentația este destinată în principal persoanelor care se familiarizează cu subiectul. Prin urmare, ar trebui să îndeplinească câteva puncte importante: - -- Începeți de la simplu și general. Treceți la subiecte mai avansate abia la sfârșit. -- Încercați să explicați lucrul cât mai bine posibil. Încercați, de exemplu, să explicați mai întâi subiectul unui coleg. -- Furnizați doar informațiile de care utilizatorul are nevoie cu adevărat pentru subiectul respectiv. -- Verificați dacă informațiile dvs. sunt într-adevăr adevărate. Testați fiecare cod. -- Fiți concis - scurtați ceea ce scrieți la jumătate. Și apoi, dacă este necesar, încă o dată. -- Economisiți evidențiatoarele de orice fel, de la text îngroșat la cadre precum `.[note]`. -- În coduri respectați [Coding Standard |coding-standard]. - -Însușiți-vă și [sintaxa |syntax]. Pentru previzualizarea articolului în timpul scrierii, puteți utiliza [editorul cu previzualizare |https://editor.nette.org/]. - - -Versiuni lingvistice --------------------- - -Limba principală este engleza, modificările dvs. ar trebui deci să fie și în engleză. Dacă engleza nu este punctul dvs. forte, utilizați [DeepL Translator |https://www.deepl.com/translator] și ceilalți vă vor verifica textul. - -Traducerea în celelalte limbi va fi efectuată automat după aprobarea și finisarea modificării dvs. - - -Modificări triviale -------------------- - -Pentru a contribui la documentație este necesar să aveți un cont pe [GitHub |https://github.com]. - -Cel mai simplu mod de a efectua o mică modificare în documentație este să utilizați linkurile de la sfârșitul fiecărei pagini: - -- *Arată pe GitHub* deschide forma sursă a paginii respective pe GitHub. Apoi este suficient să apăsați butonul `E` și puteți începe editarea (este necesar să fiți autentificat pe GitHub). -- *Deschide previzualizarea* deschide editorul, unde vedeți imediat și forma vizuală finală. - -Deoarece [editorul cu previzualizare |https://editor.nette.org/] nu are posibilitatea de a salva modificările direct pe GitHub, este necesar ca după finalizarea modificărilor să copiați textul sursă în clipboard (cu butonul *Copy to clipboard*) și apoi să-l lipiți în editorul de pe GitHub. Sub câmpul de editare se află formularul de trimitere. Aici nu uitați să rezumați pe scurt și să explicați motivul modificării dvs. După trimitere se creează așa-numitul pull request (PR), care poate fi editat ulterior. - - -Modificări mai mari -------------------- - -Mai potrivit decât utilizarea interfeței GitHub este să fiți familiarizat cu elementele de bază ale lucrului cu sistemul de versionare Git. Dacă nu stăpâniți lucrul cu Git, puteți consulta ghidul [git - the simple guide |https://rogerdudler.github.io/git-guide/] și eventual să utilizați unul dintre multele [clienți grafici |https://git-scm.com/downloads/guis]. - -Modificați documentația în acest mod: - -1) pe GitHub creați un [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] al depozitului [nette/docs |https://github.com/nette/docs] -2) [clonați |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] acest depozit pe computerul dvs. -3) apoi în [ramura corespunzătoare |#Structura documentației] efectuați modificările -4) verificați spațiile în exces din text folosind instrumentul [Code-Checker |code-checker:] -4) salvați modificările (commit) -6) dacă sunteți mulțumit de modificări, trimiteți-le (push) pe GitHub în fork-ul dvs. -7) de acolo le trimiteți către depozitul `nette/docs` creând un [pull request |https://help.github.com/articles/creating-a-pull-request] (PR) - -Este obișnuit să primiți comentarii cu observații. Urmăriți modificările propuse și încorporați-le. Adăugați modificările propuse ca noi commit-uri și retrimiteți-le pe GitHub. Nu creați niciodată un nou pull request pentru a modifica unul existent. - - -Structura documentației ------------------------ - -Întreaga documentație este găzduită pe GitHub în depozitul [nette/docs |https://github.com/nette/docs]. Versiunea curentă este în master, versiunile mai vechi sunt plasate în ramuri precum `doc-3.x`, `doc-2.x`. - -Conținutul fiecărei ramuri este împărțit în directoare principale reprezentând domeniile individuale ale documentației. De exemplu, `application/` corespunde https://doc.nette.org/ro/application, `latte/` corespunde https://latte.nette.org etc. Fiecare dintre aceste directoare conține subdirectoare reprezentând versiunile lingvistice (`cs`, `en`, ...) și eventual subdirectorul `files` cu imagini, care pot fi inserate în paginile din documentație. diff --git a/contributing/ro/syntax.texy b/contributing/ro/syntax.texy deleted file mode 100644 index d756e4e2b0..0000000000 --- a/contributing/ro/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Sintaxa documentației -********************* - -Documentația utilizează Markdown & [sintaxa Texy |https://texy.nette.org/syntax] cu unele extensii. - - -Linkuri -======= - -Pentru linkurile interne se utilizează notația în paranteze drepte `[link]`. Fie în forma cu bară verticală `[text link |țintă link]`, fie prescurtat `[text link]`, dacă ținta este identică cu textul (după transformarea în litere mici și cratime): - -- `[Page name]` -> `<a href="/ro/page-name">Page name</a>` -- `[text link |Page name]` -> `<a href="/ro/page-name">text link</a>` - -Putem face link către o altă versiune lingvistică sau către o altă secțiune. Prin secțiune se înțelege o bibliotecă Nette (de ex. `forms`, `latte`, etc.) sau secțiuni speciale precum `best-practices`, `quickstart` etc.: - -- `[cs:Page name]` -> `<a href="/cs/page-name">Page name</a>` (aceeași secțiune, altă limbă) -- `[tracy:Page name]` -> `<a href="//tracy.nette.org/ro/page-name">Page name</a>` (altă secțiune, aceeași limbă) -- `[tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Page name</a>` (altă secțiune și limbă) - -Folosind `#` este de asemenea posibil să țintim un anumit titlu de pe pagină. - -- `[#Heading]` -> `<a href="#toc-heading">Heading</a>` (titlu pe pagina curentă) -- `[Page name#Heading]` -> `<a href="/ro/page-name#toc-heading">Page name</a>` - -Link către pagina de start a secțiunii: (`@home` este o expresie specială pentru pagina de start a secțiunii) - -- `[link text |@home]` -> `<a href="/ro/">link text</a>` -- `[link text |tracy:]` -> `<a href="//tracy.nette.org/ro/">link text</a>` - - -Linkuri către documentația API ------------------------------- - -Specificați întotdeauna doar folosind această notație: - -- `[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] - -Utilizați nume complet calificate doar la prima mențiune. Pentru linkurile ulterioare utilizați numele simplificat: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Linkuri către documentația PHP ------------------------------- - -- `[php:substr]` -> [php:substr] - - -Cod sursă -========= - -Blocul de cod începe cu <code>```lang</code> și se termină cu <code>```</code>. Limbajele suportate sunt `php`, `latte`, `neon`, `html`, `css`, `js` și `sql`. Pentru indentare utilizați întotdeauna tabulatori. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Puteți specifica și numele fișierului ca <code>```php .{file: ArrayTest.php}</code> și blocul de cod se va reda în acest mod: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Titluri -======= - -Titlul cel mai înalt (adică numele paginii) subliniați-l cu asteriscuri. Pentru separarea secțiunilor utilizați semne de egal. Subliniați titlurile cu semne de egal și apoi cu cratime: - -``` -Aplicații MVC & presenteri -************************** -... - - -Crearea linkurilor -================== -... - - -Linkuri în șabloane -------------------- -... -``` - - -Cadre și stiluri -================ - -Perexul îl marcăm cu clasa `.[perex]` .[perex] - -Nota o marcăm cu clasa `.[note]` .[note] - -Sfatul îl marcăm cu clasa `.[tip]` .[tip] - -Avertismentul îl marcăm cu clasa `.[caution]` .[caution] - -Avertismentul mai accentuat îl marcăm cu clasa `.[warning]` .[warning] - -Numărul versiunii `.{data-version:2.4.10}` .{data-version:2.4.10} - -Scrieți clasele înainte de rând: - -``` -.[perex] -Acesta este perexul. -``` - -Vă rugăm să rețineți că cadrele precum `.[tip]` "atrag" ochii, deci se utilizează pentru accentuare, nu pentru informații mai puțin importante. Prin urmare, utilizați-le cu maximă economie. - - -Cuprins -======= - -Cuprinsul (linkurile din meniul din dreapta) este generat automat pentru toate paginile a căror dimensiune depășește 4 000 de octeți, acest comportament implicit putând fi modificat folosind [#Meta tag-uri] `{{toc}}`. Textul care formează cuprinsul este preluat standard direct din textul titlurilor, dar folosind modificatorul `.{toc}` este posibil să se afișeze în cuprins un alt text, ceea ce este util în special pentru titlurile mai lungi. - -``` - - -Titlu lung și inteligent .{toc: Orice alt text afișat în cuprins} -================================================================= -``` - - -Meta tag-uri -============ - -- setarea unui nume personalizat pentru pagină (în `<title>` și navigarea breadcrumb) `{{title: Alt nume}}` -- redirecționare `{{redirect: pla:cs}}` - vezi [#Linkuri] -- forțarea `{{toc}}` sau interzicerea `{{toc: no}}` cuprinsului automat (căsuța cu linkuri către titlurile individuale) - -{{priority: -1}} diff --git a/contributing/sl/@home.texy b/contributing/sl/@home.texy deleted file mode 100644 index 00b95b7b0d..0000000000 --- a/contributing/sl/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Postanite prispevalec k Nette -***************************** - -.[perex] -Ugotovite, kako se lahko vključite v naš odprtokodni projekt. Osvojite postopke za prispevanje k izvorni kodi in dokumentaciji ter postanite del skupnosti razvijalcev, ki aktivno sodelujejo pri izboljševanju Nette. - - -**Koda** - -- [Kako prispevati h kodi? |code] -- [Standard kodiranja |coding-standard] - -**Dokumentacija** - -- [Kako prispevati k dokumentaciji? |documentation] -- [Sintaksa dokumentacije |syntax] -- "Predogledni urejevalnik":https://editor.nette.org diff --git a/contributing/sl/@left-menu.texy b/contributing/sl/@left-menu.texy deleted file mode 100644 index 87eefc02fa..0000000000 --- a/contributing/sl/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Koda -**** -- [Kako prispevati h kodi? |code] -- [Standard kodiranja |coding-standard] - -Dokumentacija -************* -- [Kako prispevati k dokumentaciji? |documentation] -- [Sintaksa dokumentacije |syntax] -- "Predogledni urejevalnik":https://editor.nette.org diff --git a/contributing/sl/code.texy b/contributing/sl/code.texy deleted file mode 100644 index 6bd373891b..0000000000 --- a/contributing/sl/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Kako prispevati h kodi -********************** - -.[perex] -Se pripravljate prispevati k Nette Frameworku in potrebujete orientacijo glede pravil in postopkov? Ta vodnik za začetnike vam bo korak za korakom pokazal, kako učinkovito prispevati h kodi, delati z repozitoriji in implementirati spremembe. - - -Postopek -======== - -Za prispevanje h kodi je nujno imeti račun na [GitHub|https://github.com] in biti seznanjen z osnovami dela z verzijskim sistemom Git. Če ne obvladate dela z Gitom, si lahko ogledate vodnik [git - the simple guide |https://rogerdudler.github.io/git-guide/] in po potrebi uporabite katerega od mnogih [grafičnih klientov |https://git-scm.com/downloads/guis]. - - -Priprava okolja in repozitorija -------------------------------- - -1) na GitHubu si ustvarite [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] repozitorija [paketa |www:packages], ki ga nameravate urejati -2) ta repozitorij [klonirajte |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] na svoj računalnik -3) namestite odvisnosti, vključno z [Nette Testerjem |tester:], z ukazom `composer install` -4) preverite, ali testi delujejo, z zagonom `composer tester` -5) ustvarite si [novo vejo |#Nova veja], ki temelji na zadnji izdani različici - - -Implementacija lastnih sprememb -------------------------------- - -Zdaj lahko izvedete svoje lastne prilagoditve kode: - -1) sprogramirajte zahtevane spremembe in ne pozabite na teste -2) prepričajte se, da testi uspešno potekajo, z uporabo `composer tester` -3) preverite, ali koda ustreza [standardom kodiranja |#Standardi kodiranja] -4) spremembe shranite (commitnite) z opisom v [tem formatu |#Opis commita] - -Lahko ustvarite več commitov, enega za vsak logični korak. Vsak commit bi moral biti smiseln sam po sebi. - - -Pošiljanje sprememb -------------------- - -Ko boste s spremembami zadovoljni, jih lahko pošljete: - -1) pošljite (pushnite) spremembe na GitHub v vaš fork -2) od tam jih pošljite v Nette repozitorij z ustvarjanjem [pull requesta|https://help.github.com/articles/creating-a-pull-request] (PR) -3) v opisu navedite [dovolj informacij |#Opis pull requesta] - - -Vključevanje pripomb --------------------- - -Vaše commite bodo zdaj videli tudi drugi. Običajno je, da boste prejeli komentarje s pripombami: - -1) spremljajte predlagane prilagoditve -2) vključite jih kot nove commite ali jih [združite s prejšnjimi |https://help.github.com/en/github/using-git/about-git-rebase] -3) ponovno pošljite commite na GitHub in samodejno se bodo pojavili v pull requestu - -Nikoli ne ustvarjajte novega pull requesta zaradi urejanja obstoječega. - - -Dokumentacija -------------- - -Če ste spremenili funkcionalnost ali dodali novo, je ne pozabite tudi [dodati v dokumentacijo |documentation]. - - -Nova veja -========= - -Če je mogoče, izvajajte spremembe glede na zadnjo izdano različico, tj. zadnjo oznako (tag) v dani veji. Za oznako `v3.2.1` ustvarite vejo s tem ukazom: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Standardi kodiranja -=================== - -Vaša koda mora ustrezati [standardu kodiranja |coding standard], ki se uporablja v Nette Frameworku. Za preverjanje in popravljanje kode je na voljo samodejno orodje. Lahko ga namestite prek Composerja **globalno** v mapo po vaši izbiri: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Zdaj bi morali imeti možnost zagnati orodje v terminalu. S prvim ukazom preverite in z drugim tudi popravite kodo v mapah `src` in `tests` v trenutnem imeniku: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Opis commita -============ - -V Nette imajo predmeti commitov format: `Presenter: fixed AJAX detection [Closes #69]` - -- področje, ki mu sledi dvopičje -- namen commita v preteklem času, če je mogoče, začnite z besedo: »added« (dodana nova lastnost), »fixed« (popravek), »refactored« (sprememba v kodi brez spremembe obnašanja), changed, removed -- če commit prekine povratno združljivost, dodajte »BC break« -- morebitna povezava z issue trackerjem kot `(#123)` ali `[Closes #69]` -- za subjektom lahko sledi ena prosta vrstica in nato podrobnejši opis, vključno na primer s povezavami na forum - - -Opis pull requesta -================== - -Pri ustvarjanju pull requesta vam vmesnik GitHub omogoča vnos naslova in opisa. Navedite jedrnat naslov in v opisu podajte čim več informacij o razlogih za vašo spremembo. - -Prikazala se bo tudi glava, kjer določite, ali gre za novo funkcijo ali popravek napake in ali lahko pride do prekinitve povratne združljivosti (BC break). Če obstaja povezan problem (issue), se nanj sklicujte, da bo zaprt po odobritvi pull requesta. - -``` -- bug fix / new feature? <!-- #številke issue-jev, če obstajajo --> -- BC break? yes/no -- doc PR: nette/docs#? <!-- zelo dobrodošlo, glej https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/sl/coding-standard.texy b/contributing/sl/coding-standard.texy deleted file mode 100644 index 8b4e2ffb2b..0000000000 --- a/contributing/sl/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Standard kodiranja -****************** - -.[perex] -Ta dokument opisuje pravila in priporočila za razvoj Nette. Pri prispevanju kode k Nette jih morate upoštevati. Najlažji način za to je posnemanje obstoječe kode. Gre za to, da vsa koda izgleda, kot da jo je napisala ena oseba. - -Nette Coding Standard ustreza [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] z dvema glavnima izjemama: za zamikanje uporablja [zavihke namesto presledkov |#Zavihki namesto presledkov] in za [konstante razredov uporablja PascalCase|https://blog.nette.org/sl/for-less-screaming-in-the-code]. - - -Splošna pravila -=============== - -- Vsaka PHP datoteka mora vsebovati `declare(strict_types=1)` -- Dve prazni vrstici se uporabljata za ločevanje metod za boljšo berljivost. -- Razlog za uporabo operatorja za utišanje (shut-up operator) mora biti dokumentiran: `@mkdir($dir); // @ - mapa lahko obstaja`. -- Če je uporabljen šibko tipiziran primerjalni operator (tj. `==`, `!=`, ...), mora biti namen dokumentiran: `// == sprejmi null` -- V eno datoteko `exceptions.php` lahko zapišete več izjem. -- Pri vmesnikih se ne določa vidnost metod, ker so vedno javne. -- Vsaka lastnost, vračana vrednost in parameter morajo imeti naveden tip. Nasprotno pa pri končnih konstantah tipa nikoli ne navajamo, ker je očiten. -- Za omejevanje niza naj se uporabljajo enojni narekovaji, razen v primerih, ko sam literal vsebuje apostrofe. - - -Poimenovalne konvencije -======================= - -- Ne uporabljajte okrajšav, razen če je celotno ime predolgo. -- Pri dvočrkovnih okrajšavah uporabljajte velike črke, pri daljših okrajšavah pascal/camel. -- Za ime razreda uporabljajte samostalnik ali besedno zvezo. -- Imena razredov morajo vsebovati ne samo specifičnost (`Array`), ampak tudi splošnost (`ArrayIterator`). Izjema so atributi jezika PHP. -- "Konstante razredov in enumeracije naj uporabljajo PascalCaps":https://blog.nette.org/sl/for-less-screaming-in-the-code. -- "Vmesniki in abstraktni razredi ne smejo vsebovati predpon ali pripon":https://blog.nette.org/sl/prefixes-and-suffixes-do-not-belong-in-interface-names kot `Abstract`, `Interface` ali `I`. - - -Oblikovanje in oklepaji -======================= - -Nette Coding Standard ustreza PSR-12 (oz. PER Coding Style), v nekaterih točkah ga dopolnjuje ali spreminja: - -- puščične funkcije se pišejo brez presledka pred oklepajem, tj. `fn($a) => $b` -- ne zahteva se prazna vrstica med različnimi tipi `use` import stavkov -- vračani tip funkcije/metode in začetni zaviti oklepaj sta vedno na ločenih vrsticah: - -```php - public function find( - string $dir, - array $options, - ): array - { - // telo metode - } -``` - -Začetni zaviti oklepaj na ločeni vrstici je pomemben za vizualno ločevanje signature funkcije/metode od telesa. Če je signatura na eni vrstici, je ločitev očitna (slika levo), če je na več vrsticah, se v PSR signaturi in telesi zlivata (sredina), medtem ko sta v Nette standardu še naprej ločeni (desno): - -[* new-line-after.webp *] - - -Bloki dokumentacije (phpDoc) -============================ - -Glavno pravilo: Nikoli ne podvajajte nobenih informacij v signaturi, kot je tip parametra ali vračani tip, brez dodane vrednosti. - -Dokumentacijski blok za definicijo razreda: - -- Začne se z opisom razreda. -- Sledi prazna vrstica. -- Sledijo anotacije `@property` (ali `@property-read`, `@property-write`), ena za drugo. Sintaksa je: anotacija, presledek, tip, presledek, $ime. -- Sledijo anotacije `@method`, ena za drugo. Sintaksa je: anotacija, presledek, vračani tip, presledek, ime(tip $param, ...). -- Anotacija `@author` se izpušča. Avtorstvo se hrani v zgodovini izvorne kode. -- Lahko se uporabita anotaciji `@internal` ali `@deprecated`. - -```php -/** - * MIME del sporočila. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Dokumentacijski blok za lastnost, ki vsebuje samo anotacijo `@var`, bi moral biti enovrstičen: - -```php -/** @var string[] */ -private array $name; -``` - -Dokumentacijski blok za definicijo metode: - -- Začne se s kratkim opisom metode. -- Brez prazne vrstice. -- Anotacije `@param` po posameznih vrsticah. -- Anotacija `@return`. -- Anotacije `@throws`, ena za drugo. -- Lahko se uporabita anotaciji `@internal` ali `@deprecated`. - -Vsaki anotaciji sledi en presledek, z izjemo `@param`, za katero za boljšo berljivost sledita dva presledka. - -```php -/** - * Najde datoteko v imeniku. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Zavihki namesto presledkov -========================== - -Zavihki imajo v primerjavi s presledki več prednosti: - -- velikost zamika je mogoče prilagoditi v urejevalnikih in na "spletu":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size -- kodi ne vsiljujejo uporabnikove preference glede velikosti zamika, zato je koda bolje prenosljiva -- lahko jih napišemo z enim pritiskom tipke (kjerkoli, ne samo v urejevalnikih, ki spreminjajo zavihke v presledke) -- zamikanje je njihov namen -- spoštujejo potrebe slabovidnih in slepih kolegov - -Z uporabo zavihkov v naših projektih omogočamo prilagajanje širine, kar se večini ljudi morda zdi nepotrebno, vendar je za ljudi z okvaro vida nujno. - -Za slepe programerje, ki uporabljajo braillove zaslone, vsak presledek predstavlja eno braillovo celico. Če je torej privzeti zamik 4 presledki, zamik 3. stopnje zapravi 12 dragocenih braillovih celic, še preden se koda začne. Na 40-celičnem zaslonu, ki se najpogosteje uporablja pri prenosnikih, je to več kot četrtina razpoložljivih celic, ki so zapravljene brez kakršnekoli informacije. - - -{{priority: -1}} diff --git a/contributing/sl/documentation.texy b/contributing/sl/documentation.texy deleted file mode 100644 index 8ae7191d0c..0000000000 --- a/contributing/sl/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Kako prispevati k dokumentaciji -******************************* - -.[perex] -Prispevanje k dokumentaciji je ena najbolj koristnih dejavnosti, saj pomagate drugim razumeti ogrodje. - - -Kako pisati? ------------- - -Dokumentacija je namenjena predvsem ljudem, ki se s temo seznanjajo. Zato bi morala izpolnjevati nekaj pomembnih točk: - -- Začnite s preprostim in splošnim. K naprednejšim temam preidite šele na koncu. -- Poskusite stvar čim bolje pojasniti. Poskusite na primer temo najprej pojasniti kolegu. -- Navajajte samo tiste informacije, ki jih uporabnik dejansko potrebuje vedeti o dani temi. -- Preverite, ali so vaše informacije resnično pravilne. Vsako kodo preizkusite. -- Bodite jedrnati - kar napišete, skrajšajte na polovico. In potem mirno še enkrat. -- Varčujte z vsemi vrstami poudarkov, od krepke pisave do okvirjev kot `.[note]`. -- V kodah upoštevajte [Standard kodiranja |Coding Standard]. - -Osvojite tudi [sintakso |syntax]. Za predogled članka med pisanjem lahko uporabite [urejevalnik s predogledom |https://editor.nette.org/]. - - -Jezikovne različice -------------------- - -Primarni jezik je angleščina, zato bi morale biti vaše spremembe najprej v angleščini. Če angleščina ni vaša močna stran, uporabite [DeepL Translator |https://www.deepl.com/translator] in drugi vam bodo besedilo preverili. - -Prevod v ostale jezike bo izveden samodejno po odobritvi in dodelavi vaše prilagoditve. - - -Manjše prilagoditve -------------------- - -Za prispevanje k dokumentaciji je nujno imeti račun na [GitHub|https://github.com]. - -Najlažji način za manjšo spremembo v dokumentaciji je uporaba povezav na koncu vsake strani: - -- *Pokaži na GitHubu* odpre izvorno obliko dane strani na GitHubu. Nato samo pritisnite gumb `E` in lahko začnete urejati (potrebno je biti prijavljen na GitHubu). -- *Odpri predogled* odpre urejevalnik, kjer takoj vidite tudi končno vizualno podobo. - -Ker [urejevalnik s predogledom |https://editor.nette.org/] nima možnosti shranjevanja sprememb neposredno na GitHub, je treba po končanem urejanju izvorni tekst kopirati v odložišče (z gumbom *Copy to clipboard*) in ga nato prilepiti v urejevalnik na GitHubu. Pod urejevalnim poljem je obrazec za pošiljanje. Tukaj ne pozabite na kratko povzeti in pojasniti razlog vaše prilagoditve. Po pošiljanju nastane t.i. pull request (PR), ki ga je mogoče nadalje urejati. - - -Večje prilagoditve ------------------- - -Primernejše kot uporaba vmesnika GitHub je biti seznanjen z osnovami dela z verzijskim sistemom Git. Če ne obvladate dela z Gitom, si lahko ogledate vodnik [git - the simple guide |https://rogerdudler.github.io/git-guide/] in po potrebi uporabite katerega od mnogih [grafičnih klientov |https://git-scm.com/downloads/guis]. - -Dokumentacijo urejajte na ta način: - -1) na GitHubu si ustvarite [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] repozitorija [nette/docs |https://github.com/nette/docs] -2) ta repozitorij [klonirajte |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] na svoj računalnik -3) nato v [ustrezni veji |#Struktura dokumentacije] izvedite spremembe -4) preverite odvečne presledke v besedilu z orodjem [Code-Checker |code-checker:] -4) spremembe shranite (commitnite) -6) če ste s spremembami zadovoljni, jih pošljite (pushnite) na GitHub v vaš fork -7) od tam jih pošljite v repozitorij `nette/docs` z ustvarjanjem [pull requesta|https://help.github.com/articles/creating-a-pull-request] (PR) - -Običajno je, da boste prejemali komentarje s pripombami. Spremljajte predlagane spremembe in jih vključite. Predlagane spremembe dodajte kot nove commite in ponovno pošljite na GitHub. Nikoli ne ustvarjajte novega pull requesta zaradi urejanja pull requesta. - - -Struktura dokumentacije ------------------------ - -Celotna dokumentacija je nameščena na GitHubu v repozitoriju [nette/docs |https://github.com/nette/docs]. Trenutna različica je v veji `master`, starejše različice so nameščene v vejah kot `doc-3.x`, `doc-2.x`. - -Vsebina vsake veje se deli na glavne mape, ki predstavljajo posamezna področja dokumentacije. Na primer `application/` ustreza https://doc.nette.org/sl/application, `latte/` ustreza https://latte.nette.org/sl itd. Vsaka ta mapa vsebuje podmape, ki predstavljajo jezikovne različice (`sl`, `en`, ...) in po potrebi podmapo `files` s slikami, ki jih je mogoče vstavljati na strani v dokumentaciji. diff --git a/contributing/sl/syntax.texy b/contributing/sl/syntax.texy deleted file mode 100644 index 55aed5ac18..0000000000 --- a/contributing/sl/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Sintaksa dokumentacije -********************** - -Dokumentacija uporablja Markdown & [sintakso Texy |https://texy.nette.org/syntax] z nekaterimi razširitvami. - - -Povezave -======== - -Za notranje povezave se uporablja zapis v oglatih oklepajih `[povezava]`. In sicer bodisi v obliki z navpičnico `[besedilo povezave |cilj povezave]`, bodisi skrajšano `[besedilo povezave]`, če je cilj enak besedilu (po pretvorbi v male črke in pomišljaje): - -- `[Ime strani |Page name]` -> `<a href="/en/page-name">Ime strani</a>` -- `[besedilo povezave |Page name]` -> `<a href="/en/page-name">besedilo povezave</a>` - -Povezujemo lahko v drugo jezikovno različico ali v drugo sekcijo. Sekcija pomeni Nette knjižnico (npr. `forms`, `latte`, ipd.) ali posebne sekcije kot `best-practices`, `quickstart` itd.: - -- `[cs:Ime strani |cs:Page name]` -> `<a href="/cs/page-name">cs:Ime strani</a>` (ista sekcija, drug jezik) -- `[tracy:Ime strani |tracy:Page name]` -> `<a href="//tracy.nette.org/en/page-name">tracy:Ime strani</a>` (druga sekcija, isti jezik) -- `[tracy:cs:Ime strani |tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">tracy:cs:Ime strani</a>` (druga sekcija in jezik) - -S pomočjo `#` je mogoče tudi ciljati na določen naslov na strani. - -- `[Naslov |#Heading]` -> `<a href="#toc-heading">Naslov</a>` (naslov na trenutni strani) -- `[Ime strani#Naslov |Page name#Heading]` -> `<a href="/en/page-name#toc-heading">Ime strani#Naslov</a>` - -Povezava na uvodno stran sekcije: (`@home` je poseben izraz za domačo stran sekcije) - -- `[besedilo povezave |@home]` -> `<a href="/en/">besedilo povezave</a>` -- `[besedilo povezave |tracy:]` -> `<a href="//tracy.nette.org/en/">besedilo povezave</a>` - - -Povezave do API dokumentacije ------------------------------ - -Vedno navajajte samo s tem zapisom: - -- `[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] - -Popolnoma kvalificirana imena uporabljajte samo ob prvi omembi. Za nadaljnje povezave uporabite poenostavljeno ime: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Povezave do PHP dokumentacije ------------------------------ - -- `[php:substr]` -> [php:substr] - - -Izvorna koda -============ - -Blok kode se začne z <code>```lang</code> in konča z <code>```</code>. Podprti jeziki so `php`, `latte`, `neon`, `html`, `css`, `js` in `sql`. Za zamikanje vedno uporabljajte zavihke. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Lahko tudi navedete ime datoteke kot <code>```php .{file: ArrayTest.php}</code> in blok kode se bo izrisal na ta način: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Naslovi -======= - -Najvišji naslov (torej ime strani) podčrtajte z zvezdicami. Za ločevanje sekcij uporabljajte enačaje. Naslove podčrtajte z enačaji in nato s pomišljaji: - -``` -MVC Aplikacije & presenterji -**************************** -... - - -Ustvarjanje povezav -=================== -... - - -Povezave v predlogah --------------------- -... -``` - - -Okvirji in stili -================ - -Perex označimo z razredom `.[perex]` - -Opombo označimo z razredom `.[note]` - -Nasvet označimo z razredom `.[tip]` - -Opozorilo označimo z razredom `.[caution]` - -Močnejše opozorilo označimo z razredom `.[warning]` - -Številko različice `.{data-version:2.4.10}` - -Razrede zapišite pred vrstico: - -``` -.[perex] -To je perex. -``` - -Zavedajte se prosim, da okvirji kot `.[tip]` »pritegnejo« oči, zato se uporabljajo za poudarjanje, ne pa za manj pomembne informacije. Zato z njihovo uporabo maksimalno varčujte. - - -Vsebina -======= - -Vsebina (povezave v desnem meniju) je samodejno generirana za vse strani, katerih velikost presega 4.000 bajtov, pri čemer je to privzeto obnašanje mogoče prilagoditi s pomočjo [meta oznak |#Meta značke] `{{toc}}`. Besedilo, ki tvori vsebino, se standardno vzame neposredno iz besedila naslovov, vendar je s pomočjo modifikatorja `.{toc}` mogoče v vsebini prikazati drugo besedilo, kar je koristno predvsem za daljše naslove. - -``` - - -Dolg in inteligenten naslov .{toc: Poljubno drugo besedilo, prikazano v vsebini} -================================================================================ -``` - - -Meta značke -=========== - -- nastavitev lastnega imena strani (v `<title>` in drobtinicah) `{{title: Drugo ime}}` -- preusmeritev `{{redirect: pla:cs}}` - glej [#povezave] -- vsiljenje `{{toc}}` ali prepoved `{{toc: no}}` samodejne vsebine (okvirček s povezavami na posamezne naslove) - -{{priority: -1}} diff --git a/contributing/uk/@home.texy b/contributing/uk/@home.texy deleted file mode 100644 index befb74c6c5..0000000000 --- a/contributing/uk/@home.texy +++ /dev/null @@ -1,17 +0,0 @@ -Станьте контриб'ютором Nette -**************************** - -.[perex] -Дізнайтеся, як ви можете долучитися до нашого open source проекту. Ознайомтеся з процедурами внесення внеску у вихідний код та документацію та станьте частиною спільноти розробників, які активно беруть участь у вдосконаленні Nette. - - -**Код** - -- [Як зробити внесок у код? |code] -- [Стандарт кодування |coding-standard] - -**Документація** - -- [Як зробити внесок у документацію? |documentation] -- [Синтаксис документації |syntax] -- "Редактор попереднього перегляду":https://editor.nette.org diff --git a/contributing/uk/@left-menu.texy b/contributing/uk/@left-menu.texy deleted file mode 100644 index 6a23fcbcb1..0000000000 --- a/contributing/uk/@left-menu.texy +++ /dev/null @@ -1,10 +0,0 @@ -Код -*** -- [Як зробити внесок у код? |code] -- [Стандарт кодування |coding-standard] - -Документація -************ -- [Як зробити внесок у документацію? |documentation] -- [Синтаксис документації |syntax] -- "Редактор попереднього перегляду":https://editor.nette.org diff --git a/contributing/uk/code.texy b/contributing/uk/code.texy deleted file mode 100644 index 5e0e04479c..0000000000 --- a/contributing/uk/code.texy +++ /dev/null @@ -1,118 +0,0 @@ -Як зробити внесок у код -*********************** - -.[perex] -Ви збираєтеся зробити внесок у Nette Framework і вам потрібно розібратися в правилах та процедурах? Цей посібник для початківців крок за кроком покаже вам, як ефективно робити внесок у код, працювати з репозиторіями та впроваджувати зміни. - - -Процедура -========= - -Для того, щоб зробити внесок у код, необхідно мати обліковий запис на [GitHub|https://github.com] та бути знайомим з основами роботи з системою контролю версій Git. Якщо ви не володієте роботою з Git, можете ознайомитися з посібником [git - the simple guide |https://rogerdudler.github.io/git-guide/] та, за потреби, скористатися одним з багатьох [графічних клієнтів |https://git-scm.com/downloads/guis]. - - -Підготовка середовища та репозиторію ------------------------------------- - -1) на GitHub створіть [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] репозиторію [пакета |www:packages], який ви збираєтеся змінити -2) цей репозиторій [клонуєте |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] на свій комп'ютер -3) встановіть залежності, включно з [Nette Tester |tester:], за допомогою команди `composer install` -4) перевірте, чи працюють тести, запустивши `composer tester` -5) створіть [нову гілку |#Нова гілка] на основі останньої випущеної версії - - -Реалізація власних змін ------------------------ - -Тепер ви можете внести свої власні зміни до коду: - -1) запрограмуйте необхідні зміни та не забудьте про тести -2) переконайтеся, що тести проходять успішно, за допомогою `composer tester` -3) перевірте, чи код відповідає [стандарту кодування |#Стандарти кодування] -4) збережіть зміни (зробіть коміт) з описом у [цьому форматі |#Опис коміту] - -Ви можете створити кілька комітів, по одному для кожного логічного кроку. Кожен коміт повинен бути осмисленим сам по собі. - - -Надсилання змін ---------------- - -Як тільки ви будете задоволені змінами, можете їх надіслати: - -1) надішліть (push) зміни на GitHub у ваш форк -2) звідти надішліть їх до репозиторію Nette, створивши [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) -3) надайте в описі [достатньо інформації |#Опис pull request] - - -Врахування зауважень --------------------- - -Ваші коміти тепер побачать і інші. Зазвичай ви отримуватимете коментарі із зауваженнями: - -1) слідкуйте за запропонованими змінами -2) врахуйте їх як нові коміти або [об'єднайте з попередніми |https://help.github.com/en/github/using-git/about-git-rebase] -3) знову надішліть коміти на GitHub, і вони автоматично з'являться в pull request - -Ніколи не створюйте новий pull request для зміни існуючого. - - -Документація ------------- - -Якщо ви змінили функціональність або додали нову, не забудьте також [додати це до документації |documentation]. - - -Нова гілка -========== - -Якщо це можливо, вносьте зміни щодо останньої випущеної версії, тобто останнього тегу в даній гілці. Для тегу `v3.2.1` ви створите гілку цією командою: - -```shell -git checkout -b new_branch_name v3.2.1 -``` - - -Стандарти кодування -=================== - -Ваш код повинен відповідати [стандарту кодування |Coding Standard], що використовується в Nette Framework. Для перевірки та виправлення коду доступний автоматичний інструмент. Його можна встановити через Composer **глобально** у вибрану вами папку: - -```shell -composer create-project nette/coding-standard /path/to/nette-coding-standard -``` - -Тепер ви повинні мати можливість запустити інструмент у терміналі. Першою командою ви перевірите, а другою – виправите код у папках `src` та `tests` у поточному каталозі: - -```shell -/path/to/nette-coding-standard/ecs check -/path/to/nette-coding-standard/ecs check --fix -``` - - -Опис коміту -=========== - -У Nette теми комітів мають формат: `Presenter: виправлено виявлення AJAX [Closes #69]` - -- область, за якою слідує двокрапка -- мета коміту в минулому часі, якщо можливо, почніть зі слова: `added` (додано нову властивість), `fixed` (виправлення), `refactored` (зміна в коді без зміни поведінки), `changed`, `removed` -- якщо коміт порушує зворотну сумісність, додайте "BC break" -- можливий зв'язок з трекером проблем, як `(#123)` або `[Closes #69]` -- за темою може слідувати один порожній рядок, а потім детальніший опис, включно з, наприклад, посиланнями на форум - - -Опис pull request -================= - -При створенні pull request інтерфейс GitHub дозволить вам ввести назву та опис. Вкажіть змістовну назву, а в описі надайте якомога більше інформації про причини вашої зміни. - -Також відобразиться заголовок, де вкажіть, чи це нова функція, чи виправлення помилки, і чи може відбутися порушення зворотної сумісності (BC break). Якщо є пов'язана проблема (issue), посилайтеся на неї, щоб її було закрито після схвалення pull request. - -``` -- виправлення помилки / нова функція? <!-- #номери issue, якщо є --> -- BC break? так/ні -- doc PR: nette/docs#? <!-- дуже вітається, див. https://nette.org/en/writing --> -``` - - -{{priority: -1}} diff --git a/contributing/uk/coding-standard.texy b/contributing/uk/coding-standard.texy deleted file mode 100644 index 20e5a5e9a9..0000000000 --- a/contributing/uk/coding-standard.texy +++ /dev/null @@ -1,128 +0,0 @@ -Стандарт кодування -****************** - -.[perex] -Цей документ описує правила та рекомендації для розробки Nette. При внесенні коду до Nette ви повинні їх дотримуватися. Найпростіший спосіб зробити це – наслідувати існуючий код. Мета полягає в тому, щоб весь код виглядав так, ніби його написала одна людина. - -Стандарт кодування Nette відповідає [PSR-12 Extended Coding Style |https://www.php-fig.org/psr/psr-12/] з двома основними винятками: для відступів він використовує [#табуляції замість пробілів] та для [констант класів використовує PascalCase|https://blog.nette.org/uk/for-less-screaming-in-the-code]. - - -Загальні правила -================ - -- Кожен файл PHP повинен містити `declare(strict_types=1)` -- Два порожні рядки використовуються для розділення методів для кращої читабельності. -- Причина використання оператора приглушення помилок (`@`) повинна бути задокументована: `@mkdir($dir); // @ - каталог може існувати`. -- Якщо використовується оператор порівняння зі слабкою типізацією (тобто `==`, `!=`, ...), намір повинен бути задокументований: `// == прийняти null` -- В один файл `exceptions.php` можна записати кілька винятків. -- Для інтерфейсів не вказується видимість методів, оскільки вони завжди публічні. -- Кожна властивість, повернене значення та параметр повинні мати вказаний тип. Навпаки, для фінальних констант тип ніколи не вказуємо, оскільки він очевидний. -- Для обмеження рядка слід використовувати одинарні лапки (`'`), за винятком випадків, коли сам літерал містить апострофи. - - -Угоди про іменування -==================== - -- Не використовуйте скорочення, якщо повна назва не надто довга. -- Для дволітерних скорочень використовуйте великі літери, для довших скорочень – Pascal/camelCase. -- Для назви класу використовуйте іменник або словосполучення. -- Назви класів повинні містити не лише специфічність (`Array`), але й загальність (`ArrayIterator`). Винятком є атрибути мови PHP. -- "Константи класів та enum-и повинні використовувати PascalCase":https://blog.nette.org/uk/for-less-screaming-in-the-code. -- "Інтерфейси та абстрактні класи не повинні містити префіксів або суфіксів":https://blog.nette.org/uk/prefixes-and-suffixes-do-not-belong-in-interface-names як `Abstract`, `Interface` або `I`. - - -Перенесення та фігурні дужки -============================ - -Стандарт кодування Nette відповідає PSR-12 (або PER Coding Style), в деяких пунктах доповнює або змінює його: - -- стрілкові функції пишуться без пробілу перед дужкою, тобто `fn($a) => $b` -- не вимагається порожній рядок між різними типами імпортів `use` -- тип повернення функції/методу та початкова фігурна дужка завжди знаходяться на окремих рядках: - -```php - public function find( - string $dir, - array $options, - ): array - { - // тіло методу - } -``` - -Початкова фігурна дужка на окремому рядку важлива для візуального розділення сигнатури функції/методу від тіла. Якщо сигнатура знаходиться на одному рядку, розділення очевидне (зображення зліва), якщо на кількох рядках, у PSR сигнатури та тіла зливаються (посередині), тоді як у стандарті Nette вони залишаються розділеними (праворуч): - -[* new-line-after.webp *] - - -Блоки документації (phpDoc) -=========================== - -Основне правило: Ніколи не дублюйте жодної інформації в сигнатурі, такої як тип параметра або тип повернення, без доданої вартості. - -Блок документації для визначення класу: - -- Починається з опису класу. -- Слідує порожній рядок. -- Слідують анотації `@property` (або `@property-read`, `@property-write`), одна за одною. Синтаксис: анотація, пробіл, тип, пробіл, `$ім'я`. -- Слідують анотації `@method`, одна за одною. Синтаксис: анотація, пробіл, тип повернення, пробіл, ім'я(тип $param, ...). -- Анотація `@author` пропускається. Авторство зберігається в історії вихідного коду. -- Можна використовувати анотації `@internal` або `@deprecated`. - -```php -/** - * MIME message part. - * - * @property string $encoding - * @property-read array $headers - * @method string getSomething(string $name) - * @method static bool isEnabled() - */ -``` - -Блок документації для властивості, який містить лише анотацію `@var`, повинен бути однорядковим: - -```php -/** @var string[] */ -private array $name; -``` - -Блок документації для визначення методу: - -- Починається з короткого опису методу. -- Жодного порожнього рядка. -- Анотації `@param` по окремих рядках. -- Анотація `@return`. -- Анотації `@throws`, одна за одною. -- Можна використовувати анотації `@internal` або `@deprecated`. - -За кожною анотацією слідує один пробіл, за винятком `@param`, за якою для кращої читабельності слідують два пробіли. - -```php -/** - * Знаходить файл у каталозі. - * @param string[] $options - * @return string[] - * @throws DirectoryNotFoundException - */ -public function find(string $dir, array $options): array -``` - - -Табуляції замість пробілів -========================== - -Табуляції мають кілька переваг перед пробілами: - -- розмір відступу можна налаштувати в редакторах та на `"веб":https://developer.mozilla.org/en-US/docs/Web/CSS/tab-size` -- не нав'язують коду переваги користувача щодо розміру відступу, тому код краще переноситься -- їх можна написати одним натисканням клавіші (будь-де, не тільки в редакторах, які перетворюють табуляції на пробіли) -- відступи – це їхній сенс -- поважають потреби колег з вадами зору та незрячих - -Використовуючи табуляції в наших проектах, ми дозволяємо налаштовувати ширину, що може здатися більшості людей зайвим, але для людей з вадами зору є необхідним. - -Для незрячих програмістів, які використовують брайлівські дисплеї, кожен пробіл представляє одну брайлівську комірку. Якщо стандартний відступ становить 4 пробіли, відступ 3-го рівня марнує 12 цінних брайлівських комірок ще до початку коду. На 40-комірковому дисплеї, який найчастіше використовується для ноутбуків, це більше чверті доступних комірок, які марнуються без будь-якої інформації. - - -{{priority: -1}} diff --git a/contributing/uk/documentation.texy b/contributing/uk/documentation.texy deleted file mode 100644 index 95b53fa278..0000000000 --- a/contributing/uk/documentation.texy +++ /dev/null @@ -1,68 +0,0 @@ -Як зробити внесок у документацію -******************************** - -.[perex] -Внесок у документацію є однією з найкорисніших діяльностей, оскільки ви допомагаєте іншим зрозуміти фреймворк. - - -Як писати? ----------- - -Документація призначена насамперед для людей, які знайомляться з темою. Тому вона повинна відповідати кільком важливим пунктам: - -- Починайте з простого та загального. До більш складних тем переходьте лише наприкінці. -- Намагайтеся пояснити річ якомога краще. Спробуйте, наприклад, спочатку пояснити тему колезі. -- Наводьте лише ту інформацію, яка дійсно потрібна користувачеві для даної теми. -- Перевірте, чи ваша інформація дійсно правдива. Кожен код протестуйте. -- Будьте лаконічними - те, що напишете, скоротіть наполовину. А потім, можливо, ще раз. -- Економте на виділеннях усіх видів, від жирного шрифту до рамок типу `.[note]`. -- У кодах дотримуйтесь [стандарту кодування |Coding Standard]. - -Освойте також [синтаксис |syntax]. Для попереднього перегляду статті під час її написання можете використовувати [редактор з попереднім переглядом |https://editor.nette.org/]. - - -Мовні версії ------------- - -Основною мовою є англійська, тому ваші зміни повинні бути як чеською, так і англійською. Якщо англійська не є вашою сильною стороною, використовуйте [DeepL Translator |https://www.deepl.com/translator], а інші перевірять ваш текст. - -Переклад на інші мови буде виконано автоматично після схвалення та доопрацювання вашої правки. - - -Тривіальні правки ------------------ - -Для внесення внеску в документацію необхідно мати обліковий запис на [GitHub|https://github.com]. - -Найпростіший спосіб внести невелику зміну в документацію – скористатися посиланнями в кінці кожної сторінки: - -- *Показати на GitHub* відкриє вихідний код даної сторінки на GitHub. Потім достатньо натиснути кнопку `E`, і ви можете почати редагувати (необхідно бути авторизованим на GitHub). -- *Відкрити попередній перегляд* відкриє редактор, де ви одразу побачите і кінцевий візуальний вигляд. - -Оскільки [редактор з попереднім переглядом |https://editor.nette.org/] не має можливості зберігати зміни безпосередньо на GitHub, необхідно після завершення редагування скопіювати вихідний текст у буфер обміну (кнопкою *Copy to clipboard*), а потім вставити його в редактор на GitHub. Під полем редагування є форма для надсилання. Тут не забудьте коротко підсумувати та пояснити причину вашої правки. Після надсилання створюється так званий pull request (PR), який можна далі редагувати. - - -Більші правки -------------- - -Більш доцільно, ніж використовувати інтерфейс GitHub, бути знайомим з основами роботи з системою контролю версій Git. Якщо ви не володієте роботою з Git, можете ознайомитися з посібником [git - the simple guide |https://rogerdudler.github.io/git-guide/] та, за потреби, скористатися одним з багатьох [графічних клієнтів |https://git-scm.com/downloads/guis]. - -Документацію редагуйте таким чином: - -1) на GitHub створіть [fork |https://help.github.com/en/github/getting-started-with-github/fork-a-repo] репозиторію [nette/docs |https://github.com/nette/docs] -2) цей репозиторій [клонуєте |https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository] на свій комп'ютер -3) потім у [відповідній гілці |#Структура документації] внесіть зміни -4) перевірте зайві пробіли в тексті за допомогою інструменту [Code-Checker |code-checker:] -4) збережіть зміни (зробіть коміт) -6) якщо ви задоволені змінами, надішліть (push) їх на GitHub у ваш форк -7) звідти надішліть їх до репозиторію `nette/docs`, створивши [pull request|https://help.github.com/articles/creating-a-pull-request] (PR) - -Зазвичай ви отримуватимете коментарі із зауваженнями. Слідкуйте за запропонованими змінами та враховуйте їх. Запропоновані зміни додайте як нові коміти та знову надішліть на GitHub. Ніколи не створюйте новий pull request для зміни існуючого pull request. - - -Структура документації ----------------------- - -Вся документація розміщена на GitHub у репозиторії [nette/docs |https://github.com/nette/docs]. Поточна версія знаходиться в гілці `master`, старіші версії розміщені в гілках, таких як `doc-3.x`, `doc-2.x`. - -Вміст кожної гілки поділяється на основні папки, що представляють окремі області документації. Наприклад, `application/` відповідає https://doc.nette.org/cs/application, `latte/` відповідає https://latte.nette.org тощо. Кожна така папка містить підпапки, що представляють мовні версії (`cs`, `en`, ...), та, за потреби, підпапку `files` із зображеннями, які можна вставляти на сторінки документації. diff --git a/contributing/uk/syntax.texy b/contributing/uk/syntax.texy deleted file mode 100644 index c9d1b66d60..0000000000 --- a/contributing/uk/syntax.texy +++ /dev/null @@ -1,142 +0,0 @@ -Синтаксис документації -********************** - -Документація використовує Markdown та [синтаксис Texy |https://texy.nette.org/syntax] з деякими розширеннями. - - -Посилання -========= - -Для внутрішніх посилань використовується запис у квадратних дужках `[посилання]`. Це може бути або у формі з вертикальною рискою `[текст посилання |ціль посилання]`, або скорочено `[текст посилання]`, якщо ціль збігається з текстом (після перетворення на малі літери та дефіси): - -- `[Назва сторінки |Page name]` -> `<a href="/uk/page-name">Назва сторінки</a>` -- `[текст посилання |Page name]` -> `<a href="/uk/page-name">текст посилання</a>` - -Ми можемо посилатися на іншу мовну версію або інший розділ. Розділом вважається бібліотека Nette (наприклад, `forms`, `latte` тощо) або спеціальні розділи, такі як `best-practices`, `quickstart` тощо: - -- `[cs:Назва сторінки |cs:Page name]` -> `<a href="/cs/page-name">Назва сторінки</a>` (той самий розділ, інша мова) -- `[tracy:Назва сторінки |tracy:Page name]` -> `<a href="//tracy.nette.org/uk/page-name">Назва сторінки</a>` (інший розділ, та сама мова) -- `[tracy:cs:Назва сторінки |tracy:cs:Page name]` -> `<a href="//tracy.nette.org/cs/page-name">Назва сторінки</a>` (інший розділ та мова) - -За допомогою `#` також можна націлитися на конкретний заголовок на сторінці. - -- `[Заголовок |#Heading]` -> `<a href="#toc-heading">Заголовок</a>` (заголовок на поточній сторінці) -- `[Назва сторінки#Заголовок |Page name#Heading]` -> `<a href="/uk/page-name#toc-heading">Назва сторінки</a>` - -Посилання на головну сторінку розділу: (`@home` – це спеціальний вираз для домашньої сторінки розділу) - -- `[текст посилання |@home]` -> `<a href="/uk/">текст посилання</a>` -- `[текст посилання |tracy:]` -> `<a href="//tracy.nette.org/uk/">текст посилання</a>` - - -Посилання на документацію API ------------------------------ - -Завжди вказуйте лише за допомогою цього запису: - -- `[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] - -Повністю кваліфіковані назви використовуйте лише при першій згадці. Для подальших посилань використовуйте спрощену назву: - -- `[Form::setTranslator() |api:Nette\Forms\Form::setTranslator()]` -> [Form::setTranslator() |api:Nette\Forms\Form::setTranslator()] - - -Посилання на документацію PHP ------------------------------ - -- `[php:substr]` -> [php:substr] - - -Вихідний код -============ - -Блок коду починається з <code>```lang</code> і закінчується <code>```</code>. Підтримувані мови: `php`, `latte`, `neon`, `html`, `css`, `js` та `sql`. Для відступів завжди використовуйте табуляції. - -``` - ```php - public function renderPage($id) - { - } - ``` -``` - -Ви також можете вказати ім'я файлу як <code>```php .{file: ArrayTest.php}</code>, і блок коду буде відрендерено таким чином: - -```php .{file: ArrayTest.php} -public function renderPage($id) -{ -} -``` - - -Заголовки -========= - -Найвищий заголовок (тобто назву сторінки) підкресліть зірочками. Для розділення секцій використовуйте знаки рівності. Заголовки підкреслюйте знаками рівності, а потім дефісами: - -``` -MVC Додатки & презентери -************************ -... - - -Створення посилань -================== -... - - -Посилання в шаблонах --------------------- -... -``` - - -Рамки та стилі -============== - -Перекс позначимо класом `.[perex]` .[perex] - -Примітку позначимо класом `.[note]` .[note] - -Пораду позначимо класом `.[tip]` .[tip] - -Застереження позначимо класом `.[caution]` .[caution] - -Більш сильне застереження позначимо класом `.[warning]` .[warning] - -Номер версії `.{data-version:2.4.10}` .{data-version:2.4.10} - -Класи записуйте перед рядком: - -``` -.[perex] -Це перекс. -``` - -Будь ласка, усвідомте, що рамки, такі як `.[tip]`, "притягують" очі, тому їх використовують для підкреслення, а не для менш важливої інформації. Тому максимально економте їх використання. - - -Зміст -===== - -Зміст (посилання в правому меню) генерується автоматично для всіх сторінок, розмір яких перевищує 4 000 байт, причому цю стандартну поведінку можна змінити за допомогою [#Метатеги] `{{toc}}`. Текст, що утворює зміст, стандартно береться безпосередньо з тексту заголовків, але за допомогою модифікатора `.{toc}` можна відобразити в змісті інший текст, що особливо корисно для довших заголовків. - -``` - - -Довгий та розумний заголовок .{toc: Будь-який інший текст, відображений у змісті} -================================================================================= -``` - - -Метатеги -======== - -- встановлення власної назви сторінки (у `<title>` та навігаційному ланцюжку) `{{title: Інша назва}}` -- перенаправлення `{{redirect: pla:cs}}` - див. [#Посилання] -- примусове `{{toc}}` або заборона `{{toc: no}}` автоматичного змісту (блок з посиланнями на окремі заголовки) - -{{priority: -1}} diff --git a/database/bg/@home.texy b/database/bg/@home.texy deleted file mode 100644 index 7eccf1677b..0000000000 --- a/database/bg/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Поддържани бази данни -===================== - -Nette поддържа следните бази данни: - -|* Сървър на база данни |* DSN име |* Поддръжка в Core |* Поддръжка в Explorer -| MySQL (>= 5.1) | mysql | ДА | ДА -| PostgreSQL (>= 9.0) | pgsql | ДА | ДА -| Sqlite 3 (>= 3.8) | sqlite | ДА | ДА -| Oracle | oci | ДА | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ДА | ДА -| MS SQL (PDO_DBLIB) | mssql | ДА | - -| ODBC | odbc | ДА | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database значително улеснява извличането на данни от базата данни, без да е необходимо да се пишат SQL заявки. Изпълнява ефективни заявки и не прехвърля излишни данни.}} diff --git a/database/bg/@left-menu.texy b/database/bg/@left-menu.texy deleted file mode 100644 index 928b1f73ea..0000000000 --- a/database/bg/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Въведение |guide] -- [SQL достъп |sql way] -- [Explorer |Explorer] -- [Трансакции |transactions] -- [Изключения |exceptions] -- [Рефлексия |reflection] -- [Мапинг |mapping] -- [Конфигурация |configuration] -- [Рискове за сигурността |security] -- [Надграждане |en:upgrading] diff --git a/database/bg/@meta.texy b/database/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/database/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/database/bg/configuration.texy b/database/bg/configuration.texy deleted file mode 100644 index 68d49c5011..0000000000 --- a/database/bg/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Конфигурация на базата данни -**************************** - -.[perex] -Преглед на конфигурационните опции за Nette Database. - -Ако не използвате целия framework, а само тази библиотека, прочетете [как да заредите конфигурацията|bootstrap:]. - - -Една връзка ------------ - -Конфигурация на една връзка към база данни: - -```neon -database: - # DSN, единственият задължителен ключ - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Създава сървисите `Nette\Database\Connection` и `Nette\Database\Explorer`, които обикновено предаваме чрез [autowiring |dependency-injection:autowiring], или чрез връзка към [тяхното име |#DI Сървиси]. - -Други настройки: - -```neon -database: - # покажи панела на базата данни в Tracy Bar? - debugger: ... # (bool) по подразбиране е true - - # покажи EXPLAIN на заявките в Tracy Bar? - explain: ... # (bool) по подразбиране е true - - # разреши autowiring за тази връзка? - autowired: ... # (bool) по подразбиране е true при първата връзка - - # конвенции за таблици: discovered, static или име на клас - conventions: discovered # (string) по подразбиране е 'discovered' - - options: - # свързване към базата данни само когато е необходимо? - lazy: ... # (bool) по подразбиране е false - - # PHP клас на драйвера на базата данни - driverClass: # (string) - - # само MySQL: задава sql_mode - sqlmode: # (string) - - # само MySQL: задава SET NAMES - charset: # (string) по подразбиране е 'utf8mb4' - - # само MySQL: преобразува TINYINT(1) в bool - convertBoolean: # (bool) по подразбиране е false - - # връща колони с дата като immutable обекти (от версия 3.2.1) - newDateTime: # (bool) по подразбиране е false - - # само Oracle и SQLite: формат за съхранение на дата - formatDateTime: # (string) по подразбиране е 'U' -``` - -В ключа `options` могат да се посочват други опции, които ще намерите в [документацията на PDO драйверите |https://www.php.net/manual/en/pdo.drivers.php], като например: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Множество връзки ----------------- - -В конфигурацията можем да дефинираме и множество връзки към бази данни, като ги разделим на именувани секции: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring е включен само за сървисите от първата секция. Това може да се промени с помощта на `autowired: false` или `autowired: true`. - - -DI Сървиси ----------- - -Тези сървиси се добавят към DI контейнера, където `###` представлява името на връзката: - -| Име | Тип | Описание -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | връзка с базата данни -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |database:explorer] - - -Ако дефинираме само една връзка, имената на сървисите ще бъдат `database.default.connection` и `database.default.explorer`. Ако дефинираме повече връзки, както в примера по-горе, имената ще отговарят на секциите, т.е. `database.main.connection`, `database.main.explorer` и след това `database.another.connection` и `database.another.explorer`. - -Сървисите, които не са autowired, предаваме изрично чрез връзка към тяхното име: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/bg/exceptions.texy b/database/bg/exceptions.texy deleted file mode 100644 index 92a7983581..0000000000 --- a/database/bg/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Изключения -********** - -Nette Database използва йерархия от изключения. Основният клас е `Nette\Database\DriverException`, който наследява от `PDOException` и предоставя разширени възможности за работа с грешки в базата данни: - -- Методът `getDriverCode()` връща кода на грешката от драйвера на базата данни -- Методът `getSqlState()` връща SQLSTATE кода -- Методите `getQueryString()` и `getParameters()` позволяват да се получи оригиналната заявка и нейните параметри - -От `DriverException` наследяват следните специализирани изключения: - -- `ConnectionException` - сигнализира за неуспешно свързване към сървъра на базата данни -- `ConstraintViolationException` - основен клас за нарушаване на ограниченията на базата данни, от който наследяват: - - `ForeignKeyConstraintViolationException` - нарушаване на външен ключ - - `NotNullConstraintViolationException` - нарушаване на ограничението NOT NULL - - `UniqueConstraintViolationException` - нарушаване на уникалността на стойността - - -Пример за прихващане на изключение `UniqueConstraintViolationException`, което възниква, когато се опитваме да вмъкнем потребител с имейл, който вече съществува в базата данни (при условие, че колоната email има уникален индекс). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Потребител с този имейл вече съществува.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Възникна грешка при регистрацията: ' . $e->getMessage(); -} -``` diff --git a/database/bg/explorer.texy b/database/bg/explorer.texy deleted file mode 100644 index b8d1c4a799..0000000000 --- a/database/bg/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer предлага интуитивен и ефективен начин за работа с базата данни. Той автоматично се грижи за релациите между таблиците и оптимизацията на заявките, така че можете да се съсредоточите върху своето приложение. Работи веднага без настройка. Ако се нуждаете от пълен контрол над SQL заявките, можете да използвате [SQL достъп |database:sql-way]. - -- Работата с данни е естествена и лесно разбираема -- Генерира оптимизирани SQL заявки, които зареждат само необходимите данни -- Позволява лесен достъп до свързани данни без необходимост от писане на JOIN заявки -- Работи незабавно без каквато и да е конфигурация или генериране на ентитита - -</div> - - -С Explorer започвате с извикване на метода `table()` на обекта [api:Nette\Database\Explorer] (подробности за връзката ще намерите в главата [Връзка и конфигурация |database:configuration]): - -```php -$books = $explorer->table('book'); // 'book' е името на таблицата -``` - -Методът връща обект [Selection |api:Nette\Database\Table\Selection], който представлява SQL заявка. Към този обект можем да навързваме други методи за филтриране и сортиране на резултатите. Заявката се съставя и изпълнява едва в момента, когато започнем да изискваме данни. Например, чрез преминаване през цикъл `foreach`. Всеки ред е представен от обект [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // извеждане на колона 'title' - echo $book->author_id; // извеждане на колона 'author_id' -} -``` - -Explorer значително улеснява работата с [#релации между таблици]. Следващият пример показва колко лесно можем да изведем данни от свързани таблици (книги и техните автори). Обърнете внимание, че не е необходимо да пишем никакви JOIN заявки, Nette ги създава за нас: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Книга: ' . $book->title; - echo 'Автор: ' . $book->author->name; // създава JOIN към таблица 'author' -} -``` - -Nette Database Explorer оптимизира заявките, за да бъдат възможно най-ефективни. Горепосоченият пример ще изпълни само две SELECT заявки, независимо дали обработваме 10 или 10 000 книги. - -Освен това Explorer следи кои колони се използват в кода и зарежда от базата данни само тях, като по този начин спестява допълнителна производителност. Това поведение е напълно автоматично и адаптивно. Ако по-късно промените кода и започнете да използвате други колони, Explorer автоматично ще промени заявките. Не е необходимо нищо да настройвате, нито да мислите кои колони ще ви трябват - оставете това на Nette. - - -Филтриране и сортиране -====================== - -Класът `Selection` предоставя методи за филтриране и сортиране на избора на данни. - -.[language-php] -| `where($condition, ...$params)` | Добавя условие WHERE. Множество условия се свързват с оператор AND -| `whereOr(array $conditions)` | Добавя група условия WHERE, свързани с оператор OR -| `wherePrimary($value)` | Добавя условие WHERE по първичен ключ -| `order($columns, ...$params)` | Задава сортиране ORDER BY -| `select($columns, ...$params)` | Специфицира колоните, които трябва да бъдат заредени -| `limit($limit, $offset = null)` | Ограничава броя на редовете (LIMIT) и опционално задава OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Задава пагиниране -| `group($columns, ...$params)` | Групира редове (GROUP BY) -| `having($condition, ...$params)` | Добавя условие HAVING за филтриране на групирани редове - -Методите могат да се навързват (т.нар. [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. - -В тези методи можете също да използвате специална нотация за достъп до [данни от свързани таблици |#Заявки през свързани таблици]. - - -Екраниране и идентификатори ---------------------------- - -Методите автоматично екранират параметрите и ограждат идентификаторите (имената на таблици и колони) с кавички, като по този начин предотвратяват SQL injection. За правилното функциониране е необходимо да се спазват няколко правила: - -- Ключовите думи, имената на функции, процедури и т.н. пишете с **главни букви**. -- Имената на колони и таблици пишете с **малки букви**. -- Низовете винаги вмъквайте чрез **параметри**. - -```php -where('name = ' . $name); // КРИТИЧНА УЯЗВИМОСТ: SQL injection -where('name LIKE "%search%"'); // ГРЕШНО: усложнява автоматичното ограждане с кавички -where('name LIKE ?', '%search%'); // ПРАВИЛНО: стойност, вмъкната чрез параметър - -where('name like ?', $name); // ГРЕШНО: генерира: `name` `like` ? -where('name LIKE ?', $name); // ПРАВИЛНО: генерира: `name` LIKE ? -where('LOWER(name) = ?', $value);// ПРАВИЛНО: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Филтрира резултатите с помощта на условия WHERE. Силната му страна е интелигентната работа с различни типове стойности и автоматичният избор на SQL оператори. - -Основно използване: - -```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' -``` - -Благодарение на автоматичното откриване на подходящи оператори не е необходимо да се занимаваме с различни специални случаи. Nette ги решава за нас: - -```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) -// може да се използва и заместващ въпросителен знак без оператор: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Методът правилно обработва и отрицателни условия и празни масиви: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- нищо не намира -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- намира всичко -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- намира всичко -// $table->where('NOT id ?', $ids); Внимание - този синтаксис не се поддържа -``` - -Като параметър можем да предадем и резултат от друга таблица - създава се подзаявка: - -```php -// 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')); -``` - -Условията можем да предадем и като масив, чиито елементи се свързват с AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -В масива можем да използваме двойки ключ => стойност и Nette отново автоматично избира правилните оператори: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -В масива можем да комбинираме SQL изрази със заместващи въпросителни знаци и множество параметри. Това е подходящо за комплексни условия с точно дефинирани оператори: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // два параметъра предаваме като масив -]); -``` - -Многократното извикване на `where()` автоматично свързва условията с AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Подобно на `where()` добавя условия, но с тази разлика, че ги свързва с OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -И тук можем да използваме по-комплексни изрази: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Добавя условие за първичния ключ на таблицата: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Ако таблицата има композитен първичен ключ (напр. `foo_id`, `bar_id`), предаваме го като масив: - -```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(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Определя реда, в който ще бъдат върнати редовете. Можем да сортираме по една или повече колони, в низходящ или възходящ ред, или по собствен израз: - -```php -$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(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Специфицира колоните, които трябва да бъдат върнати от базата данни. По подразбиране Nette Database Explorer връща само тези колони, които реално се използват в кода. Методът `select()` така използваме в случаите, когато трябва да върнем специфични изрази: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Псевдонимите, дефинирани с `AS`, след това са достъпни като свойства на обекта ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // достъп до псевдонима -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Ограничава броя на върнатите редове (LIMIT) и опционално позволява да се зададе offset: - -```php -$table->limit(10); // LIMIT 10 (връща първите 10 реда) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -За пагиниране е по-подходящо да се използва методът `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Улеснява пагинирането на резултатите. Приема номер на страницата (изчисляван от 1) и брой елементи на страница. Опционално може да се предаде референция към променлива, в която ще се съхрани общият брой страници: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Общо страници: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Групира редове според зададените колони (GROUP BY). Обикновено се използва във връзка с агрегатни функции: - -```php -// Преброява броя на продуктите във всяка категория -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Задава условие за филтриране на групирани редове (HAVING). Може да се използва във връзка с метода `group()` и агрегатни функции: - -```php -// Намира категории, които имат повече от 100 продукта -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Четене на данни -=============== - -За четене на данни от базата данни имаме на разположение няколко полезни метода: - -.[language-php] -| `foreach ($table as $key => $row)` | Итерира през всички редове, `$key` е стойността на първичния ключ, `$row` е обект ActiveRow -| `$row = $table->get($key)` | Връща един ред според първичния ключ -| `$row = $table->fetch()` | Връща текущия ред и премества указателя към следващия -| `$array = $table->fetchPairs()` | Създава асоциативен масив от резултатите -| `$array = $table->fetchAll()` | Връща всички редове като масив -| `count($table)` | Връща броя на редовете в обекта Selection - -Обектът [ActiveRow |api:Nette\Database\Table\ActiveRow] е предназначен само за четене. Това означава, че не може да се променят стойностите на неговите свойства. Това ограничение гарантира консистенцията на данните и предотвратява неочаквани странични ефекти. Данните се зареждат от базата данни и всяка промяна трябва да бъде извършена изрично и контролирано. - - -`foreach` - итерация през всички редове ---------------------------------------- - -Най-лесният начин да изпълните заявка и да получите редове е чрез итерация в цикъл `foreach`. Автоматично стартира SQL заявка. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key е стойността на първичния ключ, $book е ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Изпълнява SQL заявка и връща ред според първичния ключ, или `null`, ако не съществува. - -```php -$book = $explorer->table('book')->get(123); // връща ActiveRow с ID 123 или null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Връща ред и премества вътрешния указател към следващия. Ако вече не съществуват други редове, връща `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Връща резултатите като асоциативен масив. Първият аргумент определя името на колоната, която ще се използва като ключ в масива, вторият аргумент определя името на колоната, която ще се използва като стойност: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Ако посочим само първия параметър, стойността ще бъде целият ред, т.е. обект `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -В случай на дублиращи се ключове се използва стойността от последния ред. При използване на `null` като ключ масивът ще бъде индексиран числово от нула (тогава не възникват колизии): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Алтернативно можете като параметър да посочите callback, който за всеки ред ще връща или самата стойност, или двойка ключ-стойност. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Първа книга (Ян Новак)', ...] - -// Callback може също да връща масив с двойка ключ & стойност: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Първа книга' => 'Ян Новак', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Връща всички редове като асоциативен масив от обекти `ActiveRow`, където ключовете са стойностите на първичните ключове. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Методът `count()` без параметър връща броя на редовете в обекта `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // алтернатива -``` - -Внимание, `count()` с параметър извършва агрегатна функция COUNT в базата данни, вижте по-долу. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Преобразува обект `ActiveRow` в асоциативен масив, където ключовете са имената на колоните, а стойностите са съответните данни. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray ще бъде ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Агрегиране -========== - -Класът `Selection` предоставя методи за лесно извършване на агрегатни функции (COUNT, SUM, MIN, MAX, AVG и т.н.). - -.[language-php] -| `count($expr)` | Преброява броя на редовете -| `min($expr)` | Връща минималната стойност в колоната -| `max($expr)` | Връща максималната стойност в колоната -| `sum($expr)` | Връща сумата на стойностите в колоната -| `aggregation($function)` | Позволява да се извърши произволна агрегатна функция. Напр. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Изпълнява SQL заявка с функцията COUNT и връща резултата. Методът се използва за установяване колко реда отговарят на определено условие: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Внимание, [#count()] без параметър само връща броя на редовете в обекта `Selection`. - - -min(string $expr) и max(string $expr) .[method] ------------------------------------------------ - -Методите `min()` и `max()` връщат минималната и максималната стойност в специфицираната колона или израз: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Връща сумата на стойностите в специфицираната колона или израз: - -```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] ----------------------------------------------------------------------- - -Позволява да се извърши произволна агрегатна функция. - -```php -// средна цена на продуктите в категория -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// свързва етикетите на продукта в един низ -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Ако трябва да агрегираме резултати, които вече сами по себе си са произлезли от някаква агрегатна функция и групиране (напр. `SUM(стойност)` върху групирани редове), като втори аргумент посочваме агрегатната функция, която трябва да се приложи върху тези междинни резултати: - -```php -// Изчислява общата цена на продуктите на склад за отделните категории и след това сумира тези цени заедно. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -В този пример първо изчисляваме общата цена на продуктите във всяка категория (`SUM(price * stock) AS category_total`) и групираме резултатите по `category_id`. След това използваме `aggregation('SUM(category_total)', 'SUM')` за сумиране на тези междинни суми `category_total`. Вторият аргумент `'SUM'` казва, че върху междинните резултати трябва да се приложи функцията SUM. - - -Insert, Update & Delete -======================= - -Nette Database Explorer опростява вмъкването, актуализирането и изтриването на данни. Всички посочени методи в случай на грешка изхвърлят изключение `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Вмъква нови записи в таблицата. - -**Вмъкване на един запис:** - -Новият запис предаваме като асоциативен масив или iterable обект (например ArrayHash, използван във [формите |forms:]), където ключовете отговарят на имената на колоните в таблицата. - -Ако таблицата има дефиниран първичен ключ, методът връща обект `ActiveRow`, който се презарежда от базата данни, за да се отразят евентуалните промени, извършени на ниво база данни (тригери, стойности по подразбиране на колони, изчисления на auto-increment колони). По този начин се гарантира консистенцията на данните и обектът винаги съдържа актуалните данни от базата данни. Ако няма еднозначен първичен ключ, връща предадените данни под формата на масив. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row е инстанция на ActiveRow и съдържа пълните данни на вмъкнатия ред, -// включително автоматично генерираното ID и евентуалните промени, извършени от тригери -echo $row->id; // Извежда ID на нововмъкнатия потребител -echo $row->created_at; // Извежда времето на създаване, ако е зададено от тригер -``` - -**Вмъкване на няколко записа едновременно:** - -Методът `insert()` позволява да се вмъкнат няколко записа с една SQL заявка. В този случай връща броя на вмъкнатите редове. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows ще бъде 2 -``` - -Като параметър може също да се предаде обект `Selection` с избор на данни. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Вмъкване на специални стойности:** - -Като стойности можем да предаваме и файлове, обекти DateTime или SQL литерали: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // преобразува в база данни формат - 'avatar' => fopen('image.jpg', 'rb'), // вмъква бинарно съдържание на файла - 'uuid' => $explorer::literal('UUID()'), // извиква функцията UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Актуализира редове в таблицата според зададения филтър. Връща броя на действително променените редове. - -Променяните колони предаваме като асоциативен масив или iterable обект (например ArrayHash, използван във [формите |forms:]), където ключовете отговарят на имената на колоните в таблицата: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -За промяна на числови стойности можем да използваме операторите `+=` и `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // увеличава стойността на колоната 'points' с 1 - 'coins-=' => 1, // намалява стойността на колоната 'coins' с 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Изтрива редове от таблицата според зададения филтър. Връща броя на изтритите редове. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -При извикване на `update()` и `delete()` не забравяйте с помощта на `where()` да специфицирате редовете, които трябва да се променят/изтрият. Ако `where()` не използвате, операцията ще се извърши върху цялата таблица! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Актуализира данни в реда на базата данни, представен от обекта `ActiveRow`. Като параметър приема iterable с данни, които трябва да се актуализират (ключовете са имената на колоните). За промяна на числови стойности можем да използваме операторите `+=` и `-=`: - -След извършване на актуализацията `ActiveRow` автоматично се презарежда от базата данни, за да се отразят евентуалните промени, извършени на ниво база данни (напр. тригери). Методът връща `true` само ако е настъпила действителна промяна на данните. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // увеличаваме броя на показванията -]); -echo $article->views; // Извежда текущия брой показвания -``` - -Този метод актуализира само един конкретен ред в базата данни. За масова актуализация на повече редове използвайте метода [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Изтрива реда от базата данни, който е представен от обекта `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Изтрива книга с ID 1 -``` - -Този метод изтрива само един конкретен ред в базата данни. За масово изтриване на повече редове използвайте метода [#Selection::delete()]. - - -Релации между таблици -===================== - -В релационните бази данни данните са разделени на няколко таблици и са взаимно свързани с помощта на външни ключове. Nette Database Explorer предлага революционен начин за работа с тези релации - без писане на JOIN заявки и без необходимост от каквото и да е конфигуриране или генериране. - -За илюстрация на работата с релации ще използваме примерна база данни с книги ([ще я намерите в GitHub |https://github.com/nette-examples/books]). В базата данни имаме таблици: - -- `author` - писатели и преводачи (колони `id`, `name`, `web`, `born`) -- `book` - книги (колони `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - етикети (колони `id`, `name`) -- `book_tag` - свързваща таблица между книги и етикети (колони `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Структура на базата данни, използвана в примерите - -В нашия пример с база данни за книги намираме няколко типа връзки (въпреки че моделът е опростен спрямо реалността): - -- Едно към много (1:N) – всяка книга **има един** автор, авторът може да напише **няколко** книги. -- Нула към много (0:N) – книгата **може да има** преводач, преводачът може да преведе **няколко** книги. -- Нула към едно (0:1) – книгата **може да има** следващ том. -- Много към много (M:N) – книгата **може да има няколко** етикета и етикетът може да бъде присвоен на **няколко** книги. - -В тези връзки винаги съществува родителска и дъщерна таблица. Например във връзката между автор и книга таблицата `author` е родителска, а `book` е дъщерна - можем да си го представим така, че книгата винаги "принадлежи" на някой автор. Това се проявява и в структурата на базата данни: дъщерната таблица `book` съдържа външен ключ `author_id`, който сочи към родителската таблица `author`. - -Ако трябва да изведем книгите, включително имената на техните автори, имаме две възможности. Или да получим данните с една SQL заявка с помощта на JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Или да заредим данните на две стъпки - първо книгите, а след това техните автори - и след това да ги съберем в PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- id на авторите на получените книги -``` - -Вторият подход всъщност е по-ефективен, въпреки че това може да е изненадващо. Данните се зареждат само веднъж и могат да бъдат по-добре използвани в кеша. Точно по този начин работи Nette Database Explorer - всичко решава под повърхността и ви предлага елегантно API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'заглавие: ' . $book->title; - echo 'написано от: ' . $book->author->name; // $book->author е запис от таблица 'author' - echo 'преведено от: ' . $book->translator?->name; -} -``` - - -Достъп до родителска таблица ----------------------------- - -Достъпът до родителската таблица е пряк. Става въпрос за връзки като *книгата има автор* или *книгата може да има преводач*. Свързаният запис получаваме чрез свойството на обекта ActiveRow - неговото име отговаря на името на колоната с външния ключ без суфикса `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // намира автора според колоната author_id -echo $book->translator?->name; // намира преводача според translator_id -``` - -Когато достъпим свойството `$book->author`, Explorer в таблицата `book` търси колона, чието име съдържа низа `author` (т.е. `author_id`). Според стойността в тази колона зарежда съответния запис от таблицата `author` и го връща като `ActiveRow`. Подобно работи и `$book->translator`, който използва колоната `translator_id`. Тъй като колоната `translator_id` може да съдържа `null`, използваме в кода оператора `?->`. - -Алтернативен път предлага методът `ref()`, който приема два аргумента, името на целевата таблица и името на свързващата колона, и връща инстанция на `ActiveRow` или `null`: - -```php -echo $book->ref('author', 'author_id')->name; // връзка към автора -echo $book->ref('author', 'translator_id')->name; // връзка към преводача -``` - -Методът `ref()` е подходящ, ако не може да се използва достъп чрез свойство, тъй като таблицата съдържа колона със същото име (т.е. `author`). В останалите случаи се препоръчва използването на достъп чрез свойство, който е по-четлив. - -Explorer автоматично оптимизира заявките към базата данни. Когато преминаваме през книгите в цикъл и достъпваме техните свързани записи (автори, преводачи), Explorer не генерира заявка за всяка книга поотделно. Вместо това изпълнява само една SELECT заявка за всеки тип връзка, като по този начин значително намалява натоварването на базата данни. Например: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Този код ще извика само тези три светкавични заявки към базата данни: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id от колоната author_id на избраните книги -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id от колоната translator_id на избраните книги -``` - -.[note] -Логиката за намиране на свързващата колона е дадена от имплементацията на [Conventions |api:Nette\Database\Conventions]. Препоръчваме използването на [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], които анализират външните ключове и позволяват лесно да се работи със съществуващите връзки между таблиците. - - -Достъп до дъщерна таблица -------------------------- - -Достъпът до дъщерната таблица работи в обратна посока. Сега питаме *какви книги е написал този автор* или *превел този преводач*. За този тип заявка използваме метода `related()`, който връща `Selection` със свързаните записи. Нека разгледаме пример: - -```php -$author = $explorer->table('author')->get(1); - -// Извежда всички книги от автора -foreach ($author->related('book.author_id') as $book) { - echo "Написал: $book->title"; -} - -// Извежда всички книги, които авторът е превел -foreach ($author->related('book.translator_id') as $book) { - echo "Превел: $book->title"; -} -``` - -Методът `related()` приема описанието на връзката като един аргумент с точкова нотация или като два отделни аргумента: - -```php -$author->related('book.translator_id'); // един аргумент -$author->related('book', 'translator_id'); // два аргумента -``` - -Explorer може автоматично да открие правилната свързваща колона въз основа на името на родителската таблица. В този случай се свързва чрез колоната `book.author_id`, тъй като името на изходната таблица е `author`: - -```php -$author->related('book'); // използва book.author_id -``` - -Ако съществуват няколко възможни връзки, Explorer ще изхвърли изключение [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Методът `related()` можем, разбира се, да използваме и при преминаване през повече записи в цикъл и Explorer и в този случай автоматично оптимизира заявките: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' написал:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Този код ще генерира само две светкавични SQL заявки: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id на избраните автори -``` - - -Връзка Много към много ----------------------- - -За връзка много към много (M:N) е необходимо съществуването на свързваща таблица (в нашия случай `book_tag`), която съдържа две колони с външни ключове (`book_id`, `tag_id`). Всяка от тези колони сочи към първичния ключ на една от свързваните таблици. За получаване на свързаните данни първо получаваме записите от свързващата таблица с помощта на `related('book_tag')` и след това продължаваме към целевите данни: - -```php -$book = $explorer->table('book')->get(1); -// извежда имената на етикетите, присвоени към книгата -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // извежда името на етикета през свързващата таблица -} - -$tag = $explorer->table('tag')->get(1); -// или обратно: извежда имената на книгите, означени с този етикет -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // извежда името на книгата -} -``` - -Explorer отново оптимизира SQL заявките до ефективна форма: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id на избраните книги -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id на етикетите, намерени в book_tag -``` - - -Заявки през свързани таблици ----------------------------- - -В методите `where()`, `select()`, `order()` и `group()` можем да използваме специални нотации за достъп до колони от други таблици. Explorer автоматично създава необходимите JOIN-ове. - -**Точкова нотация** (`родителска_таблица.колона`) се използва за връзка 1:N от гледна точка на дъщерната таблица: - -```php -$books = $explorer->table('book'); - -// Намира книги, чийто автор има име, започващо с 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Сортира книгите по името на автора низходящо -$books->order('author.name DESC'); - -// Извежда името на книгата и името на автора -$books->select('book.title, author.name'); -``` - -**Нотация с двоеточие** (`:дъщерна_таблица.колона`) се използва за връзка 1:N от гледна точка на родителската таблица: - -```php -$authors = $explorer->table('author'); - -// Намира автори, които са написали книга с 'PHP' в заглавието -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Преброява броя на книгите за всеки автор -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -В горепосочения пример с нотация с двоеточие (`:book.title`) не е специфицирана колоната с външния ключ. Explorer автоматично открива правилната колона въз основа на името на родителската таблица. В този случай се свързва чрез колоната `book.author_id`, тъй като името на изходната таблица е `author`. Ако съществуват няколко възможни връзки, Explorer ще изхвърли изключение [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Свързващата колона може да бъде изрично посочена в скоби: - -```php -// Намира автори, които са превели книга с 'PHP' в заглавието -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Нотациите могат да се навързват за достъп през няколко таблици: - -```php -// Намира автори на книги, означени с етикета 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Разширяване на условията за JOIN --------------------------------- - -Методът `joinWhere()` разширява условията, които се посочват при свързване на таблици в SQL след ключовата дума `ON`. - -Да предположим, че искаме да намерим книги, преведени от конкретен преводач: - -```php -// Намира книги, преведени от преводач на име 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -В условието `joinWhere()` можем да използваме същите конструкции като в метода `where()` - оператори, заместващи въпросителни знаци, масиви от стойности или SQL изрази. - -За по-сложни заявки с повече JOIN-ове можем да дефинираме псевдоними на таблици: - -```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) -``` - -Обърнете внимание, че докато методът `where()` добавя условия към клаузата `WHERE`, методът `joinWhere()` разширява условията в клаузата `ON` при свързване на таблици. diff --git a/database/bg/guide.texy b/database/bg/guide.texy deleted file mode 100644 index e4775b3ff2..0000000000 --- a/database/bg/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database е мощно и елегантно ниво за работа с бази данни за PHP с акцент върху простотата и интелигентните функции. Предлага два начина за работа с базата данни - [Explorer |Explorer] за бърза разработка на приложения или [SQL достъп |SQL way] за директна работа със заявки. - -<div class="grid gap-3"> -<div> - - -[SQL достъп |SQL way] -===================== -- Безопасни параметризирани заявки -- Прецизен контрол върху формата на SQL заявките -- Когато пишете сложни заявки с разширени функции -- Оптимизирате производителността с помощта на специфични SQL функции - -</div> - -<div> - - -[Explorer |Explorer] -==================== -- Разработвате бързо без писане на SQL -- Интуитивна работа с релациите между таблиците -- Ще оцените автоматичната оптимизация на заявките -- Подходящо за бърза и удобна работа с базата данни - -</div> - -</div> - - -Инсталация -========== - -Можете да изтеглите и инсталирате библиотеката с помощта на инструмента [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Поддържани бази данни -===================== - -Nette Database поддържа следните бази данни: - -|* Сървър на база данни |* DSN име |* Поддръжка в Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | ДА -| PostgreSQL (>= 9.0) | pgsql | ДА -| Sqlite 3 (>= 3.8) | sqlite | ДА -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ДА -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Два подхода към базата данни -============================ - -Nette Database ви дава избор: можете или да пишете SQL заявки директно (SQL достъп), или да ги оставите да се генерират автоматично (Explorer). Нека видим как двата подхода решават едни и същи задачи: - -[SQL достъп|sql way] - SQL заявки - -```php -// вмъкване на запис -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// получаване на записи: автори на книги -$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 -'); - -// изход (не е оптимален, генерира N допълнителни заявки) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Автор $author->name е написал $author->books_count книги:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer достъп|explorer] - автоматично генериране на SQL - -```php -// вмъкване на запис -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// получаване на записи: автори на книги -$authors = $database->table('authors') - ->where('active', 1); - -// изход (автоматично генерира само 2 оптимизирани заявки) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Автор $author->name е написал {$books->count()} книги:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Explorer достъпът генерира и оптимизира SQL заявките автоматично. В дадения пример SQL достъпът ще генерира N+1 заявки (една за авторите и след това по една за книгите на всеки автор), докато Explorer автоматично оптимизира заявките и изпълнява само две - една за авторите и една за всички техни книги. - -Двата подхода могат да се комбинират свободно в приложението според нуждите. - - -Свързване и конфигурация -======================== - -За да се свържете с базата данни, е достатъчно да създадете инстанция на класа [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Параметърът `$dsn` (data source name) е същият, [както се използва от PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], напр. `mysql:host=127.0.0.1;dbname=test`. В случай на неуспех, хвърля изключение `Nette\Database\ConnectionException`. - -Въпреки това, по-удобен начин предлага [конфигурацията на приложението |configuration], където е достатъчно да добавите секция `database` и ще се създадат необходимите обекти, както и панелът за база данни в лентата на [Tracy |tracy:] . - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -След това [получаваме обекта на връзката като сървис от DI контейнера |dependency-injection:passing-dependencies], напр.: - -```php -class Model -{ - public function __construct( - // или Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Повече информация за [конфигурацията на базата данни |configuration]. - - -Ръчно създаване на Explorer ---------------------------- - -Ако не използвате Nette DI контейнер, можете да създадете инстанция на `Nette\Database\Explorer` ръчно: - -```php -// свързване с базата данни -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// хранилище за кеш, имплементира Nette\Caching\Storage, напр.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// грижи се за рефлексията на структурата на базата данни -$structure = new Nette\Database\Structure($connection, $storage); -// дефинира правила за мапиране на имената на таблици, колони и външни ключове -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Управление на връзката -====================== - -При създаване на обект `Connection` автоматично се осъществява връзка. Ако искате да отложите връзката, използвайте lazy режим - можете да го включите в [конфигурацията |configuration], като зададете `lazy: true`, или по следния начин: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -За управление на връзката използвайте методите `connect()`, `disconnect()` и `reconnect()`. -- `connect()` създава връзка, ако все още не съществува, като може да хвърли изключение `Nette\Database\ConnectionException`. -- `disconnect()` прекъсва текущата връзка с базата данни. -- `reconnect()` извършва прекъсване и последващо повторно свързване с базата данни. Този метод също може да хвърли изключение `Nette\Database\ConnectionException`. - -Освен това можете да следите събитията, свързани с връзката, с помощта на събитието `onConnect`, което е масив от callback-ове, които се извикват след установяване на връзка с базата данни. - -```php -// изпълнява се след свързване с базата данни -$database->onConnect[] = function($database) { - echo "Свързано с базата данни"; -}; -``` - - -Tracy Debug Bar -=============== - -Ако използвате [Tracy |tracy:], автоматично се активира панелът Database в Debug лентата, който показва всички изпълнени заявки, техните параметри, времето за изпълнение и мястото в кода, където са били извикани. - -[* db-panel.webp *] diff --git a/database/bg/mapping.texy b/database/bg/mapping.texy deleted file mode 100644 index c56310b380..0000000000 --- a/database/bg/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Преобразуване на типове -*********************** - -.[perex] -Nette Database автоматично преобразува стойностите, върнати от базата данни, в съответните PHP типове. - - -Дата и час ----------- - -Данните за време се преобразуват в обекти `Nette\Utils\DateTime`. Ако искате данните за време да се преобразуват в immutable обекти `Nette\Database\DateTime`, задайте опцията `newDateTime: true` в [конфигурацията |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'); -``` - -В случай на MySQL, преобразува типа данни `TIME` в обекти `DateInterval`. - - -Булеви стойности ----------------- - -Булевите стойности автоматично се преобразуват в `true` или `false`. При MySQL се преобразува `TINYINT(1)`, ако зададем `convertBoolean: true` в [конфигурацията |configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Числови стойности ------------------ - -Числовите стойности се преобразуват в `int` или `float` според типа на колоната в базата данни: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Персонализирана нормализация ----------------------------- - -С помощта на метода `setRowNormalizer(?callable $normalizer)` можете да зададете персонализирана функция за трансформиране на редовете от базата данни. Това е полезно например за автоматично преобразуване на типове данни. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // тук се извършва преобразуването на типове - return $row; -}); -``` diff --git a/database/bg/reflection.texy b/database/bg/reflection.texy deleted file mode 100644 index 496557ba0e..0000000000 --- a/database/bg/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Рефлексия на структурата -************************ - -.{data-version:3.2.1} -Nette Database предоставя инструменти за интроспекция на структурата на базата данни с помощта на класа [api:Nette\Database\Reflection]. Тя позволява получаване на информация за таблици, колони, индекси и външни ключове. Можете да използвате рефлексията за генериране на схеми, създаване на гъвкави приложения, работещи с база данни, или общи инструменти за бази данни. - -Получаваме обекта на рефлексията от инстанцията на връзката с базата данни: - -```php -$reflection = $database->getReflection(); -``` - - -Получаване на таблици ---------------------- - -Readonly свойството `$reflection->tables` съдържа асоциативен масив на всички таблици в базата данни: - -```php -// Извеждане на имената на всички таблици -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Налични са още два метода: - -```php -// Проверка за съществуване на таблица -if ($reflection->hasTable('users')) { - echo "Таблицата users съществува"; -} - -// Връща обект на таблицата; ако не съществува, хвърля изключение -$table = $reflection->getTable('users'); -``` - - -Информация за таблицата ------------------------ - -Таблицата е представена от обект [Table|api:Nette\Database\Reflection\Table], който предоставя следните readonly свойства: - -- `$name: string` – име на таблицата -- `$view: bool` – дали е изглед -- `$fullName: ?string` – пълно име на таблицата, включително схемата (ако съществува) -- `$columns: array<string, Column>` – асоциативен масив от колоните на таблицата -- `$indexes: Index[]` – масив от индексите на таблицата -- `$primaryKey: ?Index` – първичен ключ на таблицата или null -- `$foreignKeys: ForeignKey[]` – масив от външните ключове на таблицата - - -Колони ------- - -Свойството `columns` на таблицата предоставя асоциативен масив от колони, където ключът е името на колоната, а стойността е инстанция на [Column|api:Nette\Database\Reflection\Column] със следните свойства: - -- `$name: string` – име на колоната -- `$table: ?Table` – референция към таблицата на колоната -- `$nativeType: string` – нативен тип данни на базата данни -- `$size: ?int` – размер/дължина на типа -- `$nullable: bool` – дали колоната може да съдържа NULL -- `$default: mixed` – стойност по подразбиране на колоната -- `$autoIncrement: bool` – дали колоната е auto-increment -- `$primary: bool` – дали е част от първичния ключ -- `$vendor: array` – допълнителни метаданни, специфични за дадената система за бази данни - -```php -foreach ($table->columns as $name => $column) { - echo "Колона: $name\n"; - echo "Тип: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Да' : 'Не') . "\n"; -} -``` - - -Индекси -------- - -Свойството `indexes` на таблицата предоставя масив от индекси, където всеки индекс е инстанция на [Index|api:Nette\Database\Reflection\Index] със следните свойства: - -- `$columns: Column[]` – масив от колони, образуващи индекса -- `$unique: bool` – дали индексът е уникален -- `$primary: bool` – дали е първичен ключ -- `$name: ?string` – име на индекса - -Първичният ключ на таблицата може да бъде получен с помощта на свойството `primaryKey`, което връща или обект `Index`, или `null` в случай, че таблицата няма първичен ключ. - -```php -// Извеждане на индекси -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Индекс" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Колони: $columns\n"; - echo " Unique: " . ($index->unique ? 'Да' : 'Не') . "\n"; -} - -// Извеждане на първичния ключ -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Първичен ключ: $columns\n"; -} -``` - - -Външни ключове --------------- - -Свойството `foreignKeys` на таблицата предоставя масив от външни ключове, където всеки външен ключ е инстанция на [ForeignKey|api:Nette\Database\Reflection\ForeignKey] със следните свойства: - -- `$foreignTable: Table` – реферирана таблица -- `$localColumns: Column[]` – масив от локални колони -- `$foreignColumns: Column[]` – масив от реферирани колони -- `$name: ?string` – име на външния ключ - -```php -// Извеждане на външни ключове -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/bg/security.texy b/database/bg/security.texy deleted file mode 100644 index 8cd001caa2..0000000000 --- a/database/bg/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Рискове за сигурността -********************** - -<div class=perex> - -Базата данни често съдържа чувствителни данни и позволява извършването на опасни операции. За безопасна работа с Nette Database е ключово: - -- Да се разбира разликата между безопасно и опасно API -- Да се използват параметризирани заявки -- Да се валидират правилно входните данни - -</div> - - -Какво е SQL Injection? -====================== - -SQL инжекцията е най-сериозният риск за сигурността при работа с база данни. Възниква, когато необработен вход от потребител стане част от SQL заявка. Нападателят може да вмъкне собствени SQL команди и по този начин: -- Да получи неоторизиран достъп до данни -- Да модифицира или изтрие данни в базата данни -- Да заобиколи автентикацията - -```php -// ❌ ОПАСЕН КОД - уязвим към SQL инжекция -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Нападателят може да въведе например стойност: ' OR '1'='1 -// Резултатната заявка ще бъде: SELECT * FROM users WHERE name = '' OR '1'='1' -// Което ще върне всички потребители -``` - -Същото се отнася и за Database Explorer: - -```php -// ❌ ОПАСЕН КОД - уязвим към SQL инжекция -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Параметризирани заявки -====================== - -Основната защита срещу SQL инжекция са параметризираните заявки. Nette Database предлага няколко начина за тяхното използване. - -Най-простият начин е използването на **заместващи въпросителни знаци**: - -```php -// ✅ Безопасна параметризирана заявка -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Безопасно условие в Explorer -$table->where('name = ?', $name); -``` - -Това важи за всички други методи в [Database Explorer|explorer], които позволяват вмъкване на изрази със заместващи въпросителни знаци и параметри. - -За командите INSERT, UPDATE или клаузата WHERE можем да предадем стойности в масив: - -```php -// ✅ Безопасен INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Безопасен INSERT в Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Валидация на стойностите на параметрите -======================================= - -Параметризираните заявки са основният градивен елемент за безопасна работа с базата данни. Въпреки това, стойностите, които вмъкваме в тях, трябва да преминат през няколко нива на проверка: - - -Проверка на типа ----------------- - -**Най-важното е да се гарантира правилният тип данни на параметрите** - това е необходимо условие за безопасното използване на Nette Database. Базата данни предполага, че всички входни данни имат правилния тип данни, съответстващ на дадената колона. - -Например, ако `$name` в предишните примери неочаквано беше масив вместо низ, Nette Database щеше да се опита да вмъкне всички негови елементи в SQL заявката, което би довело до грешка. Затова **никога не използвайте** невалидирани данни от `$_GET`, `$_POST` или `$_COOKIE` директно в заявките към базата данни. - - -Проверка на формата -------------------- - -На второ ниво проверяваме формата на данните - например дали низовете са в UTF-8 кодиране и тяхната дължина съответства на дефиницията на колоната, или дали числовите стойности са в допустимия диапазон за дадения тип данни на колоната. - -На това ниво на валидация можем частично да разчитаме и на самата база данни - много бази данни ще отхвърлят невалидни данни. Въпреки това, поведението може да варира, някои могат тихо да скъсят дълги низове или да отрежат числа извън диапазона. - - -Домейн проверка ---------------- - -Третото ниво представляват логически проверки, специфични за вашето приложение. Например, проверка дали стойностите от select полетата съответстват на предлаганите опции, дали числата са в очаквания диапазон (напр. възраст 0-150 години) или дали взаимните зависимости между стойностите имат смисъл. - - -Препоръчителни начини за валидация ----------------------------------- - -- Използвайте [Nette Forms|forms:], които автоматично осигуряват правилната валидация на всички входове -- Използвайте [Presenters|application:] и посочвайте типовете данни за параметрите в методите `action*()` и `render*()` -- Или реализирайте собствен слой за валидация с помощта на стандартни PHP инструменти като `filter_var()` - - -Безопасна работа с колони -========================= - -В предишната секция показахме как правилно да валидираме стойностите на параметрите. При използване на масиви в SQL заявки обаче трябва да обърнем същото внимание и на техните ключове. - -```php -// ❌ ОПАСЕН КОД - ключовете в масива не са обработени -$database->query('INSERT INTO users', $_POST); -``` - -При командите INSERT и UPDATE това е критична грешка в сигурността - нападателят може да вмъкне или промени всяка колона в базата данни. Може например да зададе `is_admin = 1` или да вмъкне произволни данни в чувствителни колони (т.нар. Mass Assignment Vulnerability). - -В условията WHERE е още по-опасно, тъй като те могат да съдържат оператори: - -```php -// ❌ ОПАСЕН КОД - ключовете в масива не са обработени -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// изпълнява заявка WHERE (`salary` > 100000) -``` - -Нападателят може да използва този подход за систематично откриване на заплатите на служителите. Започва например със заявка за заплати над 100 000, след това под 50 000 и чрез постепенно стесняване на диапазона може да разкрие приблизителните заплати на всички служители. Този тип атака се нарича SQL enumeration. - -Методите `where()` и `whereOr()` са още [много по-гъвкави |explorer#where] и поддържат SQL изрази в ключовете и стойностите, включително оператори и функции. Това дава възможност на нападателя да извърши SQL инжекция: - -```php -// ❌ ОПАСЕН КОД - нападателят може да вмъкне собствен SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// изпълнява заявка WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Тази атака прекратява първоначалното условие с помощта на `0)`, добавя собствена `SELECT` команда с помощта на `UNION`, за да получи чувствителни данни от таблицата `users`, и затваря синтактично правилната заявка с помощта на `WHERE (1)`. - - -Бял списък на колони --------------------- - -За безопасна работа с имената на колони се нуждаем от механизъм, който да гарантира, че потребителят може да работи само с разрешени колони и не може да добавя собствени. Можем да се опитаме да открием и блокираме опасни имена на колони (черен списък), но този подход е ненадежден - нападателят винаги може да измисли нов начин да запише опасно име на колона, който не сме предвидили. - -Затова е много по-безопасно да обърнем логиката и да дефинираме изричен списък с разрешени колони (бял списък): - -```php -// Колони, които потребителят може да редактира -$allowedColumns = ['name', 'email', 'active']; - -// Премахваме всички неразрешени колони от входа -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Сега можем безопасно да използваме в заявки, като например: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Динамични идентификатори -======================== - -За динамични имена на таблици и колони използвайте заместващия символ `?name`. Той осигурява правилното екраниране на идентификаторите според синтаксиса на дадената база данни (напр. с помощта на обратни кавички в MySQL): - -```php -// ✅ Безопасно използване на доверени идентификатори -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Резултат в MySQL: SELECT `name` FROM `users` -``` - -Важно: използвайте символа `?name` само за доверени стойности, дефинирани в кода на приложението. За стойности от потребителя използвайте отново [бял списък |#Бял списък на колони]. В противен случай се излагате на рискове за сигурността: - -```php -// ❌ ОПАСНО - никога не използвайте вход от потребител -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/bg/sql-way.texy b/database/bg/sql-way.texy deleted file mode 100644 index 6833ea9af8..0000000000 --- a/database/bg/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL достъп -********** - -.[perex] -Nette Database предлага два начина: можете да пишете SQL заявки сами (SQL достъп) или да ги оставите да се генерират автоматично (вижте [Explorer |explorer]). SQL достъпът ви дава пълен контрол над заявките, като същевременно гарантира тяхното безопасно изграждане. - -.[note] -Подробности за свързването и конфигурацията на базата данни можете да намерите в глава [Свързване и конфигурация |guide#Свързване и конфигурация]. - - -Основно запитване -================= - -За запитвания към базата данни се използва методът `query()`. Той връща обект [ResultSet |api:Nette\Database\ResultSet], който представлява резултата от заявката. В случай на неуспех, методът [хвърля изключение |exceptions]. Можем да обходим резултата от заявката с помощта на цикъл `foreach` или да използваме някоя от [помощните функции |#Получаване на данни]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -За безопасно вмъкване на стойности в SQL заявки използваме параметризирани заявки. Nette Database ги прави максимално прости - достатъчно е да добавите запетая и стойност след SQL заявката: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -При повече параметри имате две опции за запис. Можете или да "вмъквате" параметри в SQL заявката: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Или първо да напишете цялата SQL заявка и след това да добавите всички параметри: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Защита от SQL injection -======================= - -Защо е важно да се използват параметризирани заявки? Защото те ви защитават от атака, наречена SQL injection, при която нападателят може да вмъкне собствени SQL команди и по този начин да получи или повреди данни в базата данни. - -.[warning] -**Никога не вмъквайте променливи директно в SQL заявката!** Винаги използвайте параметризирани заявки, които ви защитават от SQL injection. - -```php -// ❌ ОПАСЕН КОД - уязвим към SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Безопасна параметризирана заявка -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Запознайте се с [възможните рискове за сигурността |security]. - - -Техники за запитване -==================== - - -Условия WHERE -------------- - -Можете да запишете условията WHERE като асоциативен масив, където ключовете са имената на колоните, а стойностите са данните за сравнение. Nette Database автоматично избира най-подходящия SQL оператор според типа на стойността. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -В ключа можете също изрично да посочите оператора за сравнение: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // използва оператор > - 'name LIKE' => '%John%', // използва оператор LIKE - 'email NOT LIKE' => '%example.com%', // използва оператор NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette автоматично обработва специални случаи като `null` стойности или масиви. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // използва оператор = - 'category_id' => [1, 2, 3], // използва IN - 'description' => null, // използва IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -За отрицателни условия използвайте оператора `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // използва оператор <> - 'category_id NOT' => [1, 2, 3], // използва NOT IN - 'description NOT' => null, // използва IS NOT NULL - 'id' => [], // пропуска се -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -За свързване на условия се използва операторът `AND`. Това може да се промени с помощта на [заместващия символ ?or |#Подсказки за изграждане на SQL]. - - -Правила ORDER BY ----------------- - -Сортирането `ORDER BY` може да се запише с помощта на масив. В ключовете посочваме колоните, а стойността ще бъде булева променлива, определяща дали да се сортира възходящо: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // възходящо - 'name' => false, // низходящо -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Вмъкване на данни (INSERT) --------------------------- - -За вмъкване на записи се използва SQL инструкцията `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Методът `getInsertId()` връща ID на последния вмъкнат ред. При някои бази данни (напр. PostgreSQL) е необходимо да се посочи като параметър името на последователността, от която трябва да се генерира ID, с помощта на `$database->getInsertId($sequenceId)`. - -Като параметри можем да предаваме и [#специални стойности] като файлове, обекти DateTime или enum типове. - -Вмъкване на няколко записа наведнъж: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Многократното INSERT е много по-бързо, тъй като се изпълнява една единствена заявка към базата данни, вместо много отделни. - -**Предупреждение за сигурност:** Никога не използвайте невалидирани данни като `$values`. Запознайте се с [възможните рискове |security#Безопасна работа с колони]. - - -Актуализация на данни (UPDATE) ------------------------------- - -За актуализация на записи се използва SQL инструкцията `UPDATE`. - -```php -// Актуализация на един запис -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Броят на засегнатите редове се връща от `$result->getRowCount()`. - -За UPDATE можем да използваме операторите `+=` и `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // инкрементиране на login_count -], 1); -``` - -Пример за вмъкване или редактиране на запис, ако вече съществува. Ще използваме техниката `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 -``` - -Забележете, че Nette Database разпознава в какъв контекст на SQL инструкцията вмъкваме параметъра с масив и съответно изгражда SQL кода от него. Така от първия масив е изградил `(id, name, year) VALUES (123, 'Jim', 1978)`, докато втория е преобразувал във формата `name = 'Jim', year = 1978`. Разглеждаме това по-подробно в секцията [#Подсказки за изграждане на SQL]. - - -Изтриване на данни (DELETE) ---------------------------- - -За изтриване на записи се използва SQL инструкцията `DELETE`. Пример за получаване на броя на изтритите редове: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Подсказки за изграждане на SQL ------------------------------- - -Подсказката е специален placeholder в SQL заявката, който указва как стойността на параметъра трябва да се преобразува в SQL израз: - -| Подсказка | Описание | Автоматично се използва -|-----------|-------------------------------------------------|----------------------------- -| `?name` | използва се за вмъкване на име на таблица или колона | - -| `?values` | генерира `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | генерира присвояване `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | свързва условията в масива с оператор `AND` | `WHERE ?`, `HAVING ?` -| `?or` | свързва условията в масива с оператор `OR` | - -| `?order` | генерира клауза `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -За динамично вмъкване на имена на таблици и колони в заявката се използва placeholder-ът `?name`. Nette Database се грижи за правилното обработване на идентификаторите според конвенциите на дадената база данни (напр. затваряне в обратни кавички в MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (в MySQL) -``` - -**Внимание:** използвайте символа `?name` само за имена на таблици и колони от валидирани входове, в противен случай се излагате на [риск за сигурността |security#Динамични идентификатори]. - -Обикновено не е необходимо да се посочват другите подсказки, тъй като Nette използва интелигентно автоматично откриване при съставянето на SQL заявката (вижте третата колона на таблицата). Но можете да ги използвате например в ситуация, когато искате да свържете условията с `OR` вместо с `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' -``` - - -Специални стойности -------------------- - -Освен обичайните скаларни типове (string, int, bool), можете да предавате като параметри и специални стойности: - -- файлове: `fopen('image.gif', 'r')` вмъква бинарното съдържание на файла -- дата и час: обекти `DateTime` се преобразуват в база данни формат -- enum типове: инстанции на `enum` се преобразуват в тяхната стойност -- SQL литерали: създадени с помощта на `Connection::literal('NOW()')` се вмъкват директно в заявката - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -При бази данни, които нямат нативна поддръжка за типа данни `datetime` (като SQLite и Oracle), `DateTime` се преобразува в стойност, определена в [конфигурацията на базата данни |configuration] чрез елемента `formatDateTime` (стойността по подразбиране е `U` - unix timestamp). - - -SQL литерали ------------- - -В някои случаи трябва да посочите директно SQL код като стойност, който обаче не трябва да се разбира като низ и да се екранира. За това служат обектите от класа `Nette\Database\SqlLiteral`. Те се създават от метода `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()) -``` - -Или алтернативно: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL литералите могат да съдържат параметри: - -```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) -``` - -Благодарение на което можем да създаваме интересни комбинации: - -```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') -``` - - -Получаване на данни -=================== - - -Кратки пътища за SELECT заявки ------------------------------- - -За опростяване на извличането на данни `Connection` предлага няколко кратки пътя, които комбинират извикването на `query()` със следващо `fetch*()`. Тези методи приемат същите параметри като `query()`, т.е. SQL заявка и незадължителни параметри. Пълно описание на методите `fetch*()` ще намерите [по-долу |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Изпълнява заявка и връща първия ред като обект `Row` -| `fetchAll($sql, ...$params): array` | Изпълнява заявка и връща всички редове като масив от обекти `Row` -| `fetchPairs($sql, ...$params): array` | Изпълнява заявка и връща асоциативен масив, където първата колона представлява ключ, а втората - стойност -| `fetchField($sql, ...$params): mixed` | Изпълнява заявка и връща стойността на първото поле от първия ред -| `fetchList($sql, ...$params): ?array` | Изпълнява заявка и връща първия ред като индексиран масив - -Пример: - -```php -// fetchField() - връща стойността на първата клетка -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - итерация през редове --------------------------------- - -След изпълнение на заявката се връща обект [ResultSet|api:Nette\Database\ResultSet], който позволява обхождане на резултатите по няколко начина. Най-лесният начин да изпълните заявка и да получите редовете е чрез итерация в цикъл `foreach`. Този начин е най-икономичен откъм памет, тъй като връща данните постепенно и не ги съхранява всички наведнъж в паметта. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` може да се итерира само веднъж. Ако трябва да итерирате многократно, първо трябва да заредите данните в масив, например с помощта на метода `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Връща ред като обект `Row`. Ако няма повече редове, връща `null`. Премества вътрешния указател към следващия ред. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // зарежда първия ред -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Връща всички останали редове от `ResultSet` като масив от обекти `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // зарежда всички редове -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Връща резултатите като асоциативен масив. Първият аргумент определя името на колоната, която ще се използва като ключ в масива, вторият аргумент определя името на колоната, която ще се използва като стойност: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Ако посочим само първия параметър, стойността ще бъде целият ред, т.е. обект `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -В случай на дублиращи се ключове, се използва стойността от последния ред. При използване на `null` като ключ, масивът ще бъде индексиран числово от нула (тогава не възникват колизии): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Алтернативно, можете да посочите като параметър callback, който за всеки ред ще връща или самата стойност, или двойка ключ-стойност. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback може също да връща масив с двойка ключ & стойност: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Връща стойността на първото поле от текущия ред. Ако няма повече редове, връща `null`. Премества вътрешния указател към следващия ред. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // зарежда името от първия ред -``` - - -fetchList(): ?array .[method] ------------------------------ - -Връща ред като индексиран масив. Ако няма повече редове, връща `null`. Премества вътрешния указател към следващия ред. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Връща броя на засегнатите редове от последната заявка `UPDATE` или `DELETE`. За `SELECT` това е броят на върнатите редове, но той може да не е известен - в такъв случай методът връща `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Връща броя на колоните в `ResultSet`. - - -Информация за заявките -====================== - -За целите на дебъгването можем да получим информация за последната изпълнена заявка: - -```php -echo $database->getLastQueryString(); // извежда SQL заявката - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // извежда SQL заявката -echo $result->getTime(); // извежда времето за изпълнение в секунди -``` - -За показване на резултата като HTML таблица може да се използва: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -ResultSet предлага информация за типовете на колоните: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column е тип $type->type"; // напр. 'id е тип int' -} -``` - - -Логване на заявки ------------------ - -Можем да реализираме собствено логване на заявки. Събитието `onQuery` е масив от callback-ове, които се извикват след всяка изпълнена заявка: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Заявка: ' . $result->getQueryString()); - $logger->info('Време: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Голям резултатен набор: ' . $result->getRowCount() . ' реда'); - } -}; -``` diff --git a/database/bg/transactions.texy b/database/bg/transactions.texy deleted file mode 100644 index dd532f0a4a..0000000000 --- a/database/bg/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Транзакции -********** - -.[perex] -Транзакциите гарантират, че или всички операции в рамките на трансакцията ще бъдат изпълнени, или нито една няма да бъде изпълнена. Те са полезни за осигуряване на консистентност на данните при по-сложни операции. - -Най-лесният начин за използване на транзакции изглежда така: - -```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; -} -``` - -Можете да запишете същото много по-елегантно с помощта на метода `transaction()`. Той приема като параметър callback, който изпълнява в транзакция. Ако callback-ът премине без изключение, транзакцията се потвърждава автоматично. Ако възникне изключение, транзакцията се отменя (rollback) и изключението се разпространява по-нататък. - -```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' - ]); -}); -``` - -Методът `transaction()` може също да връща стойности: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // връща броя на актуализираните редове -}); -``` diff --git a/database/el/@home.texy b/database/el/@home.texy deleted file mode 100644 index ff1b599908..0000000000 --- a/database/el/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Υποστηριζόμενες βάσεις δεδομένων -================================ - -Το Nette υποστηρίζει τις ακόλουθες βάσεις δεδομένων: - -|* Διακομιστής βάσης δεδομένων |* Όνομα DSN |* Υποστήριξη στον Core |* Υποστήριξη στον Explorer -| MySQL (>= 5.1) | mysql | ΝΑΙ | ΝΑΙ -| PostgreSQL (>= 9.0) | pgsql | ΝΑΙ | ΝΑΙ -| Sqlite 3 (>= 3.8) | sqlite | ΝΑΙ | ΝΑΙ -| Oracle | oci | ΝΑΙ | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ΝΑΙ | ΝΑΙ -| MS SQL (PDO_DBLIB) | mssql | ΝΑΙ | - -| ODBC | odbc | ΝΑΙ | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Η Nette Database απλοποιεί σημαντικά την ανάκτηση δεδομένων από τη βάση δεδομένων χωρίς την ανάγκη γραφής ερωτημάτων SQL. Θέτει αποτελεσματικά ερωτήματα και δεν μεταφέρει περιττά δεδομένα.}} diff --git a/database/el/@left-menu.texy b/database/el/@left-menu.texy deleted file mode 100644 index 2bfe03617b..0000000000 --- a/database/el/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Εισαγωγή |guide] -- [Πρόσβαση SQL |sql way] -- [Explorer] -- [Συναλλαγές |transactions] -- [Εξαιρέσεις |exceptions] -- [Reflection |reflection] -- [Αντιστοίχιση |mapping] -- [Διαμόρφωση |configuration] -- [Κίνδυνοι ασφαλείας |security] -- [Αναβάθμιση |en:upgrading] diff --git a/database/el/@meta.texy b/database/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/database/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/database/el/configuration.texy b/database/el/configuration.texy deleted file mode 100644 index 221c114176..0000000000 --- a/database/el/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Διαμόρφωση βάσης δεδομένων -************************** - -.[perex] -Επισκόπηση των επιλογών διαμόρφωσης για το Nette Database. - -Αν δεν χρησιμοποιείτε ολόκληρο το framework, αλλά μόνο αυτή τη βιβλιοθήκη, διαβάστε [πώς να φορτώσετε τη διαμόρφωση |bootstrap:]. - - -Μία σύνδεση ------------ - -Διαμόρφωση μιας σύνδεσης βάσης δεδομένων: - -```neon -database: - # DSN, το μοναδικό υποχρεωτικό κλειδί - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Δημιουργεί τις υπηρεσίες `Nette\Database\Connection` και `Nette\Database\Explorer`, τις οποίες συνήθως περνάμε με [autowiring |dependency-injection:autowiring], ή με αναφορά στο [όνομά τους |#Υπηρεσίες DI]. - -Περαιτέρω ρυθμίσεις: - -```neon -database: - # εμφάνιση του πίνακα database στο Tracy Bar; - debugger: ... # (bool) προεπιλογή είναι true - - # εμφάνιση EXPLAIN των queries στο Tracy Bar; - explain: ... # (bool) προεπιλογή είναι true - - # ενεργοποίηση autowiring για αυτή τη σύνδεση; - autowired: ... # (bool) προεπιλογή είναι true στην πρώτη σύνδεση - - # συμβάσεις πινάκων: discovered, static ή όνομα κλάσης - conventions: discovered # (string) προεπιλογή είναι 'discovered' - - options: - # σύνδεση στη βάση δεδομένων μόνο όταν χρειάζεται; - lazy: ... # (bool) προεπιλογή είναι false - - # PHP κλάση του database driver - driverClass: # (string) - - # μόνο MySQL: ορίζει το sql_mode - sqlmode: # (string) - - # μόνο MySQL: ορίζει το SET NAMES - charset: # (string) προεπιλογή είναι 'utf8mb4' - - # μόνο MySQL: μετατρέπει το TINYINT(1) σε bool - convertBoolean: # (bool) προεπιλογή είναι false - - # επιστρέφει στήλες με ημερομηνία ως immutable αντικείμενα (από την έκδοση 3.2.1) - newDateTime: # (bool) προεπιλογή είναι false - - # μόνο Oracle και SQLite: μορφή για αποθήκευση ημερομηνίας - formatDateTime: # (string) προεπιλογή είναι 'U' -``` - -Στο κλειδί `options` μπορούν να αναφερθούν και άλλες επιλογές, τις οποίες θα βρείτε στην [τεκμηρίωση των PDO drivers |https://www.php.net/manual/en/pdo.drivers.php], όπως για παράδειγμα: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Πολλαπλές συνδέσεις -------------------- - -Στη διαμόρφωση μπορούμε να ορίσουμε και πολλαπλές συνδέσεις βάσης δεδομένων χωρίζοντάς τις σε ονομασμένες ενότητες: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Το Autowiring είναι ενεργοποιημένο μόνο για τις υπηρεσίες από την πρώτη ενότητα. Μπορεί να αλλάξει χρησιμοποιώντας `autowired: false` ή `autowired: true`. - - -Υπηρεσίες DI ------------- - -Αυτές οι υπηρεσίες προστίθενται στο DI container, όπου το `###` αντιπροσωπεύει το όνομα της σύνδεσης: - -| Όνομα | Τύπος | Περιγραφή -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | σύνδεση με τη βάση δεδομένων -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Αν ορίσουμε μόνο μία σύνδεση, τα ονόματα των υπηρεσιών θα είναι `database.default.connection` και `database.default.explorer`. Αν ορίσουμε πολλαπλές συνδέσεις όπως στο παραπάνω παράδειγμα, τα ονόματα θα αντιστοιχούν στις ενότητες, δηλ. `database.main.connection`, `database.main.explorer` και επιπλέον `database.another.connection` και `database.another.explorer`. - -Τις μη-autowired υπηρεσίες τις περνάμε ρητά με αναφορά στο όνομά τους: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/el/exceptions.texy b/database/el/exceptions.texy deleted file mode 100644 index 9948c5f939..0000000000 --- a/database/el/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Εξαιρέσεις -********** - -Το Nette Database χρησιμοποιεί μια ιεραρχία εξαιρέσεων. Η βασική κλάση είναι η `Nette\Database\DriverException`, η οποία κληρονομεί από την `PDOException` και παρέχει διευρυμένες δυνατότητες για την εργασία με σφάλματα βάσης δεδομένων: - -- Η μέθοδος `getDriverCode()` επιστρέφει τον κωδικό σφάλματος από τον οδηγό (driver) της βάσης δεδομένων. -- Η μέθοδος `getSqlState()` επιστρέφει τον κωδικό SQLSTATE. -- Οι μέθοδοι `getQueryString()` και `getParameters()` επιτρέπουν την απόκτηση του αρχικού ερωτήματος (query) και των παραμέτρων του. - -Από την `DriverException` κληρονομούν οι ακόλουθες εξειδικευμένες εξαιρέσεις: - -- `ConnectionException` - σηματοδοτεί αποτυχία σύνδεσης στον διακομιστή της βάσης δεδομένων. -- `ConstraintViolationException` - βασική κλάση για παραβίαση περιορισμών βάσης δεδομένων, από την οποία κληρονομούν: - - `ForeignKeyConstraintViolationException` - παραβίαση ξένου κλειδιού. - - `NotNullConstraintViolationException` - παραβίαση περιορισμού NOT NULL. - - `UniqueConstraintViolationException` - παραβίαση μοναδικότητας τιμής. - - -Παράδειγμα σύλληψης της εξαίρεσης `UniqueConstraintViolationException`, η οποία προκύπτει όταν προσπαθούμε να εισαγάγουμε έναν χρήστη με email που υπάρχει ήδη στη βάση δεδομένων (υποθέτοντας ότι η στήλη `email` έχει μοναδικό ευρετήριο - unique index). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Υπάρχει ήδη χρήστης με αυτό το email.'; // User with this email already exists. - -} catch (Nette\Database\DriverException $e) { - echo 'Παρουσιάστηκε σφάλμα κατά την εγγραφή: ' . $e->getMessage(); // An error occurred during registration: -} -``` diff --git a/database/el/explorer.texy b/database/el/explorer.texy deleted file mode 100644 index f09cee5fdd..0000000000 --- a/database/el/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Ο Explorer προσφέρει έναν διαισθητικό και αποτελεσματικό τρόπο εργασίας με τη βάση δεδομένων. Φροντίζει αυτόματα για τις σχέσεις μεταξύ των πινάκων και τη βελτιστοποίηση των ερωτημάτων (queries), ώστε να μπορείτε να επικεντρωθείτε στην εφαρμογή σας. Λειτουργεί αμέσως χωρίς καμία ρύθμιση. Αν χρειάζεστε πλήρη έλεγχο των ερωτημάτων SQL, μπορείτε να χρησιμοποιήσετε την [προσέγγιση SQL |sql-way]. - -- Η εργασία με τα δεδομένα είναι φυσική και εύκολα κατανοητή. -- Παράγει βελτιστοποιημένα ερωτήματα SQL που φορτώνουν μόνο τα απαραίτητα δεδομένα. -- Επιτρέπει εύκολη πρόσβαση σε σχετιζόμενα δεδομένα χωρίς την ανάγκη γραφής ερωτημάτων JOIN. -- Λειτουργεί άμεσα χωρίς καμία διαμόρφωση ή παραγωγή οντοτήτων (entities). - -</div> - - -Με τον Explorer ξεκινάτε καλώντας τη μέθοδο `table()` του αντικειμένου [api:Nette\Database\Explorer] (λεπτομέρειες για τη σύνδεση θα βρείτε στο κεφάλαιο [Σύνδεση και Διαμόρφωση |guide#Σύνδεση και Διαμόρφωση]): - -```php -$books = $explorer->table('book'); // 'book' είναι το όνομα του πίνακα -``` - -Η μέθοδος επιστρέφει ένα αντικείμενο [Selection |api:Nette\Database\Table\Selection], το οποίο αντιπροσωπεύει ένα ερώτημα SQL. Σε αυτό το αντικείμενο μπορούμε να συνδέσουμε περαιτέρω μεθόδους για φιλτράρισμα και ταξινόμηση των αποτελεσμάτων. Το ερώτημα συντάσσεται και εκτελείται μόνο τη στιγμή που αρχίζουμε να ζητάμε δεδομένα, για παράδειγμα, με τη διέλευση ενός βρόχου `foreach`. Κάθε γραμμή αντιπροσωπεύεται από ένα αντικείμενο [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // εμφάνιση της στήλης 'title' - echo $book->author_id; // εμφάνιση της στήλης 'author_id' -} -``` - -Ο Explorer διευκολύνει θεμελιωδώς την εργασία με τις [#σχέσεις μεταξύ πινάκων]. Το ακόλουθο παράδειγμα δείχνει πόσο εύκολα μπορούμε να εμφανίσουμε δεδομένα από συνδεδεμένους πίνακες (βιβλία και οι συγγραφείς τους). Παρατηρήστε ότι δεν χρειάζεται να γράψουμε κανένα ερώτημα JOIN, το Nette τα δημιουργεί για εμάς: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Βιβλίο: ' . $book->title; // Book: - echo 'Συγγραφέας: ' . $book->author->name; // δημιουργεί JOIN στον πίνακα 'author' // Author: -} -``` - -Το Nette Database Explorer βελτιστοποιεί τα ερωτήματα ώστε να είναι όσο το δυνατόν πιο αποτελεσματικά. Το παραπάνω παράδειγμα εκτελεί μόνο δύο ερωτήματα SELECT, ανεξάρτητα από το αν επεξεργαζόμαστε 10 ή 10.000 βιβλία. - -Επιπλέον, ο Explorer παρακολουθεί ποιες στήλες χρησιμοποιούνται στον κώδικα και φορτώνει από τη βάση δεδομένων μόνο αυτές, εξοικονομώντας έτσι περαιτέρω απόδοση. Αυτή η συμπεριφορά είναι πλήρως αυτόματη και προσαρμοστική. Αν αργότερα τροποποιήσετε τον κώδικα και αρχίσετε να χρησιμοποιείτε άλλες στήλες, ο Explorer προσαρμόζει αυτόματα τα ερωτήματα. Δεν χρειάζεται να ρυθμίσετε τίποτα, ούτε να σκεφτείτε ποιες στήλες θα χρειαστείτε - αφήστε το στο Nette. - - -Φιλτράρισμα και Ταξινόμηση -========================== - -Η κλάση `Selection` παρέχει μεθόδους για το φιλτράρισμα και την ταξινόμηση της επιλογής δεδομένων. - -.[language-php] -| `where($condition, ...$params)` | Προσθέτει συνθήκη WHERE. Πολλαπλές συνθήκες συνδέονται με τον τελεστή AND -| `whereOr(array $conditions)` | Προσθέτει μια ομάδα συνθηκών WHERE συνδεδεμένων με τον τελεστή OR -| `wherePrimary($value)` | Προσθέτει συνθήκη WHERE βάσει του πρωτεύοντος κλειδιού -| `order($columns, ...$params)` | Ορίζει την ταξινόμηση ORDER BY -| `select($columns, ...$params)` | Καθορίζει τις στήλες που πρέπει να φορτωθούν -| `limit($limit, $offset = null)` | Περιορίζει τον αριθμό των γραμμών (LIMIT) και προαιρετικά ορίζει το OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Ορίζει τη σελίδωση -| `group($columns, ...$params)` | Ομαδοποιεί τις γραμμές (GROUP BY) -| `having($condition, ...$params)` | Προσθέτει συνθήκη HAVING για το φιλτράρισμα των ομαδοποιημένων γραμμών - -Οι μέθοδοι μπορούν να αλυσιδωθούν (το λεγόμενο [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. - -Σε αυτές τις μεθόδους μπορείτε επίσης να χρησιμοποιείτε ειδική σημειογραφία για την πρόσβαση σε [δεδομένα από σχετικούς πίνακες |#Ερωτήματα μέσω Σχετικών Πινάκων]. - - -Escaping και Αναγνωριστικά --------------------------- - -Οι μέθοδοι κάνουν αυτόματα escaping τις παραμέτρους και περικλείουν σε εισαγωγικά τα αναγνωριστικά (ονόματα πινάκων και στηλών), αποτρέποντας έτσι το SQL injection. Για τη σωστή λειτουργία, είναι απαραίτητο να τηρούνται ορισμένοι κανόνες: - -- Λέξεις-κλειδιά, ονόματα συναρτήσεων, διαδικασιών κ.λπ. γράφονται με **κεφαλαία γράμματα**. -- Ονόματα στηλών και πινάκων γράφονται με **μικρά γράμματα**. -- Τα strings πάντα εισάγονται μέσω **παραμέτρων**. - -```php -where('name = ' . $name); // ΚΡΙΣΙΜΗ ΕΥΠΑΘΕΙΑ: SQL injection -where('name LIKE "%search%"'); // ΛΑΘΟΣ: περιπλέκει την αυτόματη περικλείωση σε εισαγωγικά -where('name LIKE ?', '%search%'); // ΣΩΣΤΟ: η τιμή εισάγεται μέσω παραμέτρου - -where('name like ?', $name); // ΛΑΘΟΣ: παράγει: `name` `like` ? -where('name LIKE ?', $name); // ΣΩΣΤΟ: παράγει: `name` LIKE ? -where('LOWER(name) = ?', $value);// ΣΩΣΤΟ: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Φιλτράρει τα αποτελέσματα χρησιμοποιώντας συνθήκες WHERE. Η ισχυρή της πλευρά είναι η έξυπνη διαχείριση διαφόρων τύπων τιμών και η αυτόματη επιλογή τελεστών SQL. - -Βασική χρήση: - -```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' -``` - -Χάρη στην αυτόματη ανίχνευση των κατάλληλων τελεστών, δεν χρειάζεται να ασχολούμαστε με διάφορες ειδικές περιπτώσεις. Το Nette τις λύνει για εμάς: - -```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) -// μπορεί να χρησιμοποιηθεί και το placeholder ερωτηματικό (?) χωρίς τελεστή: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Η μέθοδος επεξεργάζεται σωστά και τις αρνητικές συνθήκες και τους κενούς πίνακες: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- τίποτα δεν θα βρεθεί -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- όλα θα βρεθούν -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- όλα θα βρεθούν -// $table->where('NOT id ?', $ids); Προσοχή - αυτή η σύνταξη δεν υποστηρίζεται -``` - -Ως παράμετρο μπορούμε να περάσουμε επίσης το αποτέλεσμα από έναν άλλο πίνακα - θα δημιουργηθεί ένα υποερώτημα (subquery): - -```php -// 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')); -``` - -Τις συνθήκες μπορούμε να τις περάσουμε επίσης ως πίνακα, τα στοιχεία του οποίου συνδέονται με AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -Στον πίνακα μπορούμε να χρησιμοποιήσουμε ζεύγη κλειδί => τιμή και το Nette πάλι επιλέγει αυτόματα τους σωστούς τελεστές: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -Στον πίνακα μπορούμε να συνδυάσουμε εκφράσεις SQL με placeholders ερωτηματικά (?) και πολλαπλές παραμέτρους. Αυτό είναι κατάλληλο για πολύπλοκες συνθήκες με ακριβώς καθορισμένους τελεστές: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // δύο παραμέτρους τις περνάμε ως πίνακα -]); -``` - -Οι πολλαπλές κλήσεις `where()` συνδέουν αυτόματα τις συνθήκες με AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Παρόμοια με το `where()`, προσθέτει συνθήκες, αλλά με τη διαφορά ότι τις συνδέει με OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Και εδώ μπορούμε να χρησιμοποιήσουμε πιο πολύπλοκες εκφράσεις: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Προσθέτει συνθήκη για το πρωτεύον κλειδί του πίνακα: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Αν ο πίνακας έχει σύνθετο πρωτεύον κλειδί (π.χ. `foo_id`, `bar_id`), το περνάμε ως πίνακα: - -```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(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Καθορίζει τη σειρά με την οποία θα επιστραφούν οι γραμμές. Μπορούμε να ταξινομήσουμε με βάση μία ή περισσότερες στήλες, σε φθίνουσα ή αύξουσα σειρά, ή με βάση μια δική μας έκφραση: - -```php -$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(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Καθορίζει τις στήλες που θα επιστραφούν από τη βάση δεδομένων. Από προεπιλογή, το Nette Database Explorer επιστρέφει μόνο τις στήλες που χρησιμοποιούνται πραγματικά στον κώδικα. Τη μέθοδο `select()` τη χρησιμοποιούμε λοιπόν σε περιπτώσεις όπου χρειαζόμαστε να επιστρέψουμε συγκεκριμένες εκφράσεις: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Τα ψευδώνυμα (aliases) που ορίζονται με `AS` είναι στη συνέχεια διαθέσιμα ως ιδιότητες του αντικειμένου ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // πρόσβαση στο alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Περιορίζει τον αριθμό των επιστρεφόμενων γραμμών (LIMIT) και προαιρετικά επιτρέπει τον ορισμό της μετατόπισης (offset): - -```php -$table->limit(10); // LIMIT 10 (επιστρέφει τις πρώτες 10 γραμμές) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Για σελίδωση, είναι προτιμότερο να χρησιμοποιήσετε τη μέθοδο `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Διευκολύνει τη σελίδωση των αποτελεσμάτων. Δέχεται τον αριθμό της σελίδας (μετρώντας από το 1) και τον αριθμό των στοιχείων ανά σελίδα. Προαιρετικά, μπορεί να περάσει μια αναφορά σε μια μεταβλητή, στην οποία θα αποθηκευτεί ο συνολικός αριθμός σελίδων: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Συνολικά σελίδες: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Ομαδοποιεί τις γραμμές με βάση τις καθορισμένες στήλες (GROUP BY). Χρησιμοποιείται συνήθως σε συνδυασμό με συναρτήσεις συγκέντρωσης (aggregation functions): - -```php -// Υπολογίζει τον αριθμό των προϊόντων σε κάθε κατηγορία -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Ορίζει συνθήκη για το φιλτράρισμα των ομαδοποιημένων γραμμών (HAVING). Μπορεί να χρησιμοποιηθεί σε συνδυασμό με τη μέθοδο `group()` και συναρτήσεις συγκέντρωσης: - -```php -// Βρίσκει κατηγορίες που έχουν περισσότερα από 100 προϊόντα -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Ανάγνωση Δεδομένων -================== - -Για την ανάγνωση δεδομένων από τη βάση δεδομένων έχουμε στη διάθεσή μας αρκετές χρήσιμες μεθόδους: - -.[language-php] -| `foreach ($table as $key => $row)` | Επαναλαμβάνεται σε όλες τις γραμμές, το `$key` είναι η τιμή του πρωτεύοντος κλειδιού, το `$row` είναι αντικείμενο ActiveRow -| `$row = $table->get($key)` | Επιστρέφει μία γραμμή με βάση το πρωτεύον κλειδί -| `$row = $table->fetch()` | Επιστρέφει την τρέχουσα γραμμή και μετακινεί τον δείκτη στην επόμενη -| `$array = $table->fetchPairs()` | Δημιουργεί έναν συσχετιστικό πίνακα από τα αποτελέσματα -| `$array = $table->fetchAll()` | Επιστρέφει όλες τις γραμμές ως πίνακα -| `count($table)` | Επιστρέφει τον αριθμό των γραμμών στο αντικείμενο Selection - -Το αντικείμενο [ActiveRow |api:Nette\Database\Table\ActiveRow] προορίζεται μόνο για ανάγνωση. Αυτό σημαίνει ότι δεν μπορούν να αλλάξουν οι τιμές των ιδιοτήτων του. Αυτός ο περιορισμός εξασφαλίζει τη συνέπεια των δεδομένων και αποτρέπει απροσδόκητες παρενέργειες. Τα δεδομένα φορτώνονται από τη βάση δεδομένων και οποιαδήποτε αλλαγή θα πρέπει να γίνεται ρητά και ελεγχόμενα. - - -`foreach` - Επανάληψη σε Όλες τις Γραμμές ------------------------------------------ - -Ο ευκολότερος τρόπος για να εκτελέσετε ένα ερώτημα και να λάβετε γραμμές είναι με την επανάληψη σε έναν βρόχο `foreach`. Εκτελεί αυτόματα το ερώτημα SQL. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // το $key είναι η τιμή του πρωτεύοντος κλειδιού, το $book είναι ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Εκτελεί το ερώτημα SQL και επιστρέφει τη γραμμή με βάση το πρωτεύον κλειδί, ή `null`, αν δεν υπάρχει. - -```php -$book = $explorer->table('book')->get(123); // επιστρέφει ActiveRow με ID 123 ή null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Επιστρέφει μια γραμμή και μετακινεί τον εσωτερικό δείκτη στην επόμενη. Αν δεν υπάρχουν άλλες γραμμές, επιστρέφει `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Επιστρέφει τα αποτελέσματα ως συσχετιστικό πίνακα. Το πρώτο όρισμα καθορίζει το όνομα της στήλης που θα χρησιμοποιηθεί ως κλειδί στον πίνακα, το δεύτερο όρισμα καθορίζει το όνομα της στήλης που θα χρησιμοποιηθεί ως τιμή: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Αν δώσουμε μόνο την πρώτη παράμετρο, η τιμή θα είναι ολόκληρη η γραμμή, δηλαδή το αντικείμενο `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -Σε περίπτωση διπλότυπων κλειδιών, χρησιμοποιείται η τιμή από την τελευταία γραμμή. Κατά τη χρήση του `null` ως κλειδί, ο πίνακας θα ευρετηριαστεί αριθμητικά από το μηδέν (τότε δεν προκύπτουν συγκρούσεις): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Εναλλακτικά, μπορείτε να δώσετε ως παράμετρο ένα callback, το οποίο θα επιστρέφει για κάθε γραμμή είτε την ίδια την τιμή, είτε ένα ζεύγος κλειδί-τιμή. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Πρώτο βιβλίο (Γιάννης Νοβάκ)', ...] - -// Το Callback μπορεί επίσης να επιστρέφει έναν πίνακα με ένα ζεύγος κλειδί & τιμή: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Πρώτο βιβλίο' => 'Γιάννης Νοβάκ', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Επιστρέφει όλες τις γραμμές ως συσχετιστικό πίνακα αντικειμένων `ActiveRow`, όπου τα κλειδιά είναι οι τιμές των πρωτευόντων κλειδιών. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Η μέθοδος `count()` χωρίς παράμετρο επιστρέφει τον αριθμό των γραμμών στο αντικείμενο `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // εναλλακτική -``` - -Προσοχή, το `count()` με παράμετρο εκτελεί τη συνάρτηση συγκέντρωσης COUNT στη βάση δεδομένων. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Μετατρέπει το αντικείμενο `ActiveRow` σε συσχετιστικό πίνακα, όπου τα κλειδιά είναι τα ονόματα των στηλών και οι τιμές είναι τα αντίστοιχα δεδομένα. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// το $bookArray θα είναι ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Συγκέντρωση -=========== - -Η κλάση `Selection` παρέχει μεθόδους για εύκολη εκτέλεση συναρτήσεων συγκέντρωσης (COUNT, SUM, MIN, MAX, AVG κ.λπ.). - -.[language-php] -| `count($expr)` | Μετρά τον αριθμό των γραμμών -| `min($expr)` | Επιστρέφει την ελάχιστη τιμή στη στήλη -| `max($expr)` | Επιστρέφει τη μέγιστη τιμή στη στήλη -| `sum($expr)` | Επιστρέφει το άθροισμα των τιμών στη στήλη -| `aggregation($function)` | Επιτρέπει την εκτέλεση οποιασδήποτε συνάρτησης συγκέντρωσης. Π.χ. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Εκτελεί ένα ερώτημα SQL με τη συνάρτηση COUNT και επιστρέφει το αποτέλεσμα. Η μέθοδος χρησιμοποιείται για να διαπιστωθεί πόσες γραμμές αντιστοιχούν σε μια συγκεκριμένη συνθήκη: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Προσοχή, η μέθοδος [#count()] χωρίς παράμετρο επιστρέφει απλώς τον αριθμό των γραμμών στο αντικείμενο `Selection`. - - -min(string $expr) και max(string $expr) .[method] -------------------------------------------------- - -Οι μέθοδοι `min()` και `max()` επιστρέφουν την ελάχιστη και τη μέγιστη τιμή στην καθορισμένη στήλη ή έκφραση: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Επιστρέφει το άθροισμα των τιμών στην καθορισμένη στήλη ή έκφραση: - -```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] ----------------------------------------------------------------------- - -Επιτρέπει την εκτέλεση οποιασδήποτε συνάρτησης συγκέντρωσης. - -```php -// μέση τιμή προϊόντων στην κατηγορία -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// συνδέει τις ετικέτες του προϊόντος σε ένα string -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Αν χρειαζόμαστε να συγκεντρώσουμε αποτελέσματα που ήδη προέκυψαν από κάποια συνάρτηση συγκέντρωσης και ομαδοποίηση (π.χ. `SUM(value)` σε ομαδοποιημένες γραμμές), ως δεύτερο όρισμα δίνουμε τη συνάρτηση συγκέντρωσης που πρέπει να εφαρμοστεί σε αυτά τα ενδιάμεσα αποτελέσματα: - -```php -// Υπολογίζει τη συνολική τιμή των προϊόντων στο απόθεμα για μεμονωμένες κατηγορίες και στη συνέχεια αθροίζει αυτές τις τιμές μαζί. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -Σε αυτό το παράδειγμα, πρώτα υπολογίζουμε τη συνολική τιμή των προϊόντων σε κάθε κατηγορία (`SUM(price * stock) AS category_total`) και ομαδοποιούμε τα αποτελέσματα με βάση το `category_id`. Στη συνέχεια, χρησιμοποιούμε το `aggregation('SUM(category_total)', 'SUM')` για να αθροίσουμε αυτά τα ενδιάμεσα αθροίσματα `category_total`. Το δεύτερο όρισμα `'SUM'` δηλώνει ότι πρέπει να εφαρμοστεί η συνάρτηση SUM στα ενδιάμεσα αποτελέσματα. - - -Εισαγωγή, Ενημέρωση & Διαγραφή -============================== - -Το Nette Database Explorer απλοποιεί την εισαγωγή, την ενημέρωση και τη διαγραφή δεδομένων. Όλες οι αναφερόμενες μέθοδοι, σε περίπτωση σφάλματος, θα προκαλέσουν την εξαίρεση `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Εισάγει νέες εγγραφές στον πίνακα. - -**Εισαγωγή μιας εγγραφής:** - -Τη νέα εγγραφή την περνάμε ως συσχετιστικό πίνακα ή iterable αντικείμενο (για παράδειγμα ArrayHash που χρησιμοποιείται στις [φόρμες |forms:]), όπου τα κλειδιά αντιστοιχούν στα ονόματα των στηλών στον πίνακα. - -Αν ο πίνακας έχει ορισμένο πρωτεύον κλειδί, η μέθοδος επιστρέφει ένα αντικείμενο `ActiveRow`, το οποίο επαναφορτώνεται από τη βάση δεδομένων, ώστε να ληφθούν υπόψη τυχόν αλλαγές που έγιναν σε επίπεδο βάσης δεδομένων (triggers, προεπιλεγμένες τιμές στηλών, υπολογισμοί auto-increment στηλών). Έτσι εξασφαλίζεται η συνέπεια των δεδομένων και το αντικείμενο περιέχει πάντα τα τρέχοντα δεδομένα από τη βάση δεδομένων. Αν δεν έχει μοναδικό πρωτεύον κλειδί, επιστρέφει τα παραδοθέντα δεδομένα με τη μορφή πίνακα. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// το $row είναι παρουσία του ActiveRow και περιέχει τα πλήρη δεδομένα της εισαχθείσας γραμμής, -// συμπεριλαμβανομένου του αυτόματα παραγόμενου ID και τυχόν αλλαγών που έγιναν από triggers -echo $row->id; // Εμφανίζει το ID του νέου εισαχθέντος χρήστη -echo $row->created_at; // Εμφανίζει τον χρόνο δημιουργίας, αν έχει οριστεί από trigger -``` - -**Εισαγωγή πολλαπλών εγγραφών ταυτόχρονα:** - -Η μέθοδος `insert()` επιτρέπει την εισαγωγή πολλαπλών εγγραφών με ένα μόνο ερώτημα SQL. Σε αυτή την περίπτωση, επιστρέφει τον αριθμό των εισαχθέντων γραμμών. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// το $insertedRows θα είναι 2 -``` - -Ως παράμετρο μπορεί επίσης να περάσει ένα αντικείμενο `Selection` με επιλογή δεδομένων. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Εισαγωγή ειδικών τιμών:** - -Ως τιμές μπορούμε να περάσουμε και αρχεία, αντικείμενα DateTime ή SQL literals: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // μετατρέπει σε μορφή βάσης δεδομένων - 'avatar' => fopen('image.jpg', 'rb'), // εισάγει το δυαδικό περιεχόμενο του αρχείου - 'uuid' => $explorer::literal('UUID()'), // καλεί τη συνάρτηση UUID() της βάσης δεδομένων -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Ενημερώνει γραμμές στον πίνακα σύμφωνα με το καθορισμένο φίλτρο. Επιστρέφει τον αριθμό των γραμμών που πραγματικά άλλαξαν. - -Τις στήλες που αλλάζουν τις περνάμε ως συσχετιστικό πίνακα ή iterable αντικείμενο (για παράδειγμα ArrayHash που χρησιμοποιείται στις [φόρμες |forms:]), όπου τα κλειδιά αντιστοιχούν στα ονόματα των στηλών στον πίνακα: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Για την αλλαγή αριθμητικών τιμών μπορούμε να χρησιμοποιήσουμε τους τελεστές `+=` και `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // αυξάνει την τιμή της στήλης 'points' κατά 1 - 'coins-=' => 1, // μειώνει την τιμή της στήλης 'coins' κατά 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Διαγράφει γραμμές από τον πίνακα σύμφωνα με το καθορισμένο φίλτρο. Επιστρέφει τον αριθμό των διαγραμμένων γραμμών. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Κατά την κλήση `update()` και `delete()`, μην ξεχάσετε να καθορίσετε με το `where()` τις γραμμές που πρέπει να τροποποιηθούν/διαγραφούν. Αν δεν χρησιμοποιήσετε το `where()`, η λειτουργία θα εκτελεστεί σε ολόκληρο τον πίνακα! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Ενημερώνει τα δεδομένα στη γραμμή της βάσης δεδομένων που αντιπροσωπεύεται από το αντικείμενο `ActiveRow`. Ως παράμετρο δέχεται ένα iterable με τα δεδομένα που πρέπει να ενημερωθούν (τα κλειδιά είναι τα ονόματα των στηλών). Για την αλλαγή αριθμητικών τιμών μπορούμε να χρησιμοποιήσουμε τους τελεστές `+=` και `-=`: - -Μετά την εκτέλεση της ενημέρωσης, το `ActiveRow` επαναφορτώνεται αυτόματα από τη βάση δεδομένων, ώστε να ληφθούν υπόψη τυχόν αλλαγές που έγιναν σε επίπεδο βάσης δεδομένων (π.χ. triggers). Η μέθοδος επιστρέφει `true` μόνο αν έγινε πραγματική αλλαγή δεδομένων. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // αυξάνουμε τον αριθμό προβολών -]); -echo $article->views; // Εμφανίζει τον τρέχοντα αριθμό προβολών -``` - -Αυτή η μέθοδος ενημερώνει μόνο μία συγκεκριμένη γραμμή στη βάση δεδομένων. Για μαζική ενημέρωση πολλαπλών γραμμών χρησιμοποιήστε τη μέθοδο [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Διαγράφει τη γραμμή από τη βάση δεδομένων, η οποία αντιπροσωπεύεται από το αντικείμενο `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Διαγράφει το βιβλίο με ID 1 -``` - -Αυτή η μέθοδος διαγράφει μόνο μία συγκεκριμένη γραμμή στη βάση δεδομένων. Για μαζική διαγραφή πολλαπλών γραμμών χρησιμοποιήστε τη μέθοδο [#Selection::delete()]. - - -Σχέσεις μεταξύ Πινάκων -====================== - -Σε σχεσιακές βάσεις δεδομένων, τα δεδομένα χωρίζονται σε πολλούς πίνακες και συνδέονται μεταξύ τους με ξένα κλειδιά. Το Nette Database Explorer φέρνει έναν επαναστατικό τρόπο εργασίας με αυτές τις σχέσεις - χωρίς να γράφετε ερωτήματα JOIN και χωρίς την ανάγκη να διαμορφώνετε ή να παράγετε οτιδήποτε. - -Για την απεικόνιση της εργασίας με τις σχέσεις θα χρησιμοποιήσουμε ένα παράδειγμα βάσης δεδομένων βιβλίων ([μπορείτε να το βρείτε στο GitHub |https://github.com/nette-examples/books]). Στη βάση δεδομένων έχουμε τους πίνακες: - -- `author` - συγγραφείς και μεταφραστές (στήλες `id`, `name`, `web`, `born`) -- `book` - βιβλία (στήλες `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - ετικέτες (στήλες `id`, `name`) -- `book_tag` - πίνακας σύνδεσης μεταξύ βιβλίων και ετικετών (στήλες `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Δομή της βάσης δεδομένων .<> - -Στο παράδειγμά μας της βάσης δεδομένων βιβλίων βρίσκουμε διάφορους τύπους σχέσεων (αν και το μοντέλο είναι απλοποιημένο σε σχέση με την πραγματικότητα): - -- One-to-many (1:N) – κάθε βιβλίο **έχει έναν** συγγραφέα, ο συγγραφέας μπορεί να γράψει **πολλά** βιβλία. -- Zero-to-many (0:N) – το βιβλίο **μπορεί να έχει** μεταφραστή, ο μεταφραστής μπορεί να μεταφράσει **πολλά** βιβλία. -- Zero-to-one (0:1) – το βιβλίο **μπορεί να έχει** επόμενο τόμο. -- Many-to-many (M:N) – το βιβλίο **μπορεί να έχει πολλές** ετικέτες και η ετικέτα μπορεί να αντιστοιχιστεί σε **πολλά** βιβλία. - -Σε αυτές τις σχέσεις υπάρχει πάντα ένας γονικός (parent) και ένας παιδικός (child) πίνακας. Για παράδειγμα, στη σχέση μεταξύ συγγραφέα και βιβλίου, ο πίνακας `author` είναι γονικός και ο `book` παιδικός - μπορούμε να το φανταστούμε έτσι ώστε το βιβλίο πάντα "ανήκει" σε κάποιον συγγραφέα. Αυτό εκδηλώνεται και στη δομή της βάσης δεδομένων: ο παιδικός πίνακας `book` περιέχει το ξένο κλειδί `author_id`, το οποίο αναφέρεται στον γονικό πίνακα `author`. - -Αν χρειαζόμαστε να εμφανίσουμε τα βιβλία συμπεριλαμβανομένων των ονομάτων των συγγραφέων τους, έχουμε δύο δυνατότητες. Είτε θα λάβουμε τα δεδομένα με ένα μόνο ερώτημα SQL χρησιμοποιώντας JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Είτε θα φορτώσουμε τα δεδομένα σε δύο βήματα - πρώτα τα βιβλία και μετά τους συγγραφείς τους - και στη συνέχεια θα τα συνθέσουμε στην PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- ids των συγγραφέων των ληφθέντων βιβλίων -``` - -Η δεύτερη προσέγγιση είναι στην πραγματικότητα πιο αποτελεσματική, αν και αυτό μπορεί να προκαλεί έκπληξη. Τα δεδομένα φορτώνονται μόνο μία φορά και μπορούν να αξιοποιηθούν καλύτερα στην cache. Ακριβώς με αυτόν τον τρόπο λειτουργεί το Nette Database Explorer - λύνει τα πάντα κάτω από την επιφάνεια και σας προσφέρει ένα κομψό API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'τίτλος: ' . $book->title; - echo 'γράφτηκε από: ' . $book->author->name; // το $book->author είναι η εγγραφή από τον πίνακα 'author' - echo 'μεταφράστηκε από: ' . $book->translator?->name; -} -``` - - -Πρόσβαση στον Γονικό Πίνακα ---------------------------- - -Η πρόσβαση στον γονικό πίνακα είναι άμεση. Πρόκειται για σχέσεις όπως *το βιβλίο έχει συγγραφέα* ή *το βιβλίο μπορεί να έχει μεταφραστή*. Την σχετιζόμενη εγγραφή την λαμβάνουμε μέσω της ιδιότητας του αντικειμένου ActiveRow - το όνομά της αντιστοιχεί στο όνομα της στήλης με το ξένο κλειδί, αφαιρώντας το `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // βρίσκει τον συγγραφέα με βάση τη στήλη author_id -echo $book->translator?->name; // βρίσκει τον μεταφραστή με βάση τη στήλη translator_id -``` - -Όταν αποκτούμε πρόσβαση στην ιδιότητα `$book->author`, ο Explorer στον πίνακα `book` αναζητά μια στήλη της οποίας το όνομα περιέχει το string `author` (δηλαδή `author_id`). Με βάση την τιμή σε αυτή τη στήλη, φορτώνει την αντίστοιχη εγγραφή από τον πίνακα `author` και την επιστρέφει ως `ActiveRow`. Παρόμοια λειτουργεί και το `$book->translator`, το οποίο χρησιμοποιεί τη στήλη `translator_id`. Επειδή η στήλη `translator_id` μπορεί να περιέχει `null`, χρησιμοποιούμε στον κώδικα τον τελεστή nullsafe `?->`. - -Μια εναλλακτική οδό προσφέρει η μέθοδος `ref()`, η οποία δέχεται δύο ορίσματα, το όνομα του πίνακα προορισμού και το όνομα της στήλης σύνδεσης, και επιστρέφει μια παρουσία `ActiveRow` ή `null`: - -```php -echo $book->ref('author', 'author_id')->name; // σχέση με τον συγγραφέα -echo $book->ref('author', 'translator_id')->name; // σχέση με τον μεταφραστή -``` - -Η μέθοδος `ref()` είναι χρήσιμη αν δεν μπορεί να χρησιμοποιηθεί η πρόσβαση μέσω ιδιότητας, επειδή ο πίνακας περιέχει στήλη με το ίδιο όνομα (δηλ. `author`). Στις υπόλοιπες περιπτώσεις, συνιστάται η χρήση της πρόσβασης μέσω ιδιότητας, η οποία είναι πιο ευανάγνωστη. - -Ο Explorer βελτιστοποιεί αυτόματα τα ερωτήματα της βάσης δεδομένων. Όταν διατρέχουμε τα βιβλία σε έναν βρόχο και αποκτούμε πρόσβαση στις σχετιζόμενες εγγραφές τους (συγγραφείς, μεταφραστές), ο Explorer δεν παράγει ένα ερώτημα για κάθε βιβλίο ξεχωριστά. Αντ' αυτού, εκτελεί μόνο ένα SELECT για κάθε τύπο σχέσης, μειώνοντας έτσι σημαντικά το φορτίο της βάσης δεδομένων. Για παράδειγμα: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Αυτός ο κώδικας θα καλέσει μόνο αυτά τα τρία αστραπιαία ερωτήματα στη βάση δεδομένων: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id από τη στήλη author_id των επιλεγμένων βιβλίων -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id από τη στήλη translator_id των επιλεγμένων βιβλίων -``` - -.[note] -Η λογική εύρεσης της στήλης σύνδεσης καθορίζεται από την υλοποίηση των [Conventions |api:Nette\Database\Conventions]. Συνιστούμε τη χρήση των [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], οι οποίες αναλύουν τα ξένα κλειδιά και επιτρέπουν την εύκολη εργασία με τις υπάρχουσες σχέσεις μεταξύ των πινάκων. - - -Πρόσβαση στον Παιδικό Πίνακα ----------------------------- - -Η πρόσβαση στον παιδικό πίνακα λειτουργεί με την αντίστροφη κατεύθυνση. Τώρα ρωτάμε *ποια βιβλία έγραψε αυτός ο συγγραφέας* ή *μετέφρασε αυτός ο μεταφραστής*. Για αυτόν τον τύπο ερωτήματος χρησιμοποιούμε τη μέθοδο `related()`, η οποία επιστρέφει ένα `Selection` με τις σχετιζόμενες εγγραφές. Ας δούμε ένα παράδειγμα: - -```php -$author = $explorer->table('author')->get(1); - -// Εμφανίζει όλα τα βιβλία του συγγραφέα -foreach ($author->related('book.author_id') as $book) { - echo "Έγραψε: $book->title"; -} - -// Εμφανίζει όλα τα βιβλία που μετέφρασε ο συγγραφέας -foreach ($author->related('book.translator_id') as $book) { - echo "Μετέφρασε: $book->title"; -} -``` - -Η μέθοδος `related()` δέχεται την περιγραφή της σύνδεσης ως ένα όρισμα με σημειογραφία τελείας ή ως δύο ξεχωριστά ορίσματα: - -```php -$author->related('book.translator_id'); // ένα όρισμα -$author->related('book', 'translator_id'); // δύο ορίσματα -``` - -Ο Explorer μπορεί να ανιχνεύσει αυτόματα τη σωστή στήλη σύνδεσης με βάση το όνομα του γονικού πίνακα. Σε αυτή την περίπτωση, η σύνδεση γίνεται μέσω της στήλης `book.author_id`, επειδή το όνομα του πίνακα πηγής είναι `author`: - -```php -$author->related('book'); // χρησιμοποιεί το book.author_id -``` - -Αν υπήρχαν περισσότερες πιθανές συνδέσεις, ο Explorer θα προκαλούσε την εξαίρεση [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Τη μέθοδο `related()` μπορούμε φυσικά να τη χρησιμοποιήσουμε και κατά τη διέλευση πολλαπλών εγγραφών σε έναν βρόχο και ο Explorer και σε αυτή την περίπτωση βελτιστοποιεί αυτόματα τα ερωτήματα: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' έγραψε:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Αυτός ο κώδικας θα παράγει μόνο δύο αστραπιαία ερωτήματα SQL: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id των επιλεγμένων συγγραφέων -``` - - -Σχέση Many-to-Many ------------------- - -Για τη σχέση many-to-many (M:N) είναι απαραίτητη η ύπαρξη ενός πίνακα σύνδεσης (στην περίπτωσή μας `book_tag`), ο οποίος περιέχει δύο στήλες με ξένα κλειδιά (`book_id`, `tag_id`). Κάθε μία από αυτές τις στήλες αναφέρεται στο πρωτεύον κλειδί ενός από τους συνδεόμενους πίνακες. Για να λάβουμε τα σχετιζόμενα δεδομένα, πρώτα λαμβάνουμε τις εγγραφές από τον πίνακα σύνδεσης χρησιμοποιώντας το `related('book_tag')` και στη συνέχεια συνεχίζουμε στα δεδομένα προορισμού: - -```php -$book = $explorer->table('book')->get(1); -// εμφανίζει τα ονόματα των ετικετών που έχουν αντιστοιχιστεί στο βιβλίο -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // εμφανίζει το όνομα της ετικέτας μέσω του πίνακα σύνδεσης -} - -$tag = $explorer->table('tag')->get(1); -// ή αντίστροφα: εμφανίζει τα ονόματα των βιβλίων που έχουν επισημανθεί με αυτή την ετικέτα -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // εμφανίζει το όνομα του βιβλίου -} -``` - -Ο Explorer πάλι βελτιστοποιεί τα ερωτήματα SQL σε αποτελεσματική μορφή: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id των επιλεγμένων βιβλίων -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id των ετικετών που βρέθηκαν στο book_tag -``` - - -Ερωτήματα μέσω Σχετικών Πινάκων -------------------------------- - -Στις μεθόδους `where()`, `select()`, `order()` και `group()` μπορούμε να χρησιμοποιούμε ειδικές σημειογραφίες για την πρόσβαση σε στήλες από άλλους πίνακες. Ο Explorer δημιουργεί αυτόματα τα απαραίτητα JOINs. - -**Σημειογραφία τελείας** (`parent_table.column`) χρησιμοποιείται για τη σχέση 1:N από την οπτική γωνία του παιδικού πίνακα: - -```php -$books = $explorer->table('book'); - -// Βρίσκει βιβλία των οποίων ο συγγραφέας έχει όνομα που αρχίζει από 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Ταξινομεί τα βιβλία με βάση το όνομα του συγγραφέα φθίνουσα -$books->order('author.name DESC'); - -// Εμφανίζει τον τίτλο του βιβλίου και το όνομα του συγγραφέα -$books->select('book.title, author.name'); -``` - -**Σημειογραφία άνω και κάτω τελείας** (`:child_table.column`) χρησιμοποιείται για τη σχέση 1:N από την οπτική γωνία του γονικού πίνακα: - -```php -$authors = $explorer->table('author'); - -// Βρίσκει συγγραφείς που έγραψαν βιβλίο με 'PHP' στον τίτλο -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Μετρά τον αριθμό των βιβλίων για κάθε συγγραφέα -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -Στο παραπάνω παράδειγμα με τη σημειογραφία άνω και κάτω τελείας (`:book.title`) δεν καθορίζεται η στήλη με το ξένο κλειδί. Ο Explorer ανιχνεύει αυτόματα τη σωστή στήλη με βάση το όνομα του γονικού πίνακα. Σε αυτή την περίπτωση, η σύνδεση γίνεται μέσω της στήλης `book.author_id`, επειδή το όνομα του πίνακα πηγής είναι `author`. Αν υπήρχαν περισσότερες πιθανές συνδέσεις, ο Explorer θα προκαλούσε την εξαίρεση [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Η στήλη σύνδεσης μπορεί να δηλωθεί ρητά σε παρένθεση: - -```php -// Βρίσκει συγγραφείς που μετέφρασαν βιβλίο με 'PHP' στον τίτλο -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Οι σημειογραφίες μπορούν να αλυσιδωθούν για πρόσβαση μέσω πολλαπλών πινάκων: - -```php -// Βρίσκει συγγραφείς βιβλίων που έχουν επισημανθεί με την ετικέτα 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Επέκταση Συνθηκών για JOIN --------------------------- - -Η μέθοδος `joinWhere()` επεκτείνει τις συνθήκες που αναφέρονται κατά τη σύνδεση πινάκων στο SQL μετά τη λέξη-κλειδί `ON`. - -Ας υποθέσουμε ότι θέλουμε να βρούμε βιβλία που μεταφράστηκαν από έναν συγκεκριμένο μεταφραστή: - -```php -// Βρίσκει βιβλία που μεταφράστηκαν από τον μεταφραστή με όνομα 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -Στη συνθήκη `joinWhere()` μπορούμε να χρησιμοποιούμε τις ίδιες κατασκευές όπως στη μέθοδο `where()` - τελεστές, placeholders ερωτηματικά (?), πίνακες τιμών ή εκφράσεις SQL. - -Για πιο πολύπλοκα ερωτήματα με πολλαπλά JOINs, μπορούμε να ορίσουμε ψευδώνυμα (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) -``` - -Παρατηρήστε ότι ενώ η μέθοδος `where()` προσθέτει συνθήκες στην πρόταση `WHERE`, η μέθοδος `joinWhere()` επεκτείνει τις συνθήκες στην πρόταση `ON` κατά τη σύνδεση των πινάκων. diff --git a/database/el/guide.texy b/database/el/guide.texy deleted file mode 100644 index c3515aba11..0000000000 --- a/database/el/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Το Nette Database είναι ένα ισχυρό και κομψό επίπεδο βάσης δεδομένων για PHP με έμφαση στην απλότητα και τις έξυπνες λειτουργίες. Προσφέρει δύο τρόπους εργασίας με τη βάση δεδομένων - [Explorer |Explorer] για γρήγορη ανάπτυξη εφαρμογών, ή [πρόσβαση SQL |SQL way] για άμεση εργασία με ερωτήματα. - -<div class="grid gap-3"> -<div> - - -[Πρόσβαση SQL |SQL way] -======================= -- Ασφαλή παραμετροποιημένα ερωτήματα -- Ακριβής έλεγχος της μορφής των ερωτημάτων SQL -- Όταν γράφετε σύνθετα ερωτήματα με προηγμένες λειτουργίες -- Βελτιστοποιείτε την απόδοση χρησιμοποιώντας συγκεκριμένες λειτουργίες SQL - -</div> - -<div> - - -[Explorer |Explorer] -==================== -- Αναπτύσσετε γρήγορα χωρίς να γράφετε SQL -- Διαισθητική εργασία με σχέσεις μεταξύ πινάκων -- Εκτιμάτε την αυτόματη βελτιστοποίηση ερωτημάτων -- Κατάλληλο για γρήγορη και άνετη εργασία με τη βάση δεδομένων - -</div> - -</div> - - -Εγκατάσταση -=========== - -Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας το εργαλείο [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Υποστηριζόμενες Βάσεις Δεδομένων -================================ - -Το Nette Database υποστηρίζει τις ακόλουθες βάσεις δεδομένων: - -|* Διακομιστής Βάσης Δεδομένων |* Όνομα DSN |* Υποστήριξη στον Explorer -|-----------------------------|-------------|-------------------------- -| MySQL (>= 5.1) | mysql | ΝΑΙ -| PostgreSQL (>= 9.0) | pgsql | ΝΑΙ -| Sqlite 3 (>= 3.8) | sqlite | ΝΑΙ -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ΝΑΙ -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Δύο Προσεγγίσεις στη Βάση Δεδομένων -=================================== - -Το Nette Database σας δίνει μια επιλογή: μπορείτε είτε να γράψετε απευθείας ερωτήματα SQL (πρόσβαση SQL), είτε να τα αφήσετε να δημιουργηθούν αυτόματα (Explorer). Ας δούμε πώς και οι δύο προσεγγίσεις επιλύουν τις ίδιες εργασίες: - -[Πρόσβαση SQL|sql way] - Ερωτήματα SQL - -```php -// εισαγωγή εγγραφής -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// λήψη εγγραφών: συγγραφείς βιβλίων -$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 -'); - -// έξοδος (δεν είναι βέλτιστη, δημιουργεί N+1 ερωτήματα) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Ο συγγραφέας $author->name έγραψε $author->books_count βιβλία:\n"; // Author $author->name wrote $author->books_count books:\n - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Πρόσβαση Explorer|explorer] - Αυτόματη δημιουργία SQL - -```php -// εισαγωγή εγγραφής -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// λήψη εγγραφών: συγγραφείς βιβλίων -$authors = $database->table('authors') - ->where('active', 1); - -// έξοδος (δημιουργεί αυτόματα μόνο 2 βελτιστοποιημένα ερωτήματα) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Ο συγγραφέας $author->name έγραψε {$books->count()} βιβλία:\n"; // Author $author->name wrote {$books->count()} books:\n - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Η προσέγγιση Explorer δημιουργεί και βελτιστοποιεί αυτόματα τα ερωτήματα SQL. Στο παραπάνω παράδειγμα, η πρόσβαση SQL δημιουργεί N+1 ερωτήματα (ένα για τους συγγραφείς και στη συνέχεια ένα για τα βιβλία κάθε συγγραφέα), ενώ ο Explorer βελτιστοποιεί αυτόματα τα ερωτήματα και εκτελεί μόνο δύο - ένα για τους συγγραφείς και ένα για όλα τα βιβλία τους. - -Και οι δύο προσεγγίσεις μπορούν να συνδυαστούν ελεύθερα στην εφαρμογή ανάλογα με τις ανάγκες. - - -Σύνδεση και Διαμόρφωση -====================== - -Για να συνδεθείτε στη βάση δεδομένων, απλώς δημιουργήστε μια παρουσία της κλάσης [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Η παράμετρος `$dsn` (data source name) είναι η ίδια [που χρησιμοποιεί το PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], π.χ. `mysql:host=127.0.0.1;dbname=test`. Σε περίπτωση αποτυχίας, θα προκαλέσει μια εξαίρεση `Nette\Database\ConnectionException`. - -Ωστόσο, ένας πιο βολικός τρόπος προσφέρεται από τη [διαμόρφωση εφαρμογής |configuration], όπου απλά προσθέτετε την ενότητα `database` και δημιουργούνται τα απαραίτητα αντικείμενα καθώς και ο πίνακας της βάσης δεδομένων στη γραμμή [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Στη συνέχεια, [λαμβάνουμε το αντικείμενο σύνδεσης ως υπηρεσία από το DI container |dependency-injection:passing-dependencies], π.χ.: - -```php -class Model -{ - public function __construct( - // ή Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Περισσότερες πληροφορίες σχετικά με τη [διαμόρφωση της βάσης δεδομένων|configuration]. - - -Χειροκίνητη Δημιουργία του Explorer ------------------------------------ - -Εάν δεν χρησιμοποιείτε το Nette DI container, μπορείτε να δημιουργήσετε χειροκίνητα την παρουσία `Nette\Database\Explorer`: - -```php -// σύνδεση στη βάση δεδομένων -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// αποθήκη για την cache, υλοποιεί το Nette\Caching\Storage, π.χ.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// φροντίζει για την αντανάκλαση της δομής της βάσης δεδομένων -$structure = new Nette\Database\Structure($connection, $storage); -// ορίζει κανόνες για την αντιστοίχιση ονομάτων πινάκων, στηλών και ξένων κλειδιών -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Διαχείριση Σύνδεσης -=================== - -Κατά τη δημιουργία του αντικειμένου `Connection`, η σύνδεση πραγματοποιείται αυτόματα. Εάν θέλετε να καθυστερήσετε τη σύνδεση, χρησιμοποιήστε τη λειτουργία lazy - την ενεργοποιείτε στη [διαμόρφωση|configuration] ορίζοντας το `lazy: true`, ή ως εξής: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Για τη διαχείριση της σύνδεσης, χρησιμοποιήστε τις μεθόδους `connect()`, `disconnect()` και `reconnect()`. -- `connect()`: δημιουργεί τη σύνδεση, εάν δεν υπάρχει ήδη. Μπορεί να προκαλέσει εξαίρεση `Nette\Database\ConnectionException`. -- `disconnect()`: αποσυνδέει την τρέχουσα σύνδεση με τη βάση δεδομένων. -- `reconnect()`: πραγματοποιεί αποσύνδεση και στη συνέχεια επανασύνδεση με τη βάση δεδομένων. Αυτή η μέθοδος μπορεί επίσης να προκαλέσει εξαίρεση `Nette\Database\ConnectionException`. - -Επιπλέον, μπορείτε να παρακολουθείτε τα συμβάντα που σχετίζονται με τη σύνδεση χρησιμοποιώντας το συμβάν `onConnect`, το οποίο είναι ένας πίνακας callbacks που καλούνται μετά την εγκατάσταση της σύνδεσης με τη βάση δεδομένων. - -```php -// εκτελείται μετά τη σύνδεση στη βάση δεδομένων -$database->onConnect[] = function($database) { - echo "Συνδεθήκατε στη βάση δεδομένων"; // Connected to the database -}; -``` - - -Tracy Debug Bar -=============== - -Εάν χρησιμοποιείτε το [Tracy |tracy:], ενεργοποιείται αυτόματα ο πίνακας Database στη γραμμή Debug, ο οποίος εμφανίζει όλα τα εκτελεσμένα ερωτήματα, τις παραμέτρους τους, τον χρόνο εκτέλεσης και το σημείο στον κώδικα όπου κλήθηκαν. - -[* db-panel.webp *] diff --git a/database/el/mapping.texy b/database/el/mapping.texy deleted file mode 100644 index 0ab89d43e2..0000000000 --- a/database/el/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Μετατροπή Τύπων -*************** - -.[perex] -Το Nette Database μετατρέπει αυτόματα τις τιμές που επιστρέφονται από τη βάση δεδομένων στους αντίστοιχους τύπους PHP. - - -Ημερομηνία και Ώρα ------------------- - -Οι χρονικές τιμές μετατρέπονται σε αντικείμενα `Nette\Utils\DateTime`. Εάν θέλετε οι χρονικές τιμές να μετατρέπονται σε αμετάβλητα (immutable) αντικείμενα `DateTimeImmutable`, ορίστε την επιλογή `newDateTime: true` στη [διαμόρφωση|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'); -``` - -Στην περίπτωση της MySQL, ο τύπος δεδομένων `TIME` μετατρέπεται σε αντικείμενα `DateInterval`. - - -Boolean Τιμές -------------- - -Οι boolean τιμές μετατρέπονται αυτόματα σε `true` ή `false`. Στην MySQL, μετατρέπεται ο τύπος `TINYINT(1)` εάν ορίσουμε `convertBoolean: true` στη [διαμόρφωση|configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Αριθμητικές Τιμές ------------------ - -Οι αριθμητικές τιμές μετατρέπονται σε `int` ή `float` ανάλογα με τον τύπο της στήλης στη βάση δεδομένων: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Προσαρμοσμένη Κανονικοποίηση ----------------------------- - -Χρησιμοποιώντας τη μέθοδο `setRowNormalizer(?callable $normalizer)`, μπορείτε να ορίσετε μια προσαρμοσμένη συνάρτηση για τη μετατροπή των γραμμών από τη βάση δεδομένων. Αυτό είναι χρήσιμο, για παράδειγμα, για την αυτόματη μετατροπή τύπων δεδομένων. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // εδώ γίνεται η μετατροπή τύπων - return $row; -}); -``` diff --git a/database/el/reflection.texy b/database/el/reflection.texy deleted file mode 100644 index 909276b244..0000000000 --- a/database/el/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Αντανάκλαση Δομής -***************** - -.{data-version:3.2.1} -Το Nette Database παρέχει εργαλεία για την ενδοσκόπηση (introspection) της δομής της βάσης δεδομένων χρησιμοποιώντας την κλάση [api:Nette\Database\Structure]. Αυτή επιτρέπει τη λήψη πληροφοριών σχετικά με πίνακες, στήλες, ευρετήρια (indexes) και ξένα κλειδιά (foreign keys). Μπορείτε να χρησιμοποιήσετε την αντανάκλαση (reflection) για τη δημιουργία σχημάτων (schemas), τη δημιουργία ευέλικτων εφαρμογών που λειτουργούν με τη βάση δεδομένων ή γενικών εργαλείων βάσης δεδομένων. - -Λαμβάνουμε το αντικείμενο αντανάκλασης από την παρουσία της σύνδεσης με τη βάση δεδομένων: - -```php -$reflection = $database->getReflection(); -``` - - -Λήψη Πινάκων ------------- - -Η ιδιότητα readonly `$reflection->tables` περιέχει έναν συσχετιστικό πίνακα όλων των πινάκων στη βάση δεδομένων: - -```php -// Εμφάνιση ονομάτων όλων των πινάκων -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Υπάρχουν δύο ακόμη διαθέσιμες μέθοδοι: - -```php -// Έλεγχος ύπαρξης πίνακα -if ($reflection->hasTable('users')) { - echo "Ο πίνακας users υπάρχει"; // Table users exists -} - -// Επιστρέφει το αντικείμενο του πίνακα. αν δεν υπάρχει, προκαλεί εξαίρεση -$table = $reflection->getTable('users'); -``` - - -Πληροφορίες για τον Πίνακα --------------------------- - -Ο πίνακας αντιπροσωπεύεται από το αντικείμενο [Table|api:Nette\Database\Reflection\Table], το οποίο παρέχει τις ακόλουθες ιδιότητες readonly: - -- `$name: string` – όνομα του πίνακα -- `$view: bool` – εάν πρόκειται για προβολή (view) -- `$fullName: ?string` – πλήρες όνομα του πίνακα συμπεριλαμβανομένου του σχήματος (εάν υπάρχει) -- `$columns: array<string, Column>` – συσχετιστικός πίνακας στηλών του πίνακα -- `$indexes: Index[]` – πίνακας ευρετηρίων του πίνακα -- `$primaryKey: ?Index` – πρωτεύον κλειδί του πίνακα ή null -- `$foreignKeys: ForeignKey[]` – πίνακας ξένων κλειδιών του πίνακα - - -Στήλες ------- - -Η ιδιότητα `columns` του πίνακα παρέχει έναν συσχετιστικό πίνακα στηλών, όπου το κλειδί είναι το όνομα της στήλης και η τιμή είναι μια παρουσία [Column|api:Nette\Database\Reflection\Column] με τις ακόλουθες ιδιότητες: - -- `$name: string` – όνομα της στήλης -- `$table: ?Table` – αναφορά στον πίνακα της στήλης -- `$nativeType: string` – εγγενής τύπος δεδομένων της βάσης δεδομένων -- `$size: ?int` – μέγεθος/μήκος του τύπου -- `$nullable: bool` – εάν η στήλη μπορεί να περιέχει NULL -- `$default: mixed` – προεπιλεγμένη τιμή της στήλης -- `$autoIncrement: bool` – εάν η στήλη είναι auto-increment -- `$primary: bool` – εάν αποτελεί μέρος του πρωτεύοντος κλειδιού -- `$vendor: array` – πρόσθετα μεταδεδομένα ειδικά για το συγκεκριμένο σύστημα βάσης δεδομένων - -```php -foreach ($table->columns as $name => $column) { - echo "Στήλη: $name\n"; // Column: - echo "Τύπος: {$column->nativeType}\n"; // Type: - echo "Nullable: " . ($column->nullable ? 'Ναι' : 'Όχι') . "\n"; // Nullable: Yes / No -} -``` - - -Ευρετήρια ---------- - -Η ιδιότητα `indexes` του πίνακα παρέχει έναν πίνακα ευρετηρίων, όπου κάθε ευρετήριο είναι μια παρουσία [Index|api:Nette\Database\Reflection\Index] με τις ακόλουθες ιδιότητες: - -- `$columns: Column[]` – πίνακας στηλών που αποτελούν το ευρετήριο -- `$unique: bool` – εάν το ευρετήριο είναι μοναδικό -- `$primary: bool` – εάν πρόκειται για πρωτεύον κλειδί -- `$name: ?string` – όνομα του ευρετηρίου - -Το πρωτεύον κλειδί του πίνακα μπορεί να ληφθεί χρησιμοποιώντας την ιδιότητα `primaryKey`, η οποία επιστρέφει είτε ένα αντικείμενο `Index`, είτε `null` στην περίπτωση που ο πίνακας δεν έχει πρωτεύον κλειδί. - -```php -// Εμφάνιση ευρετηρίων -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Ευρετήριο" . ($index->name ? " {$index->name}" : '') . ":\n"; // Index - echo " Στήλες: $columns\n"; // Columns: - echo " Μοναδικό: " . ($index->unique ? 'Ναι' : 'Όχι') . "\n"; // Unique: Yes / No -} - -// Εμφάνιση πρωτεύοντος κλειδιού -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Πρωτεύον κλειδί: $columns\n"; // Primary key: -} -``` - - -Ξένα κλειδιά ------------- - -Η ιδιότητα `foreignKeys` του πίνακα παρέχει έναν πίνακα ξένων κλειδιών, όπου κάθε ξένο κλειδί είναι μια παρουσία [ForeignKey|api:Nette\Database\Reflection\ForeignKey] με τις ακόλουθες ιδιότητες: - -- `$foreignTable: Table` – ο πίνακας στον οποίο γίνεται αναφορά -- `$localColumns: Column[]` – πίνακας τοπικών στηλών -- `$foreignColumns: Column[]` – πίνακας στηλών στις οποίες γίνεται αναφορά -- `$name: ?string` – όνομα του ξένου κλειδιού - -```php -// Εμφάνιση ξένων κλειδιών -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/el/security.texy b/database/el/security.texy deleted file mode 100644 index 8fda175c50..0000000000 --- a/database/el/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Κίνδυνοι Ασφαλείας -****************** - -<div class=perex> - -Η βάση δεδομένων συχνά περιέχει ευαίσθητα δεδομένα και επιτρέπει την εκτέλεση επικίνδυνων λειτουργιών. Για την ασφαλή εργασία με το Nette Database είναι κρίσιμο: - -- Να κατανοήσετε τη διαφορά μεταξύ ασφαλούς και μη ασφαλούς API -- Να χρησιμοποιείτε παραμετροποιημένα ερωτήματα -- Να επικυρώνετε σωστά τα δεδομένα εισόδου - -</div> - - -Τι είναι το SQL Injection; -========================== - -Το SQL injection είναι ο σοβαρότερος κίνδυνος ασφαλείας κατά την εργασία με τη βάση δεδομένων. Προκύπτει όταν η μη επεξεργασμένη είσοδος από τον χρήστη γίνεται μέρος ενός ερωτήματος SQL. Ο εισβολέας μπορεί να εισάγει δικές του εντολές SQL και έτσι: -- Να αποκτήσει μη εξουσιοδοτημένη πρόσβαση σε δεδομένα -- Να τροποποιήσει ή να διαγράψει δεδομένα στη βάση δεδομένων -- Να παρακάμψει τον έλεγχο ταυτότητας - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - ευάλωτος σε SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Ο εισβολέας μπορεί να εισάγει για παράδειγμα την τιμή: ' OR '1'='1 -// Το τελικό ερώτημα θα είναι: SELECT * FROM users WHERE name = '' OR '1'='1' -// Το οποίο επιστρέφει όλους τους χρήστες -``` - -Το ίδιο ισχύει και για το Database Explorer: - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - ευάλωτος σε SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Παραμετροποιημένα Ερωτήματα -=========================== - -Η βασική άμυνα κατά του SQL injection είναι τα παραμετροποιημένα ερωτήματα. Το Nette Database προσφέρει διάφορους τρόπους χρήσης τους. - -Ο απλούστερος τρόπος είναι η χρήση **placeholders ερωτηματικών (?)**: - -```php -// ✅ Ασφαλές παραμετροποιημένο ερώτημα -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Ασφαλής συνθήκη στο Explorer -$table->where('name = ?', $name); -``` - -Αυτό ισχύει για όλες τις άλλες μεθόδους στο [Database Explorer|explorer], που επιτρέπουν την εισαγωγή εκφράσεων με placeholders ερωτηματικά και παραμέτρους. - -Για εντολές INSERT, UPDATE ή τη ρήτρα WHERE, μπορούμε να περάσουμε τις τιμές σε έναν πίνακα: - -```php -// ✅ Ασφαλές INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Ασφαλές INSERT στο Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Επικύρωση Τιμών Παραμέτρων -========================== - -Τα παραμετροποιημένα ερωτήματα είναι ο θεμελιώδης λίθος της ασφαλούς εργασίας με τη βάση δεδομένων. Ωστόσο, οι τιμές που εισάγουμε σε αυτά πρέπει να περάσουν από διάφορα επίπεδα ελέγχου: - - -Έλεγχος Τύπου -------------- - -**Το πιο σημαντικό είναι να διασφαλιστεί ο σωστός τύπος δεδομένων των παραμέτρων** - αυτό είναι απαραίτητη προϋπόθεση για την ασφαλή χρήση του Nette Database. Η βάση δεδομένων υποθέτει ότι όλα τα δεδομένα εισόδου έχουν τον σωστό τύπο δεδομένων που αντιστοιχεί στη συγκεκριμένη στήλη. - -Για παράδειγμα, εάν το `$name` στα προηγούμενα παραδείγματα ήταν απροσδόκητα ένας πίνακας αντί για μια συμβολοσειρά, το Nette Database θα προσπαθούσε να εισάγει όλα τα στοιχεία του στο ερώτημα SQL, οδηγώντας σε σφάλμα. Επομένως, **ποτέ μην χρησιμοποιείτε** μη επικυρωμένα δεδομένα από `$_GET`, `$_POST` ή `$_COOKIE` απευθείας σε ερωτήματα βάσης δεδομένων. - - -Έλεγχος Μορφής --------------- - -Στο δεύτερο επίπεδο, ελέγχουμε τη μορφή των δεδομένων - για παράδειγμα, εάν οι συμβολοσειρές είναι σε κωδικοποίηση UTF-8 και το μήκος τους αντιστοιχεί στον ορισμό της στήλης, ή εάν οι αριθμητικές τιμές βρίσκονται εντός του επιτρεπόμενου εύρους για τον συγκεκριμένο τύπο δεδομένων της στήλης. - -Σε αυτό το επίπεδο επικύρωσης, μπορούμε εν μέρει να βασιστούμε και στην ίδια τη βάση δεδομένων - πολλές βάσεις δεδομένων απορρίπτουν μη έγκυρα δεδομένα. Ωστόσο, η συμπεριφορά μπορεί να διαφέρει, κάποιες μπορεί να περικόψουν σιωπηλά μακριές συμβολοσειρές ή να κόψουν αριθμούς εκτός εύρους. - - -Έλεγχος τομέα -------------- - -Το τρίτο επίπεδο αντιπροσωπεύουν οι λογικοί έλεγχοι που είναι ειδικοί για την εφαρμογή σας. Για παράδειγμα, η επαλήθευση ότι οι τιμές από τα select boxes αντιστοιχούν στις προσφερόμενες επιλογές, ότι οι αριθμοί βρίσκονται στο αναμενόμενο εύρος (π.χ. ηλικία 0-150 ετών) ή ότι οι αμοιβαίες εξαρτήσεις μεταξύ των τιμών έχουν νόημα. - - -Συνιστώμενοι Τρόποι Επικύρωσης ------------------------------- - -- Χρησιμοποιήστε [Nette Forms|forms:], που εξασφαλίζουν αυτόματα τη σωστή επικύρωση όλων των εισόδων -- Χρησιμοποιήστε [Presenters|application:] και δηλώστε τους τύπους δεδομένων για τις παραμέτρους στις μεθόδους `action*()` και `render*()` -- Ή υλοποιήστε το δικό σας επίπεδο επικύρωσης χρησιμοποιώντας τυπικά εργαλεία PHP όπως το `filter_var()` - - -Ασφαλής Εργασία με Στήλες -========================= - -Στην προηγούμενη ενότητα, δείξαμε πώς να επικυρώνουμε σωστά τις τιμές των παραμέτρων. Ωστόσο, κατά τη χρήση πινάκων σε ερωτήματα SQL, πρέπει να δώσουμε την ίδια προσοχή και στα κλειδιά τους. - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - τα κλειδιά στον πίνακα δεν έχουν υποστεί επεξεργασία -$database->query('INSERT INTO users', $_POST); -``` - -Στις εντολές INSERT και UPDATE, αυτό αποτελεί κρίσιμο σφάλμα ασφαλείας - ο εισβολέας μπορεί να εισάγει ή να αλλάξει οποιαδήποτε στήλη στη βάση δεδομένων. Θα μπορούσε, για παράδειγμα, να ορίσει `is_admin = 1` ή να εισάγει αυθαίρετα δεδομένα σε ευαίσθητες στήλες (η λεγόμενη Mass Assignment Vulnerability). - -Στις συνθήκες WHERE, είναι ακόμη πιο επικίνδυνο, επειδή μπορεί να περιέχουν τελεστές: - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - τα κλειδιά στον πίνακα δεν έχουν υποστεί επεξεργασία -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// εκτελεί το ερώτημα WHERE (`salary` > 100000) -``` - -Ο εισβολέας μπορεί να χρησιμοποιήσει αυτή την προσέγγιση για να ανακαλύψει συστηματικά τους μισθούς των υπαλλήλων. Μπορεί να ξεκινήσει, για παράδειγμα, με ένα ερώτημα για μισθούς πάνω από 100.000, στη συνέχεια κάτω από 50.000, και με σταδιακή στένωση του εύρους, μπορεί να αποκαλύψει τους κατά προσέγγιση μισθούς όλων των υπαλλήλων. Αυτός ο τύπος επίθεσης ονομάζεται SQL enumeration. - -Οι μέθοδοι `where()` και `whereOr()` είναι ακόμη [πολύ πιο ευέλικτες |explorer#where] και υποστηρίζουν εκφράσεις SQL στα κλειδιά και τις τιμές, συμπεριλαμβανομένων τελεστών και συναρτήσεων. Αυτό δίνει στον εισβολέα τη δυνατότητα να εκτελέσει SQL injection: - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - ο εισβολέας μπορεί να εισάγει δικό του SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// εκτελεί το ερώτημα WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Αυτή η επίθεση τερματίζει την αρχική συνθήκη χρησιμοποιώντας `0)`, προσαρτά το δικό της `SELECT` χρησιμοποιώντας `UNION` για να αποκτήσει ευαίσθητα δεδομένα από τον πίνακα `users` και κλείνει το συντακτικά σωστό ερώτημα χρησιμοποιώντας `WHERE (1)`. - - -Whitelist Στηλών ----------------- - -Για την ασφαλή εργασία με ονόματα στηλών, χρειαζόμαστε έναν μηχανισμό που να διασφαλίζει ότι ο χρήστης μπορεί να εργαστεί μόνο με τις επιτρεπόμενες στήλες και δεν μπορεί να προσθέσει δικές του. Θα μπορούσαμε να προσπαθήσουμε να ανιχνεύσουμε και να μπλοκάρουμε επικίνδυνα ονόματα στηλών (blacklist), αλλά αυτή η προσέγγιση είναι αναξιόπιστη - ο εισβολέας μπορεί πάντα να βρει έναν νέο τρόπο να γράψει ένα επικίνδυνο όνομα στήλης που δεν είχαμε προβλέψει. - -Επομένως, είναι πολύ πιο ασφαλές να αντιστρέψουμε τη λογική και να ορίσουμε μια ρητή λίστα επιτρεπόμενων στηλών (whitelist): - -```php -// Στήλες που μπορεί να επεξεργαστεί ο χρήστης -$allowedColumns = ['name', 'email', 'active']; - -// Φιλτράρουμε τα δεδομένα εισόδου για να κρατήσουμε μόνο τα επιτρεπόμενα κλειδιά -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Τώρα μπορούμε να τα χρησιμοποιήσουμε με ασφάλεια σε ερωτήματα, όπως: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Δυναμικά Αναγνωριστικά -====================== - -Για δυναμικά ονόματα πινάκων και στηλών, χρησιμοποιήστε το placeholder `?name`. Αυτό εξασφαλίζει τη σωστή διαφυγή (escaping) των αναγνωριστικών σύμφωνα με τη σύνταξη της συγκεκριμένης βάσης δεδομένων (π.χ. χρησιμοποιώντας ανάποδα εισαγωγικά `` ` `` στην MySQL): - -```php -// ✅ Ασφαλής χρήση αξιόπιστων αναγνωριστικών -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Αποτέλεσμα στην MySQL: SELECT `name` FROM `users` -``` - -Σημαντικό: χρησιμοποιήστε το σύμβολο `?name` μόνο για αξιόπιστες τιμές που ορίζονται στον κώδικα της εφαρμογής. Για τιμές από τον χρήστη, χρησιμοποιήστε ξανά τη [whitelist |#Whitelist Στηλών]. Διαφορετικά, εκτίθεστε σε κινδύνους ασφαλείας: - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟ - ποτέ μην χρησιμοποιείτε είσοδο από τον χρήστη -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/el/sql-way.texy b/database/el/sql-way.texy deleted file mode 100644 index 1cfab7b050..0000000000 --- a/database/el/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -Πρόσβαση SQL -************ - -.[perex] -Η Nette Database προσφέρει δύο τρόπους: μπορείτε να γράψετε μόνοι σας ερωτήματα SQL (πρόσβαση SQL) ή να τα αφήσετε να δημιουργηθούν αυτόματα (βλ. [Explorer |explorer]). Η πρόσβαση SQL σάς δίνει πλήρη έλεγχο των ερωτημάτων, εξασφαλίζοντας ταυτόχρονα την ασφαλή σύνταξή τους. - -.[note] -Λεπτομέρειες σχετικά με τη σύνδεση και τη διαμόρφωση της βάσης δεδομένων θα βρείτε στο κεφάλαιο [Σύνδεση και διαμόρφωση |guide#Σύνδεση και Διαμόρφωση]. - - -Βασικά ερωτήματα -================ - -Για την υποβολή ερωτημάτων στη βάση δεδομένων, χρησιμοποιείται η μέθοδος `query()`. Αυτή επιστρέφει ένα αντικείμενο [ResultSet |api:Nette\Database\ResultSet], το οποίο αντιπροσωπεύει το αποτέλεσμα του ερωτήματος. Σε περίπτωση αποτυχίας, η μέθοδος [προκαλεί εξαίρεση |exceptions]. Μπορούμε να διατρέξουμε το αποτέλεσμα του ερωτήματος χρησιμοποιώντας έναν βρόχο `foreach` ή να χρησιμοποιήσουμε κάποια από τις [βοηθητικές συναρτήσεις |#Λήψη δεδομένων]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Για την ασφαλή εισαγωγή τιμών σε ερωτήματα SQL, χρησιμοποιούμε παραμετροποιημένα ερωτήματα. Η Nette Database τα καθιστά εξαιρετικά απλά - αρκεί να προσθέσετε ένα κόμμα και την τιμή μετά το ερώτημα SQL: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Με περισσότερες παραμέτρους, έχετε δύο επιλογές σύνταξης. Μπορείτε είτε να "διανθίσετε" το ερώτημα SQL με παραμέτρους: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Ή να γράψετε πρώτα ολόκληρο το ερώτημα SQL και στη συνέχεια να επισυνάψετε όλες τις παραμέτρους: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Προστασία από SQL injection -=========================== - -Γιατί είναι σημαντικό να χρησιμοποιείτε παραμετροποιημένα ερωτήματα; Επειδή σας προστατεύουν από την επίθεση που ονομάζεται SQL injection, κατά την οποία ο εισβολέας θα μπορούσε να εισάγει δικές του εντολές SQL και έτσι να αποκτήσει ή να καταστρέψει δεδομένα στη βάση δεδομένων. - -.[warning] -**Ποτέ μην εισάγετε μεταβλητές απευθείας στο ερώτημα SQL!** Πάντα να χρησιμοποιείτε παραμετροποιημένα ερωτήματα, τα οποία σας προστατεύουν από το SQL injection. - -```php -// ❌ ΕΠΙΚΙΝΔΥΝΟΣ ΚΩΔΙΚΑΣ - ευάλωτος σε SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Ασφαλές παραμετροποιημένο ερώτημα -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Ενημερωθείτε για τους [πιθανούς κινδύνους ασφαλείας |security]. - - -Τεχνικές ερωτημάτων -=================== - - -Συνθήκες WHERE --------------- - -Μπορείτε να γράψετε τις συνθήκες WHERE ως έναν συσχετιστικό πίνακα (associative array), όπου τα κλειδιά είναι τα ονόματα των στηλών και οι τιμές είναι τα δεδομένα για σύγκριση. Η Nette Database επιλέγει αυτόματα τον καταλληλότερο τελεστή SQL ανάλογα με τον τύπο της τιμής. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -Στο κλειδί, μπορείτε επίσης να καθορίσετε ρητά τον τελεστή για σύγκριση: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // χρησιμοποιεί τον τελεστή > - 'name LIKE' => '%John%', // χρησιμοποιεί τον τελεστή LIKE - 'email NOT LIKE' => '%example.com%', // χρησιμοποιεί τον τελεστή NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Το Nette χειρίζεται αυτόματα ειδικές περιπτώσεις όπως τιμές `null` ή πίνακες. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // χρησιμοποιεί τον τελεστή = - 'category_id' => [1, 2, 3], // χρησιμοποιεί το IN - 'description' => null, // χρησιμοποιεί το IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Για αρνητικές συνθήκες, χρησιμοποιήστε τον τελεστή `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // χρησιμοποιεί τον τελεστή <> - 'category_id NOT' => [1, 2, 3], // χρησιμοποιεί το NOT IN - 'description NOT' => null, // χρησιμοποιεί το IS NOT NULL - 'id' => [], // παραλείπεται -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Για τη σύνδεση συνθηκών, χρησιμοποιείται ο τελεστής `AND`. Αυτό μπορεί να αλλάξει χρησιμοποιώντας το [placeholder ?or |#Hints για τη σύνταξη SQL]. - - -Κανόνες ORDER BY ----------------- - -Η ταξινόμηση `ORDER BY` μπορεί να γραφτεί χρησιμοποιώντας έναν πίνακα. Στα κλειδιά, αναφέρουμε τις στήλες και η τιμή θα είναι μια boolean τιμή που καθορίζει εάν θα ταξινομηθεί αύξουσα: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // αύξουσα - 'name' => false, // φθίνουσα -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Εισαγωγή δεδομένων (INSERT) ---------------------------- - -Για την εισαγωγή εγγραφών, χρησιμοποιείται η εντολή SQL `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Η μέθοδος `getInsertId()` επιστρέφει το ID της τελευταίας εισαχθείσας γραμμής. Σε ορισμένες βάσεις δεδομένων (π.χ. PostgreSQL), είναι απαραίτητο να καθορίσετε ως παράμετρο το όνομα της ακολουθίας (sequence) από την οποία θα δημιουργηθεί το ID χρησιμοποιώντας `$database->getInsertId($sequenceId)`. - -Ως παραμέτρους μπορούμε επίσης να περάσουμε [#Ειδικές τιμές] όπως αρχεία, αντικείμενα DateTime ή τύπους enum. - -Εισαγωγή πολλαπλών εγγραφών ταυτόχρονα: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Η πολλαπλή INSERT είναι πολύ ταχύτερη, επειδή εκτελείται ένα μόνο ερώτημα βάσης δεδομένων, αντί για πολλά μεμονωμένα. - -**Προειδοποίηση ασφαλείας:** Ποτέ μην χρησιμοποιείτε μη επικυρωμένα δεδομένα ως `$values`. Ενημερωθείτε για τους [πιθανούς κινδύνους |security#Ασφαλής Εργασία με Στήλες]. - - -Ενημέρωση δεδομένων (UPDATE) ----------------------------- - -Για την ενημέρωση εγγραφών, χρησιμοποιείται η εντολή SQL `UPDATE`. - -```php -// Ενημέρωση μίας εγγραφής -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Ο αριθμός των επηρεασμένων γραμμών επιστρέφεται από το `$result->getRowCount()`. - -Για το UPDATE, μπορούμε να χρησιμοποιήσουμε τους τελεστές `+=` και `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // αύξηση του login_count -], 1); -``` - -Παράδειγμα εισαγωγής ή τροποποίησης εγγραφής, εάν υπάρχει ήδη. Χρησιμοποιούμε την τεχνική `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 -``` - -Παρατηρήστε ότι η Nette Database αναγνωρίζει σε ποιο πλαίσιο της εντολής SQL εισάγουμε την παράμετρο με τον πίνακα και ανάλογα συνθέτει τον κώδικα SQL. Έτσι, από τον πρώτο πίνακα συνέθεσε `(id, name, year) VALUES (123, 'Jim', 1978)`, ενώ τον δεύτερο τον μετέτρεψε στη μορφή `name = 'Jim', year = 1978`. Αυτό το εξετάζουμε λεπτομερέστερα στην ενότητα [#Hints για τη σύνταξη SQL]. - - -Διαγραφή δεδομένων (DELETE) ---------------------------- - -Για τη διαγραφή εγγραφών, χρησιμοποιείται η εντολή SQL `DELETE`. Παράδειγμα με λήψη του αριθμού των διαγραμμένων γραμμών: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Hints για τη σύνταξη SQL ------------------------- - -Ένα hint είναι ένα ειδικό placeholder στο ερώτημα SQL που λέει πώς πρέπει να μεταγραφεί η τιμή της παραμέτρου σε έκφραση SQL: - -| Hint | Περιγραφή | Χρησιμοποιείται αυτόματα -|-----------|-------------------------------------------------|----------------------------- -| `?name` | χρησιμοποιείται για την εισαγωγή ονόματος πίνακα ή στήλης | - -| `?values` | δημιουργεί `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | δημιουργεί ανάθεση `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | συνδέει συνθήκες στον πίνακα με τον τελεστή `AND` | `WHERE ?`, `HAVING ?` -| `?or` | συνδέει συνθήκες στον πίνακα με τον τελεστή `OR` | - -| `?order` | δημιουργεί τη ρήτρα `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Για τη δυναμική εισαγωγή ονομάτων πινάκων και στηλών στο ερώτημα, χρησιμοποιείται το placeholder `?name`. Η Nette Database φροντίζει για τη σωστή επεξεργασία των αναγνωριστικών σύμφωνα με τις συμβάσεις της συγκεκριμένης βάσης δεδομένων (π.χ. κλείσιμο σε ανάποδα εισαγωγικά `` ` `` στην MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (στην MySQL) -``` - -**Προειδοποίηση:** χρησιμοποιήστε το σύμβολο `?name` μόνο για ονόματα πινάκων και στηλών από επικυρωμένες εισόδους, διαφορετικά εκτίθεστε σε [κίνδυνο ασφαλείας |security#Δυναμικά Αναγνωριστικά]. - -Τα υπόλοιπα hints συνήθως δεν χρειάζεται να αναφέρονται, καθώς το Nette χρησιμοποιεί έξυπνη αυτόματη ανίχνευση κατά τη σύνθεση του ερωτήματος SQL (βλ. τρίτη στήλη του πίνακα). Αλλά μπορείτε να το χρησιμοποιήσετε, για παράδειγμα, σε μια κατάσταση όπου θέλετε να συνδέσετε συνθήκες χρησιμοποιώντας `OR` αντί για `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' -``` - - -Ειδικές τιμές -------------- - -Εκτός από τους συνήθεις σκαλωτούς τύπους (string, int, bool), μπορείτε να περάσετε και ειδικές τιμές ως παραμέτρους: - -- αρχεία: `fopen('image.gif', 'r')` εισάγει το δυαδικό περιεχόμενο του αρχείου -- ημερομηνία και ώρα: τα αντικείμενα `DateTime` μετατρέπονται στη μορφή της βάσης δεδομένων -- τύποι enum: οι παρουσίες `enum` μετατρέπονται στην τιμή τους -- SQL literals: δημιουργημένα με `Connection::literal('NOW()')` εισάγονται απευθείας στο ερώτημα - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Σε βάσεις δεδομένων που δεν έχουν εγγενή υποστήριξη για τον τύπο δεδομένων `datetime` (όπως SQLite και Oracle), το `DateTime` μετατρέπεται στην τιμή που καθορίζεται στη [διαμόρφωση της βάσης δεδομένων |configuration] με την επιλογή `formatDateTime` (η προεπιλεγμένη τιμή είναι `U` - unix timestamp). - - -SQL Literals ------------- - -Σε ορισμένες περιπτώσεις, πρέπει να αναφέρετε απευθείας κώδικα SQL ως τιμή, ο οποίος όμως δεν πρέπει να θεωρηθεί ως συμβολοσειρά και να υποστεί escaping. Για αυτό χρησιμεύουν τα αντικείμενα της κλάσης `Nette\Database\SqlLiteral`. Τα δημιουργεί η μέθοδος `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()) -``` - -Ή εναλλακτικά: - -```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 μπορούν να περιέχουν παραμέτρους: - -```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) -``` - -Χάρη σε αυτό, μπορούμε να δημιουργήσουμε ενδιαφέροντες συνδυασμούς: - -```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') -``` - - -Λήψη δεδομένων -============== - - -Συντομεύσεις για ερωτήματα SELECT ---------------------------------- - -Για την απλοποίηση της ανάκτησης δεδομένων, το `Connection` προσφέρει αρκετές συντομεύσεις που συνδυάζουν την κλήση `query()` με την ακόλουθη `fetch*()`. Αυτές οι μέθοδοι δέχονται τις ίδιες παραμέτρους με το `query()`, δηλαδή το ερώτημα SQL και προαιρετικές παραμέτρους. Μια πλήρης περιγραφή των μεθόδων `fetch*()` βρίσκεται [παρακάτω |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Εκτελεί το ερώτημα και επιστρέφει την πρώτη γραμμή ως αντικείμενο `Row` -| `fetchAll($sql, ...$params): array` | Εκτελεί το ερώτημα και επιστρέφει όλες τις γραμμές ως πίνακα αντικειμένων `Row` -| `fetchPairs($sql, ...$params): array` | Εκτελεί το ερώτημα και επιστρέφει έναν συσχετιστικό πίνακα, όπου η πρώτη στήλη αντιπροσωπεύει το κλειδί και η δεύτερη την τιμή -| `fetchField($sql, ...$params): mixed` | Εκτελεί το ερώτημα και επιστρέφει την τιμή του πρώτου πεδίου από την πρώτη γραμμή -| `fetchList($sql, ...$params): ?array` | Εκτελεί το ερώτημα και επιστρέφει την πρώτη γραμμή ως αριθμημένο πίνακα - -Παράδειγμα: - -```php -// fetchField() - επιστρέφει την τιμή του πρώτου κελιού -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - επανάληψη μέσω γραμμών ----------------------------------- - -Μετά την εκτέλεση του ερωτήματος, επιστρέφεται ένα αντικείμενο [ResultSet |api:Nette\Database\ResultSet], το οποίο επιτρέπει την περιήγηση στα αποτελέσματα με διάφορους τρόπους. Ο ευκολότερος τρόπος για να εκτελέσετε ένα ερώτημα και να λάβετε τις γραμμές είναι με επανάληψη σε έναν βρόχο `foreach`. Αυτός ο τρόπος είναι ο πιο αποδοτικός από πλευράς μνήμης, καθώς επιστρέφει τα δεδομένα σταδιακά και δεν τα αποθηκεύει όλα στη μνήμη ταυτόχρονα. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -Το `ResultSet` μπορεί να επαναληφθεί μόνο μία φορά. Εάν χρειάζεται να επαναλάβετε πολλές φορές, πρέπει πρώτα να φορτώσετε τα δεδομένα σε έναν πίνακα, για παράδειγμα χρησιμοποιώντας τη μέθοδο `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Επιστρέφει μια γραμμή ως αντικείμενο `Row`. Εάν δεν υπάρχουν άλλες γραμμές, επιστρέφει `null`. Μετακινεί τον εσωτερικό δείκτη στην επόμενη γραμμή. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // φορτώνει την πρώτη γραμμή -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Επιστρέφει όλες τις υπόλοιπες γραμμές από το `ResultSet` ως πίνακα αντικειμένων `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // φορτώνει όλες τις γραμμές -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Επιστρέφει τα αποτελέσματα ως συσχετιστικό πίνακα. Το πρώτο όρισμα καθορίζει το όνομα της στήλης που θα χρησιμοποιηθεί ως κλειδί στον πίνακα, το δεύτερο όρισμα καθορίζει το όνομα της στήλης που θα χρησιμοποιηθεί ως τιμή: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Εάν αναφέρουμε μόνο την πρώτη παράμετρο, η τιμή θα είναι ολόκληρη η γραμμή, δηλαδή το αντικείμενο `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -Σε περίπτωση διπλότυπων κλειδιών, χρησιμοποιείται η τιμή από την τελευταία γραμμή. Κατά τη χρήση `null` ως κλειδί, ο πίνακας θα αριθμηθεί αριθμητικά από το μηδέν (τότε δεν προκύπτουν συγκρούσεις): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Εναλλακτικά, μπορείτε να δώσετε ως παράμετρο ένα callback, το οποίο θα επιστρέφει για κάθε γραμμή είτε την ίδια την τιμή, είτε ένα ζεύγος κλειδιού-τιμής. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Το callback μπορεί επίσης να επιστρέψει έναν πίνακα με ένα ζεύγος κλειδιού & τιμής: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Επιστρέφει την τιμή του πρώτου πεδίου από την τρέχουσα γραμμή. Εάν δεν υπάρχουν άλλες γραμμές, επιστρέφει `null`. Μετακινεί τον εσωτερικό δείκτη στην επόμενη γραμμή. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // φορτώνει το όνομα από την πρώτη γραμμή -``` - - -fetchList(): ?array .[method] ------------------------------ - -Επιστρέφει μια γραμμή ως αριθμημένο πίνακα. Εάν δεν υπάρχουν άλλες γραμμές, επιστρέφει `null`. Μετακινεί τον εσωτερικό δείκτη στην επόμενη γραμμή. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Επιστρέφει τον αριθμό των επηρεασμένων γραμμών από το τελευταίο ερώτημα `UPDATE` ή `DELETE`. Για το `SELECT`, είναι ο αριθμός των επιστρεφόμενων γραμμών, αλλά αυτός μπορεί να μην είναι γνωστός - σε αυτή την περίπτωση, η μέθοδος επιστρέφει `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Επιστρέφει τον αριθμό των στηλών στο `ResultSet`. - - -Πληροφορίες για τα ερωτήματα -============================ - -Για σκοπούς εντοπισμού σφαλμάτων, μπορούμε να λάβουμε πληροφορίες σχετικά με το τελευταίο εκτελεσμένο ερώτημα: - -```php -echo $database->getLastQueryString(); // εκτυπώνει το ερώτημα SQL - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // εκτυπώνει το ερώτημα SQL -echo $result->getTime(); // εκτυπώνει τον χρόνο εκτέλεσης σε δευτερόλεπτα -``` - -Για την εμφάνιση του αποτελέσματος ως πίνακα HTML, μπορείτε να χρησιμοποιήσετε: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -Το ResultSet προσφέρει πληροφορίες σχετικά με τους τύπους των στηλών: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column είναι τύπου $type->type"; // π.χ. 'id είναι τύπου int' -} -``` - - -Καταγραφή ερωτημάτων --------------------- - -Μπορούμε να υλοποιήσουμε τη δική μας καταγραφή ερωτημάτων. Το συμβάν `onQuery` είναι ένας πίνακας callbacks που καλούνται μετά από κάθε εκτελεσμένο ερώτημα: - -```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/el/transactions.texy b/database/el/transactions.texy deleted file mode 100644 index ffdd9514af..0000000000 --- a/database/el/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Συναλλαγές (Transactions) -************************* - -.[perex] -Οι συναλλαγές εγγυώνται ότι είτε όλες οι λειτουργίες εντός της συναλλαγής θα εκτελεστούν, είτε καμία. Είναι χρήσιμες για τη διασφάλιση της συνέπειας των δεδομένων κατά τη διάρκεια πιο σύνθετων λειτουργιών. - -Ο απλούστερος τρόπος χρήσης συναλλαγών μοιάζει με αυτό: - -```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; -} -``` - -Μπορείτε να γράψετε το ίδιο πράγμα πολύ πιο κομψά χρησιμοποιώντας τη μέθοδο `transaction()`. Δέχεται μια επανάκληση (callback) ως παράμετρο, την οποία εκτελεί σε μια συναλλαγή. Εάν η επανάκληση εκτελεστεί χωρίς εξαίρεση, η συναλλαγή επιβεβαιώνεται αυτόματα (commit). Εάν προκύψει εξαίρεση, η συναλλαγή ακυρώνεται (rollback) και η εξαίρεση διαδίδεται περαιτέρω. - -```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' - ]); -}); -``` - -Η μέθοδος `transaction()` μπορεί επίσης να επιστρέψει τιμές: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // επιστρέφει τον αριθμό των ενημερωμένων γραμμών -}); -``` diff --git a/database/hu/@home.texy b/database/hu/@home.texy deleted file mode 100644 index fc880e7b35..0000000000 --- a/database/hu/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Támogatott adatbázisok -====================== - -A Nette a következő adatbázisokat támogatja: - -|* Adatbázis szerver |* DSN név |* Támogatás a Core-ban |* Támogatás az Explorerben -| MySQL (>= 5.1) | mysql | IGEN | IGEN -| PostgreSQL (>= 9.0) | pgsql | IGEN | IGEN -| Sqlite 3 (>= 3.8) | sqlite | IGEN | IGEN -| Oracle | oci | IGEN | - -| MS SQL (PDO_SQLSRV) | sqlsrv | IGEN | IGEN -| MS SQL (PDO_DBLIB) | mssql | IGEN | - -| ODBC | odbc | IGEN | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: A Nette Database jelentősen leegyszerűsíti az adatok lekérdezését az adatbázisból SQL lekérdezések írása nélkül. Hatékony lekérdezéseket tesz és nem továbbít felesleges adatokat.}} diff --git a/database/hu/@left-menu.texy b/database/hu/@left-menu.texy deleted file mode 100644 index 6bf3ca54b9..0000000000 --- a/database/hu/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Bevezetés |guide] -- [SQL hozzáférés |sql way] -- [Explorer |Explorer] -- [Tranzakciók |transactions] -- [Kivételek |exceptions] -- [Reflexió |reflection] -- [Leképezés |mapping] -- [Konfiguráció |configuration] -- [Biztonsági kockázatok |security] -- [Frissítés |en:upgrading] diff --git a/database/hu/@meta.texy b/database/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/database/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/database/hu/configuration.texy b/database/hu/configuration.texy deleted file mode 100644 index 7e539ca546..0000000000 --- a/database/hu/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Adatbázis konfiguráció -********************** - -.[perex] -A Nette Database konfigurációs lehetőségeinek áttekintése. - -Ha nem a teljes keretrendszert használja, csak ezt a könyvtárat, olvassa el, [hogyan töltse be a konfigurációt |bootstrap:]. - - -Egy kapcsolat -------------- - -Egy adatbázis-kapcsolat konfigurációja: - -```neon -database: - # DSN, az egyetlen kötelező kulcs - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Létrehozza a `Nette\Database\Connection` és `Nette\Database\Explorer` szolgáltatásokat, amelyeket általában [autowiringgel |dependency-injection:autowiring] adunk át, vagy hivatkozással a [nevükre |#DI Szolgáltatások]. - -További beállítások: - -```neon -database: - # megjelenítse az adatbázis panelt a Tracy Bar-ban? - debugger: ... # (bool) alapértelmezett true - - # megjelenítse az EXPLAIN lekérdezéseket a Tracy Bar-ban? - explain: ... # (bool) alapértelmezett true - - # engedélyezze az autowiringet ehhez a kapcsolathoz? - autowired: ... # (bool) alapértelmezett true az első kapcsolatnál - - # tábla konvenciók: discovered, static vagy osztálynév - conventions: discovered # (string) alapértelmezett 'discovered' - - options: - # csak akkor csatlakozzon az adatbázishoz, amikor szükséges? - lazy: ... # (bool) alapértelmezett false - - # PHP adatbázis-illesztőprogram osztálya - driverClass: # (string) - - # csak MySQL: beállítja az sql_mode-ot - sqlmode: # (string) - - # csak MySQL: beállítja a SET NAMES-t - charset: # (string) alapértelmezett 'utf8mb4' - - # csak MySQL: a TINYINT(1)-et bool-ra konvertálja - convertBoolean: # (bool) alapértelmezett false - - # a dátum oszlopokat immutable objektumokként adja vissza (3.2.1 verziótól) - newDateTime: # (bool) alapértelmezett false - - # csak Oracle és SQLite: dátum tárolási formátuma - formatDateTime: # (string) alapértelmezett 'U' -``` - -Az `options` kulcsban további opciókat adhat meg, amelyeket a [PDO illesztőprogramok dokumentációjában |https://www.php.net/manual/en/pdo.drivers.php] talál, például: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Több kapcsolat --------------- - -A konfigurációban több adatbázis-kapcsolatot is definiálhatunk, elnevezett szekciókra osztva: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Az autowiring csak az első szekcióból származó szolgáltatásoknál van bekapcsolva. Ezt meg lehet változtatni az `autowired: false` vagy `autowired: true` segítségével. - - -DI Szolgáltatások ------------------ - -Ezek a szolgáltatások kerülnek hozzáadásra a DI konténerhez, ahol a `###` a kapcsolat nevét jelöli: - -| Név | Típus | Leírás -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | adatbázis-kapcsolat -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Ha csak egy kapcsolatot definiálunk, a szolgáltatások nevei `database.default.connection` és `database.default.explorer` lesznek. Ha több kapcsolatot definiálunk, mint a fenti példában, a nevek megfelelnek a szekcióknak, azaz `database.main.connection`, `database.main.explorer`, valamint `database.another.connection` és `database.another.explorer`. - -A nem autowire-olt szolgáltatásokat explicit módon, a nevükre való hivatkozással adjuk át: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/hu/exceptions.texy b/database/hu/exceptions.texy deleted file mode 100644 index 790fc06199..0000000000 --- a/database/hu/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Kivételek -********* - -A Nette Database kivétel-hierarchiát használ. Az alaposztály a `Nette\Database\DriverException`, amely a `PDOException`-ből öröklődik, és kibővített lehetőségeket biztosít az adatbázis-hibák kezelésére: - -- A `getDriverCode()` metódus visszaadja az adatbázis-driver hibakódját. -- A `getSqlState()` metódus visszaadja az SQLSTATE kódot. -- A `getQueryString()` és `getParameters()` metódusok lehetővé teszik az eredeti lekérdezés és paramétereinek lekérését. - -A `DriverException`-ből a következő specializált kivételek öröklődnek: - -- `ConnectionException` - jelzi az adatbázis-szerverhez való csatlakozás sikertelenségét. -- `ConstraintViolationException` - alaposztály az adatbázis-korlátozások megsértéséhez, amelyből öröklődnek: - - `ForeignKeyConstraintViolationException` - idegen kulcs megsértése. - - `NotNullConstraintViolationException` - NOT NULL korlátozás megsértése. - - `UniqueConstraintViolationException` - érték egyediségének megsértése. - - -Példa a `UniqueConstraintViolationException` kivétel elkapására, amely akkor következik be, ha olyan e-mail címmel próbálunk meg felhasználót beszúrni, amely már létezik az adatbázisban (feltéve, hogy az email oszlopnak egyedi indexe van). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Már létezik felhasználó ezzel az e-mail címmel.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Hiba történt a regisztráció során: ' . $e->getMessage(); -} -``` diff --git a/database/hu/explorer.texy b/database/hu/explorer.texy deleted file mode 100644 index 5b46016d0c..0000000000 --- a/database/hu/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Az Explorer intuitív és hatékony módot kínál az adatbázissal való munkára. Automatikusan gondoskodik a táblák közötti kapcsolatokról és a lekérdezések optimalizálásáról, így Ön az alkalmazására koncentrálhat. Azonnal működik beállítás nélkül. Ha teljes kontrollra van szüksége az SQL lekérdezések felett, használhatja az [SQL megközelítést |SQL way]. - -- Az adatokkal való munka természetes és könnyen érthető. -- Optimalizált SQL lekérdezéseket generál, amelyek csak a szükséges adatokat töltik be. -- Lehetővé teszi a kapcsolódó adatokhoz való könnyű hozzáférést JOIN lekérdezések írása nélkül. -- Azonnal működik bármilyen konfiguráció vagy entitásgenerálás nélkül. - -</div> - - -Az Explorerrel a [api:Nette\Database\Explorer] objektum `table()` metódusának meghívásával kezdhet (a csatlakozás részleteit a [Csatlakozás és konfiguráció |guide#Csatlakozás és konfiguráció] fejezetben találja): - -```php -$books = $explorer->table('book'); // 'book' a tábla neve -``` - -A metódus egy [Selection |api:Nette\Database\Table\Selection] objektumot ad vissza, amely egy SQL lekérdezést képvisel. Erre az objektumra további metódusokat láncolhatunk az eredmények szűrésére és rendezésére. A lekérdezés csak akkor áll össze és fut le, amikor elkezdjük kérni az adatokat. Például egy `foreach` ciklussal történő bejáráskor. Minden sort egy [ActiveRow |api:Nette\Database\Table\ActiveRow] objektum képvisel: - -```php -foreach ($books as $book) { - echo $book->title; // a 'title' oszlop kiírása - echo $book->author_id; // az 'author_id' oszlop kiírása -} -``` - -Az Explorer alapvetően megkönnyíti a [táblák közötti kapcsolatokkal |#Kapcsolatok a táblák között] való munkát. A következő példa bemutatja, milyen könnyen tudunk adatokat kiírni összekapcsolt táblákból (könyvek és szerzőik). Figyelje meg, hogy nem kell semmilyen JOIN lekérdezést írnunk, a Nette létrehozza őket helyettünk: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Könyv: ' . $book->title; - echo 'Szerző: ' . $book->author->name; // JOIN-t hoz létre az 'author' táblára -} -``` - -A Nette Database Explorer optimalizálja a lekérdezéseket, hogy a lehető leghatékonyabbak legyenek. A fenti példa csak két SELECT lekérdezést hajt végre, függetlenül attól, hogy 10 vagy 10 000 könyvet dolgozunk fel. - -Ráadásul az Explorer figyeli, hogy mely oszlopokat használják a kódban, és csak azokat tölti be az adatbázisból, ezzel további teljesítményt takarítva meg. Ez a viselkedés teljesen automatikus és adaptív. Ha később módosítja a kódot, és elkezd további oszlopokat használni, az Explorer automatikusan módosítja a lekérdezéseket. Nem kell semmit beállítania, sem azon gondolkodnia, mely oszlopokra lesz szüksége - bízza ezt a Nette-re. - - -Szűrés és rendezés -================== - -A `Selection` osztály metódusokat biztosít az adatok kiválasztásának szűrésére és rendezésére. - -.[language-php] -| `where($condition, ...$params)` | WHERE feltételt ad hozzá. Több feltétel AND operátorral van összekötve. -| `whereOr(array $conditions)` | OR operátorral összekötött WHERE feltételek csoportját adja hozzá. -| `wherePrimary($value)` | WHERE feltételt ad hozzá az elsődleges kulcs alapján. -| `order($columns, ...$params)` | Beállítja az ORDER BY rendezést. -| `select($columns, ...$params)` | Meghatározza a betöltendő oszlopokat. -| `limit($limit, $offset = null)` | Korlátozza a sorok számát (LIMIT) és opcionálisan beállítja az OFFSET-et. -| `page($page, $itemsPerPage, &$total = null)` | Beállítja a lapozást. -| `group($columns, ...$params)` | Csoportosítja a sorokat (GROUP BY). -| `having($condition, ...$params)` | HAVING feltételt ad hozzá a csoportosított sorok szűréséhez. - -A metódusok láncolhatók (ún. [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfészek]): `$table->where(...)->order(...)->limit(...)`. - -Ezekben a metódusokban speciális jelölést is használhat a [kapcsolódó táblákból származó adatokhoz |#Lekérdezés kapcsolódó táblákon keresztül] való hozzáféréshez. - - -Escapelés és azonosítók ------------------------ - -A metódusok automatikusan escapelik a paramétereket és idézőjelek közé teszik az azonosítókat (tábla- és oszlopneveket), ezzel megakadályozva az SQL injectiont. A helyes működéshez néhány szabályt be kell tartani: - -- A kulcsszavakat, függvényneveket, eljárásneveket stb. **nagybetűkkel** írja. -- Az oszlop- és táblaneveket **kisbetűkkel** írja. -- A stringeket mindig **paramétereken** keresztül adja át. - -```php -where('name = ' . $name); // KRITIKUS SEBEZHETŐSÉG: SQL injection -where('name LIKE "%search%"'); // ROSSZ: bonyolítja az automatikus idézőjelezést -where('name LIKE ?', '%search%'); // HELYES: érték paraméteren keresztül átadva - -where('name like ?', $name); // ROSSZ: generálja: `name` `like` ? -where('name LIKE ?', $name); // HELYES: generálja: `name` LIKE ? -where('LOWER(name) = ?', $value);// HELYES: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Szűri az eredményeket WHERE feltételekkel. Erőssége az intelligens munka különböző típusú értékekkel és az SQL operátorok automatikus kiválasztása. - -Alapvető használat: - -```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' -``` - -A megfelelő operátorok automatikus felismerésének köszönhetően nem kell különböző speciális esetekkel foglalkoznunk. A Nette megoldja őket helyettünk: - -```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) -// operátor nélküli helyettesítő kérdőjelet is használhatunk: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -A metódus helyesen kezeli a negált feltételeket és az üres tömböket is: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- semmit sem talál -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- mindent megtalál -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- mindent megtalál -// $table->where('NOT id ?', $ids); Figyelem - ez a szintaxis nem támogatott -``` - -Paraméterként átadhatunk egy másik tábla eredményét is - al-lekérdezés jön létre: - -```php -// 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')); -``` - -A feltételeket tömbként is átadhatjuk, amelynek elemei AND-del lesznek összekötve: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -A tömbben használhatunk kulcs => érték párokat, és a Nette ismét automatikusan kiválasztja a megfelelő operátorokat: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -A tömbben kombinálhatunk SQL kifejezéseket helyettesítő kérdőjelekkel és több paraméterrel. Ez alkalmas komplex feltételekhez pontosan definiált operátorokkal: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // két paramétert tömbként adunk át -]); -``` - -A `where()` többszöri hívása automatikusan AND-del köti össze a feltételeket. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Hasonlóan a `where()`-hez, feltételeket ad hozzá, de azzal a különbséggel, hogy OR-ral köti össze őket: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Itt is használhatunk komplexebb kifejezéseket: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Feltételt ad hozzá a tábla elsődleges kulcsához: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Ha a táblának összetett elsődleges kulcsa van (pl. `foo_id`, `bar_id`), tömbként adjuk át: - -```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(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Meghatározza a sorok visszaadási sorrendjét. Rendezhetünk egy vagy több oszlop szerint, csökkenő vagy növekvő sorrendben, vagy saját kifejezés szerint: - -```php -$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(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Meghatározza az adatbázisból visszaadandó oszlopokat. Alapértelmezés szerint a Nette Database Explorer csak azokat az oszlopokat adja vissza, amelyeket ténylegesen használnak a kódban. A `select()` metódust olyan esetekben használjuk, amikor specifikus kifejezéseket kell visszaadnunk: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Az `AS` segítségével definiált aliasok ezután elérhetők az ActiveRow objektum tulajdonságaként: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // hozzáférés az aliashoz -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Korlátozza a visszaadott sorok számát (LIMIT), és opcionálisan lehetővé teszi az offset beállítását: - -```php -$table->limit(10); // LIMIT 10 (az első 10 sort adja vissza) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Lapozáshoz célszerűbb a `page()` metódust használni. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Megkönnyíti az eredmények lapozását. Elfogadja az oldal számát (1-től számolva) és az oldalankénti elemek számát. Opcionálisan átadható egy referencia egy változóra, amelybe az oldalak teljes száma kerül mentésre: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Összesen oldalak: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Csoportosítja a sorokat a megadott oszlopok szerint (GROUP BY). Általában aggregáló függvényekkel együtt használják: - -```php -// Megszámolja a termékek számát minden kategóriában -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Feltételt állít be a csoportosított sorok szűréséhez (HAVING). Használható a `group()` metódussal és aggregáló függvényekkel együtt: - -```php -// Megtalálja azokat a kategóriákat, amelyek több mint 100 termékkel rendelkeznek -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Adatok olvasása -=============== - -Az adatok adatbázisból történő olvasásához számos hasznos metódus áll rendelkezésre: - -.[language-php] -| `foreach ($table as $key => $row)` | Iterál az összes soron, `$key` az elsődleges kulcs értéke, `$row` egy ActiveRow objektum -| `$row = $table->get($key)` | Visszaad egy sort az elsődleges kulcs alapján -| `$row = $table->fetch()` | Visszaadja az aktuális sort és a mutatót a következőre lépteti -| `$array = $table->fetchPairs()` | Asszociatív tömböt hoz létre az eredményekből -| `$array = $table->fetchAll()` | Visszaadja az összes sort tömbként -| `count($table)` | Visszaadja a sorok számát a Selection objektumban - -Az [ActiveRow |api:Nette\Database\Table\ActiveRow] objektum csak olvasásra szolgál. Ez azt jelenti, hogy nem lehet módosítani a tulajdonságainak értékeit. Ez a korlátozás biztosítja az adatok konzisztenciáját és megakadályozza a váratlan mellékhatásokat. Az adatok az adatbázisból töltődnek be, és bármilyen változtatást explicit módon és ellenőrzötten kell végrehajtani. - - -`foreach` - iteráció az összes soron ------------------------------------- - -A legegyszerűbb módja a lekérdezés végrehajtásának és a sorok megszerzésének a `foreach` ciklussal történő iterálás. Automatikusan elindítja az SQL lekérdezést. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key az elsődleges kulcs értéke, $book egy ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Végrehajtja az SQL lekérdezést és visszaadja a sort az elsődleges kulcs alapján, vagy `null`-t, ha nem létezik. - -```php -$book = $explorer->table('book')->get(123); // visszaadja az ActiveRow-t 123 ID-vel vagy null-t -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Visszaadja a sort és a belső mutatót a következőre lépteti. Ha már nincsenek további sorok, `null`-t ad vissza. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Visszaadja az eredményeket asszociatív tömbként. Az első argumentum határozza meg annak az oszlopnak a nevét, amely kulcsként lesz használva a tömbben, a második argumentum pedig annak az oszlopnak a nevét, amely értékként lesz használva: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Ha csak az első paramétert adjuk meg, az érték az egész sor lesz, azaz az `ActiveRow` objektum: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -Duplikált kulcsok esetén az utolsó sor értéke lesz használva. Ha `null`-t használunk kulcsként, a tömb numerikusan lesz indexelve nullától kezdve (ekkor nem történik ütközés): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternatívaként megadhat egy callbacket paraméterként, amely minden sorhoz vagy magát az értéket, vagy egy kulcs-érték párt ad vissza. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Első könyv (János Novak)', ...] - -// A callback visszaadhat egy tömböt is kulcs & érték párral: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Első könyv' => 'János Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Visszaadja az összes sort `ActiveRow` objektumok asszociatív tömbjeként, ahol a kulcsok az elsődleges kulcsok értékei. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -A `count()` metódus paraméter nélkül visszaadja a sorok számát a `Selection` objektumban: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternatíva -``` - -Figyelem, a `count()` paraméterrel aggregáló COUNT függvényt hajt végre az adatbázisban, lásd alább. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Átalakítja az `ActiveRow` objektumot asszociatív tömbbé, ahol a kulcsok az oszlopnevek, az értékek pedig a megfelelő adatok. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray lesz ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Aggregáció -========== - -A `Selection` osztály metódusokat biztosít az aggregáló függvények (COUNT, SUM, MIN, MAX, AVG stb.) egyszerű végrehajtásához. - -.[language-php] -| `count($expr)` | Megszámolja a sorok számát -| `min($expr)` | Visszaadja a minimális értéket egy oszlopban -| `max($expr)` | Visszaadja a maximális értéket egy oszlopban -| `sum($expr)` | Visszaadja az értékek összegét egy oszlopban -| `aggregation($function)` | Lehetővé teszi tetszőleges aggregáló függvény végrehajtását. Pl. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Végrehajt egy SQL lekérdezést a COUNT függvénnyel és visszaadja az eredményt. A metódus arra használatos, hogy megállapítsuk, hány sor felel meg egy bizonyos feltételnek: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Figyelem, a [#count()] paraméter nélkül csak a sorok számát adja vissza a `Selection` objektumban. - - -min(string $expr) és max(string $expr) .[method] ------------------------------------------------- - -A `min()` és `max()` metódusok visszaadják a minimális és maximális értéket a megadott oszlopban vagy kifejezésben: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Visszaadja az értékek összegét a megadott oszlopban vagy kifejezésben: - -```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] ----------------------------------------------------------------------- - -Lehetővé teszi tetszőleges aggregáló függvény végrehajtását. - -```php -// termékek átlagos ára egy kategóriában -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// összekapcsolja a termék címkéit egyetlen stringgé -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Ha olyan eredményeket kell aggregálnunk, amelyek már maguk is valamilyen aggregáló függvényből és csoportosításból származnak (pl. `SUM(érték)` csoportosított sorokon keresztül), második argumentumként megadjuk azt az aggregáló függvényt, amelyet ezekre a köztes eredményekre kell alkalmazni: - -```php -// Kiszámítja a raktáron lévő termékek teljes árát az egyes kategóriákra, majd összeadja ezeket az árakat. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -Ebben a példában először kiszámítjuk a termékek teljes árát minden kategóriában (`SUM(price * stock) AS category_total`), és csoportosítjuk az eredményeket a `category_id` szerint. Ezután az `aggregation('SUM(category_total)', 'SUM')` segítségével összeadjuk ezeket a `category_total` köztes összegeket. A második argumentum `'SUM'` azt mondja, hogy a köztes eredményekre a SUM függvényt kell alkalmazni. - - -Beszúrás, Frissítés és Törlés -============================= - -A Nette Database Explorer leegyszerűsíti az adatok beszúrását, frissítését és törlését. Minden említett metódus kivételt `Nette\Database\DriverException` dob hiba esetén. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Új rekordokat szúr be a táblába. - -**Egy rekord beszúrása:** - -Az új rekordot asszociatív tömbként vagy iterable objektumként (például az [űrlapokban |forms:] használt ArrayHash) adjuk át, ahol a kulcsok megfelelnek a tábla oszlopneveinek. - -Ha a táblának definiált elsődleges kulcsa van, a metódus egy `ActiveRow` objektumot ad vissza, amely újra betöltődik az adatbázisból, hogy figyelembe vegye az adatbázis szintjén végrehajtott esetleges változásokat (triggerek, oszlopok alapértelmezett értékei, auto-increment oszlopok számításai). Ez biztosítja az adatok konzisztenciáját, és az objektum mindig az aktuális adatokat tartalmazza az adatbázisból. Ha nincs egyértelmű elsődleges kulcsa, a átadott adatokat tömb formájában adja vissza. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row egy ActiveRow példány, és tartalmazza a beszúrt sor teljes adatait, -// beleértve az automatikusan generált ID-t és a triggerek által végrehajtott esetleges változásokat -echo $row->id; // Kiírja az újonnan beszúrt felhasználó ID-ját -echo $row->created_at; // Kiírja a létrehozás idejét, ha trigger állította be -``` - -**Több rekord beszúrása egyszerre:** - -A `insert()` metódus lehetővé teszi több rekord beszúrását egyetlen SQL lekérdezéssel. Ebben az esetben a beszúrt sorok számát adja vissza. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows értéke 2 lesz -``` - -Paraméterként átadható egy `Selection` objektum is adatkiválasztással. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Speciális értékek beszúrása:** - -Értékként átadhatunk fájlokat, DateTime objektumokat vagy SQL literálokat is: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // adatbázis formátumra konvertálja - 'avatar' => fopen('image.jpg', 'rb'), // beszúrja a fájl bináris tartalmát - 'uuid' => $explorer::literal('UUID()'), // meghívja az UUID() függvényt -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Frissíti a tábla sorait a megadott szűrő szerint. Visszaadja a ténylegesen megváltozott sorok számát. - -A módosítandó oszlopokat asszociatív tömbként vagy iterable objektumként (például az [űrlapokban |forms:] használt ArrayHash) adjuk át, ahol a kulcsok megfelelnek a tábla oszlopneveinek: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Numerikus értékek módosításához használhatjuk a `+=` és `-=` operátorokat: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // növeli a 'points' oszlop értékét 1-gyel - 'coins-=' => 1, // csökkenti a 'coins' oszlop értékét 1-gyel - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Törli a tábla sorait a megadott szűrő szerint. Visszaadja a törölt sorok számát. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Az `update()` és `delete()` hívásakor ne felejtse el a `where()` segítségével megadni a módosítandó/törlendő sorokat. Ha nem használja a `where()`-t, a művelet az egész táblán végrehajtódik! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Frissíti az adatokat az `ActiveRow` objektum által képviselt adatbázis-sorban. Paraméterként egy iterable-t fogad el a frissítendő adatokkal (a kulcsok az oszlopnevek). Numerikus értékek módosításához használhatjuk a `+=` és `-=` operátorokat: - -A frissítés végrehajtása után az `ActiveRow` automatikusan újra betöltődik az adatbázisból, hogy figyelembe vegye az adatbázis szintjén végrehajtott esetleges változásokat (pl. triggerek). A metódus csak akkor ad vissza true-t, ha tényleges adatváltozás történt. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // növeljük a megtekintések számát -]); -echo $article->views; // Kiírja az aktuális megtekintések számát -``` - -Ez a metódus csak egyetlen konkrét sort frissít az adatbázisban. Több sor tömeges frissítéséhez használja a [#Selection::update()] metódust. - - -ActiveRow::delete() .[method] ------------------------------ - -Törli az adatbázisból azt a sort, amelyet az `ActiveRow` objektum képvisel. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Törli az 1-es ID-jű könyvet -``` - -Ez a metódus csak egyetlen konkrét sort töröl az adatbázisból. Több sor tömeges törléséhez használja a [#Selection::delete()] metódust. - - -Kapcsolatok a táblák között -=========================== - -Relációs adatbázisokban az adatok több táblára vannak osztva, és idegen kulcsok segítségével kapcsolódnak egymáshoz. A Nette Database Explorer forradalmi módot kínál ezekkel a kapcsolatokkal való munkára - JOIN lekérdezések írása és bármi konfigurálása vagy generálása nélkül. - -A kapcsolatokkal való munka illusztrálására egy könyvadatbázis példáját használjuk ([megtalálható a GitHubon |https://github.com/nette-examples/books]). Az adatbázisban a következő táblák vannak: - -- `author` - írók és fordítók (oszlopok: `id`, `name`, `web`, `born`) -- `book` - könyvek (oszlopok: `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - címkék (oszlopok: `id`, `name`) -- `book_tag` - kapcsolótábla a könyvek és címkék között (oszlopok: `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** A példákban használt adatbázis struktúra *** - -A könyvadatbázis példánkban több típusú kapcsolatot találunk (bár a modell egyszerűsített a valósághoz képest): - -- Egy-a-többhöz 1:N – minden könyvnek **egy** szerzője van, egy szerző **több** könyvet írhat -- Nulla-a-többhöz 0:N – egy könyvnek **lehet** fordítója, egy fordító **több** könyvet fordíthat -- Nulla-az-egyhez 0:1 – egy könyvnek **lehet** folytatása -- Több-a-többhöz M:N – egy könyvnek **több** címkéje lehet, és egy címke **több** könyvhöz rendelhető - -Ezekben a kapcsolatokban mindig van egy szülő és egy gyermek tábla. Például a szerző és a könyv közötti kapcsolatban az `author` tábla a szülő, a `book` pedig a gyermek - elképzelhetjük úgy, hogy a könyv mindig "tartozik" valamilyen szerzőhöz. Ez megmutatkozik az adatbázis struktúrájában is: a gyermek `book` tábla tartalmaz egy `author_id` idegen kulcsot, amely a szülő `author` táblára hivatkozik. - -Ha ki kell listáznunk a könyveket a szerzőik nevével együtt, két lehetőségünk van. Vagy egyetlen SQL lekérdezéssel szerezzük meg az adatokat JOIN segítségével: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Vagy két lépésben töltjük be az adatokat - először a könyveket, majd a szerzőiket - és utána PHP-ban összerakjuk őket: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- a megszerzett könyvek szerzőinek id-jai -``` - -A második megközelítés valójában hatékonyabb, bár ez meglepő lehet. Az adatok csak egyszer töltődnek be, és jobban felhasználhatók a cache-ben. Pontosan így működik a Nette Database Explorer - mindent a felszín alatt old meg, és elegáns API-t kínál Önnek: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'cím: ' . $book->title; - echo 'írta: ' . $book->author->name; // $book->author egy rekord az 'author' táblából - echo 'fordította: ' . $book->translator?->name; -} -``` - - -Hozzáférés a szülő táblához ---------------------------- - -A szülő táblához való hozzáférés egyszerű. Olyan kapcsolatokról van szó, mint *a könyvnek van szerzője* vagy *a könyvnek lehet fordítója*. A kapcsolódó rekordot az ActiveRow objektum property-jén keresztül érjük el - a neve megegyezik az idegen kulcsot tartalmazó oszlop nevével `id` nélkül: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // megtalálja a szerzőt az author_id oszlop alapján -echo $book->translator?->name; // megtalálja a fordítót a translator_id alapján -``` - -Amikor hozzáférünk a `$book->author` property-hez, az Explorer a `book` táblában keres egy oszlopot, amelynek neve tartalmazza az `author` stringet (tehát `author_id`). Az ebben az oszlopban lévő érték alapján betölti a megfelelő rekordot az `author` táblából, és `ActiveRow`-ként adja vissza. Hasonlóan működik a `$book->translator` is, amely a `translator_id` oszlopot használja. Mivel a `translator_id` oszlop tartalmazhat `null`-t, a kódban a `?->` operátort használjuk. - -Alternatív utat kínál a `ref()` metódus, amely két argumentumot fogad el, a cél tábla nevét és a kapcsoló oszlop nevét, és egy `ActiveRow` példányt vagy `null`-t ad vissza: - -```php -echo $book->ref('author', 'author_id')->name; // kapcsolat a szerzővel -echo $book->ref('author', 'translator_id')->name; // kapcsolat a fordítóval -``` - -A `ref()` metódus akkor hasznos, ha nem lehet a property-n keresztüli hozzáférést használni, mert a tábla tartalmaz egy azonos nevű oszlopot (azaz `author`). Más esetekben a property-n keresztüli hozzáférés használata javasolt, amely olvashatóbb. - -Az Explorer automatikusan optimalizálja az adatbázis-lekérdezéseket. Amikor ciklusban járjuk be a könyveket, és hozzáférünk a kapcsolódó rekordjaikhoz (szerzők, fordítók), az Explorer nem generál lekérdezést minden egyes könyvhöz külön. Ehelyett csak egy SELECT-et hajt végre minden kapcsolattípushoz, ezzel jelentősen csökkentve az adatbázis terhelését. Például: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Ez a kód csak ezt a három villámgyors lekérdezést hívja meg az adatbázisba: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id-k a kiválasztott könyvek author_id oszlopából -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id-k a kiválasztott könyvek translator_id oszlopából -``` - -.[note] -A kapcsoló oszlop megtalálásának logikáját a [Conventions |api:Nette\Database\Conventions] implementációja határozza meg. Javasoljuk a [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions] használatát, amely elemzi az idegen kulcsokat, és lehetővé teszi a táblák közötti meglévő kapcsolatokkal való egyszerű munkát. - - -Hozzáférés a gyermek táblához ------------------------------ - -A gyermek táblához való hozzáférés fordított irányban működik. Most azt kérdezzük, *milyen könyveket írt ez a szerző* vagy *fordított ez a fordító*. Ehhez a lekérdezéstípushoz a `related()` metódust használjuk, amely egy `Selection`-t ad vissza a kapcsolódó rekordokkal. Nézzünk egy példát: - -```php -$author = $explorer->table('author')->get(1); - -// Kiírja a szerző összes könyvét -foreach ($author->related('book.author_id') as $book) { - echo "Írta: $book->title"; -} - -// Kiírja az összes könyvet, amelyet a szerző fordított -foreach ($author->related('book.translator_id') as $book) { - echo "Fordította: $book->title"; -} -``` - -A `related()` metódus a kapcsolat leírását egyetlen argumentumként pont-jelöléssel vagy két különálló argumentumként fogadja el: - -```php -$author->related('book.translator_id'); // egy argumentum -$author->related('book', 'translator_id'); // két argumentum -``` - -Az Explorer képes automatikusan felismerni a helyes kapcsoló oszlopot a szülő tábla neve alapján. Ebben az esetben a `book.author_id` oszlopon keresztül kapcsolódik, mivel a forrástábla neve `author`: - -```php -$author->related('book'); // a book.author_id-t használja -``` - -Ha több lehetséges kapcsolat létezne, az Explorer [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] kivételt dob. - -A `related()` metódust természetesen használhatjuk több rekord ciklusban történő bejárásakor is, és az Explorer ebben az esetben is automatikusan optimalizálja a lekérdezéseket: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' írta:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Ez a kód csak két villámgyors SQL lekérdezést generál: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- a kiválasztott szerzők id-jai -``` - - -Több-a-többhöz kapcsolat ------------------------- - -A több-a-többhöz (M:N) kapcsolathoz szükség van egy kapcsolótábla létezésére (esetünkben `book_tag`), amely két idegen kulcsot tartalmazó oszlopot (`book_id`, `tag_id`) tartalmaz. Ezen oszlopok mindegyike az összekapcsolt táblák egyikének elsődleges kulcsára hivatkozik. A kapcsolódó adatok megszerzéséhez először a kapcsolótábla rekordjait szerezzük meg a `related('book_tag')` segítségével, majd tovább haladunk a céladatokhoz: - -```php -$book = $explorer->table('book')->get(1); -// kiírja a könyvhöz rendelt címkék neveit -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // kiírja a címke nevét a kapcsolótáblán keresztül -} - -$tag = $explorer->table('tag')->get(1); -// vagy fordítva: kiírja az ezzel a címkével megjelölt könyvek neveit -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // kiírja a könyv nevét -} -``` - -Az Explorer ismét optimalizálja az SQL lekérdezéseket hatékony formába: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- a kiválasztott könyvek id-jai -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- a book_tag-ban talált címkék id-jai -``` - - -Lekérdezés kapcsolódó táblákon keresztül ----------------------------------------- - -A `where()`, `select()`, `order()` és `group()` metódusokban speciális jelöléseket használhatunk más táblák oszlopaihoz való hozzáféréshez. Az Explorer automatikusan létrehozza a szükséges JOIN-okat. - -**Pont-jelölés** (`szülő_tábla.oszlop`) a gyermek tábla szemszögéből nézett 1:N kapcsolathoz használatos: - -```php -$books = $explorer->table('book'); - -// Megtalálja azokat a könyveket, amelyek szerzőjének neve 'Jon'-nal kezdődik -$books->where('author.name LIKE ?', 'Jon%'); - -// Rendezi a könyveket a szerző neve szerint csökkenő sorrendben -$books->order('author.name DESC'); - -// Kiírja a könyv címét és a szerző nevét -$books->select('book.title, author.name'); -``` - -**Kettőspont-jelölés** (`:gyermek_tábla.oszlop`) a szülő tábla szemszögéből nézett 1:N kapcsolathoz használatos: - -```php -$authors = $explorer->table('author'); - -// Megtalálja azokat a szerzőket, akik 'PHP'-t tartalmazó című könyvet írtak -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Megszámolja a könyvek számát minden szerzőhöz -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -A fenti példában a kettőspont-jelöléssel (`:book.title`) nincs megadva az idegen kulcs oszlopa. Az Explorer automatikusan felismeri a helyes oszlopot a szülő tábla neve alapján. Ebben az esetben a `book.author_id` oszlopon keresztül kapcsolódik, mivel a forrástábla neve `author`. Ha több lehetséges kapcsolat létezne, az Explorer [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException] kivételt dob. - -A kapcsoló oszlopot explicit módon meg lehet adni zárójelben: - -```php -// Megtalálja azokat a szerzőket, akik 'PHP'-t tartalmazó című könyvet fordítottak -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -A jelölések láncolhatók több táblán keresztüli hozzáféréshez: - -```php -// Megtalálja a 'PHP' címkével megjelölt könyvek szerzőit -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -JOIN feltételek bővítése ------------------------- - -A `joinWhere()` metódus kibővíti azokat a feltételeket, amelyeket a táblák összekapcsolásakor az SQL-ben az `ON` kulcsszó után adunk meg. - -Tegyük fel, hogy egy adott fordító által fordított könyveket szeretnénk megtalálni: - -```php -// Megtalálja a 'David' nevű fordító által fordított könyveket -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -A `joinWhere()` feltételben ugyanazokat a konstrukciókat használhatjuk, mint a `where()` metódusban - operátorokat, helyettesítő kérdőjeleket, értékek tömbjét vagy SQL kifejezéseket. - -Összetettebb lekérdezésekhez több JOIN-nal definiálhatunk tábla aliasokat: - -```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) -``` - -Figyelje meg, hogy míg a `where()` metódus feltételeket ad hozzá a `WHERE` záradékhoz, a `joinWhere()` metódus kibővíti a feltételeket az `ON` záradékban a táblák összekapcsolásakor. diff --git a/database/hu/guide.texy b/database/hu/guide.texy deleted file mode 100644 index b7e2e44944..0000000000 --- a/database/hu/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -A Nette Database egy erőteljes és elegáns adatbázis réteg PHP számára, hangsúlyt fektetve az egyszerűségre és az okos funkciókra. Kétféle módot kínál az adatbázissal való munkára - [Explorer |Explorer] az alkalmazások gyors fejlesztéséhez, vagy [SQL megközelítés |SQL way] a lekérdezésekkel való közvetlen munkához. - -<div class="grid gap-3"> -<div> - - -[SQL megközelítés |SQL way] -=========================== -- Biztonságos paraméterezett lekérdezések -- Pontos ellenőrzés az SQL lekérdezések formája felett -- Amikor komplex lekérdezéseket ír haladó funkciókkal -- Optimalizálja a teljesítményt specifikus SQL funkciók segítségével - -</div> - -<div> - - -[Explorer |Explorer] -==================== -- Gyorsan fejleszthet SQL írása nélkül -- Intuitív munka a táblák közötti kapcsolatokkal -- Értékelni fogja a lekérdezések automatikus optimalizálását -- Alkalmas gyors és kényelmes adatbázis-kezelésre - -</div> - -</div> - - -Telepítés -========= - -A könyvtárat a [Composer|best-practices:composer] eszközzel töltheti le és telepítheti: - -```shell -composer require nette/database -``` - - -Támogatott adatbázisok -====================== - -A Nette Database a következő adatbázisokat támogatja: - -|* Adatbázis szerver |* DSN név |* Támogatás az Explorerben -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | IGEN -| PostgreSQL (>= 9.0) | pgsql | IGEN -| Sqlite 3 (>= 3.8) | sqlite | IGEN -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | IGEN -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Két megközelítés az adatbázishoz -================================ - -A Nette Database választási lehetőséget kínál: vagy közvetlenül írhat SQL lekérdezéseket (SQL megközelítés), vagy hagyhatja, hogy automatikusan generálódjanak (Explorer). Nézzük meg, hogyan oldják meg mindkét megközelítéssel ugyanazokat a feladatokat: - -[SQL megközelítés|sql way] - SQL lekérdezések - -```php -// rekord beszúrása -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// rekordok lekérése: könyvek szerzői -$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 -'); - -// listázás (nem optimális, N további lekérdezést generál) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Szerző $author->name írt $author->books_count könyvet:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer megközelítés |explorer] - automatikus SQL generálás - -```php -// rekord beszúrása -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// rekordok lekérése: könyvek szerzői -$authors = $database->table('authors') - ->where('active', 1); - -// listázás (automatikusan csak 2 optimalizált lekérdezést generál) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Szerző $author->name írt {$books->count()} könyvet:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Az Explorer megközelítés automatikusan generálja és optimalizálja az SQL lekérdezéseket. A megadott példában az SQL megközelítés N+1 lekérdezést generál (egyet a szerzőkhöz, majd egyet minden szerző könyveihez), míg az Explorer automatikusan optimalizálja a lekérdezéseket, és csak kettőt hajt végre - egyet a szerzőkhöz és egyet az összes könyvükhöz. - -Mindkét megközelítés tetszés szerint kombinálható az alkalmazásban, igény szerint. - - -Csatlakozás és konfiguráció -=========================== - -Az adatbázishoz való csatlakozáshoz elegendő létrehozni egy [api:Nette\Database\Connection] osztálypéldányt: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -A `$dsn` (data source name) paraméter ugyanaz, [amit a PDO használ |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], pl. `host=127.0.0.1;dbname=test`. Hiba esetén `Nette\Database\ConnectionException` kivételt dob. - -Azonban egy ügyesebb módszert kínál az [alkalmazáskonfiguráció |configuration], ahová elegendő hozzáadni egy `database` szekciót, és létrejönnek a szükséges objektumok, valamint az adatbázis panel a [Tracy |tracy:] sávban. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Ezután a kapcsolat objektumot [szolgáltatásként kapjuk meg a DI konténerből |dependency-injection:passing-dependencies], pl.: - -```php -class Model -{ - public function __construct( - // vagy Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -További információk az [adatbázis konfigurációjáról |configuration]. - - -Explorer manuális létrehozása ------------------------------ - -Ha nem használja a Nette DI konténert, manuálisan is létrehozhat egy `Nette\Database\Explorer` példányt: - -```php -// csatlakozás az adatbázishoz -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// tároló a cache-hez, implementálja a Nette\Caching\Storage-ot, pl.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// gondoskodik az adatbázis struktúra reflexiójáról -$structure = new Nette\Database\Structure($connection, $storage); -// definiálja a táblanevek, oszlopnevek és idegen kulcsok leképezési szabályait -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Kapcsolatkezelés -================ - -A `Connection` objektum létrehozásakor a csatlakozás automatikusan megtörténik. Ha késleltetni szeretné a csatlakozást, használja a lazy módot - ezt a [konfigurációban |configuration] a `lazy` beállításával, vagy így kapcsolhatja be: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -A kapcsolat kezeléséhez használja a `connect()`, `disconnect()` és `reconnect()` metódusokat. -- `connect()` létrehozza a kapcsolatot, ha még nem létezik, és `Nette\Database\ConnectionException` kivételt dobhat. -- `disconnect()` megszakítja az aktuális adatbázis-kapcsolatot. -- `reconnect()` megszakítja, majd újra csatlakoztatja az adatbázishoz. Ez a metódus szintén `Nette\Database\ConnectionException` kivételt dobhat. - -Ezenkívül figyelheti a csatlakozással kapcsolatos eseményeket az `onConnect` esemény segítségével, amely egy callback tömb, amely az adatbázissal való kapcsolat létrejötte után hívódik meg. - -```php -// az adatbázishoz való csatlakozás után fut le -$database->onConnect[] = function($database) { - echo "Csatlakozva az adatbázishoz"; -}; -``` - - -Tracy Debug Bar -=============== - -Ha [Tracy-t |tracy:] használ, a Database panel automatikusan aktiválódik a Debug sávban, amely megjeleníti az összes végrehajtott lekérdezést, azok paramétereit, végrehajtási idejét és a kódban való meghívásuk helyét. - -[* db-panel.webp *] diff --git a/database/hu/mapping.texy b/database/hu/mapping.texy deleted file mode 100644 index 1e96f83fbb..0000000000 --- a/database/hu/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Típuskonverzió -************** - -.[perex] -A Nette Database automatikusan konvertálja az adatbázisból visszaadott értékeket a megfelelő PHP típusokra. - - -Dátum és idő ------------- - -Az időadatok `Nette\Utils\DateTime` objektumokká konvertálódnak. Ha azt szeretné, hogy az időadatok immutable `Nette\Database\DateTime` objektumokká konvertálódjanak, állítsa a `newDateTime` opciót true-ra a [konfigurációban |configuration]. - -```php -$row = $database->fetch('SELECT created_at FROM articles'); -echo $row->created_at instanceof DateTime; // true -echo $row->created_at->format('Y. n. j.'); -``` - -MySQL esetén a `TIME` adattípust `DateInterval` objektumokká konvertálja. - - -Logikai értékek ---------------- - -A logikai értékek automatikusan `true`-ra vagy `false`-ra konvertálódnak. MySQL esetén a `TINYINT(1)` konvertálódik, ha a [konfigurációban |configuration] beállítjuk a `convertBoolean`-t. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Numerikus értékek ------------------ - -A numerikus értékek `int`-re vagy `float`-ra konvertálódnak az adatbázis oszlopának típusa szerint: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Egyéni normalizálás -------------------- - -A `setRowNormalizer(?callable $normalizer)` metódussal beállíthat egy egyéni funkciót az adatbázisból származó sorok átalakítására. Ez hasznos lehet például az adattípusok automatikus konvertálásához. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // itt történik a típuskonverzió - return $row; -}); -``` diff --git a/database/hu/reflection.texy b/database/hu/reflection.texy deleted file mode 100644 index ad380ba29b..0000000000 --- a/database/hu/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Struktúra reflexió -****************** - -.{data-version:3.2.1} -A Nette Database eszközöket biztosít az adatbázis struktúrájának introspekciójához a [api:Nette\Database\Reflection] osztály segítségével. Ez lehetővé teszi információk lekérését táblákról, oszlopokról, indexekről és idegen kulcsokról. A reflexiót használhatja sémák generálásához, rugalmas, adatbázissal dolgozó alkalmazások létrehozásához vagy általános adatbázis-eszközök készítéséhez. - -A reflexiós objektumot az adatbázis-kapcsolat példányából kapjuk meg: - -```php -$reflection = $database->getReflection(); -``` - - -Táblák lekérése ---------------- - -A `$reflection->tables` readonly property tartalmazza az adatbázis összes táblájának asszociatív tömbjét: - -```php -// Az összes tábla nevének kiírása -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Két további metódus is rendelkezésre áll: - -```php -// Tábla létezésének ellenőrzése -if ($reflection->hasTable('users')) { - echo "A users tábla létezik"; -} - -// Visszaadja a tábla objektumot; ha nem létezik, kivételt dob -$table = $reflection->getTable('users'); -``` - - -Információ a tábláról ---------------------- - -A táblát egy [Table|api:Nette\Database\Reflection\Table] objektum reprezentálja, amely a következő readonly property-ket biztosítja: - -- `$name: string` – tábla neve -- `$view: bool` – hogy nézetről van-e szó -- `$fullName: ?string` – a tábla teljes neve, beleértve a sémát (ha létezik) -- `$columns: array<string, Column>` – a tábla oszlopainak asszociatív tömbje -- `$indexes: Index[]` – a tábla indexeinek tömbje -- `$primaryKey: ?Index` – a tábla elsődleges kulcsa vagy null -- `$foreignKeys: ForeignKey[]` – a tábla idegen kulcsainak tömbje - - -Oszlopok --------- - -A tábla `columns` property-je az oszlopok asszociatív tömbjét adja meg, ahol a kulcs az oszlop neve, az érték pedig egy [Column|api:Nette\Database\Reflection\Column] példány a következő property-kkel: - -- `$name: string` – oszlop neve -- `$table: ?Table` – referencia az oszlop táblájára -- `$nativeType: string` – natív adatbázis típus -- `$size: ?int` – a típus mérete/hossza -- `$nullable: bool` – hogy az oszlop tartalmazhat-e NULL-t -- `$default: mixed` – az oszlop alapértelmezett értéke -- `$autoIncrement: bool` – hogy az oszlop auto-increment-e -- `$primary: bool` – hogy része-e az elsődleges kulcsnak -- `$vendor: array` – további, az adott adatbázis-rendszerre specifikus metaadatok - -```php -foreach ($table->columns as $name => $column) { - echo "Oszlop: $name\n"; - echo "Típus: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Igen' : 'Nem') . "\n"; -} -``` - - -Indexek -------- - -A tábla `indexes` property-je az indexek tömbjét adja meg, ahol minden index egy [Index|api:Nette\Database\Reflection\Index] példány a következő property-kkel: - -- `$columns: Column[]` – az indexet alkotó oszlopok tömbje -- `$unique: bool` – hogy az index egyedi-e -- `$primary: bool` – hogy elsődleges kulcsról van-e szó -- `$name: ?string` – az index neve - -A tábla elsődleges kulcsát a `primaryKey` property segítségével lehet lekérni, amely vagy egy `Index` objektumot ad vissza, vagy `null`-t, ha a táblának nincs elsődleges kulcsa. - -```php -// Indexek kiírása -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Oszlopok: $columns\n"; - echo " Unique: " . ($index->unique ? 'Igen' : 'Nem') . "\n"; -} - -// Elsődleges kulcs kiírása -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Elsődleges kulcs: $columns\n"; -} -``` - - -Idegen kulcsok --------------- - -A tábla `foreignKeys` property-je az idegen kulcsok tömbjét adja meg, ahol minden idegen kulcs egy [ForeignKey|api:Nette\Database\Reflection\ForeignKey] példány a következő property-kkel: - -- `$foreignTable: Table` – a hivatkozott tábla -- `$localColumns: Column[]` – a helyi oszlopok tömbje -- `$foreignColumns: Column[]` – a hivatkozott oszlopok tömbje -- `$name: ?string` – az idegen kulcs neve - -```php -// Idegen kulcsok kiírása -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/hu/security.texy b/database/hu/security.texy deleted file mode 100644 index 1922a972d3..0000000000 --- a/database/hu/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Biztonsági kockázatok -********************* - -<div class=perex> - -Az adatbázis gyakran tartalmaz érzékeny adatokat és lehetővé teszi veszélyes műveletek végrehajtását. A Nette Database biztonságos használatához kulcsfontosságú: - -- Megérteni a különbséget a biztonságos és a nem biztonságos API között -- Paraméterezett lekérdezéseket használni -- Helyesen validálni a bemeneti adatokat - -</div> - - -Mi az SQL Injection? -==================== - -Az SQL injection a legkomolyabb biztonsági kockázat az adatbázisokkal való munka során. Akkor keletkezik, ha a felhasználótól származó, nem kezelt bemenet az SQL lekérdezés részévé válik. A támadó saját SQL parancsokat illeszthet be, és ezzel: -- Jogosulatlan hozzáférést szerezhet az adatokhoz -- Módosíthatja vagy törölheti az adatokat az adatbázisban -- Megkerülheti az authentikációt - -```php -// ❌ VESZÉLYES KÓD - sebezhető az SQL injection-nel szemben -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// A támadó például megadhatja a következő értéket: ' OR '1'='1 -// Az eredményül kapott lekérdezés ez lesz: SELECT * FROM users WHERE name = '' OR '1'='1' -// Ami visszaadja az összes felhasználót -``` - -Ugyanez vonatkozik a [Database Explorer |explorer]-re is: - -```php -// ❌ VESZÉLYES KÓD - sebezhető az SQL injection-nel szemben -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Paraméterezett lekérdezések -=========================== - -Az SQL injection elleni alapvető védekezés a paraméterezett lekérdezések használata. A Nette Database több módszert is kínál ezek használatára. - -A legegyszerűbb módszer a **kérdőjeles helyettesítők** használata: - -```php -// ✅ Biztonságos paraméterezett lekérdezés -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Biztonságos feltétel az Explorerben -$table->where('name = ?', $name); -``` - -Ez érvényes minden további metódusra a [Database Explorerben |explorer], amelyek lehetővé teszik kifejezések beillesztését kérdőjeles helyettesítőkkel és paraméterekkel. - -Az INSERT, UPDATE parancsokhoz vagy a WHERE záradékhoz az értékeket tömbben adhatjuk át: - -```php -// ✅ Biztonságos INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Biztonságos INSERT az Explorerben -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Paraméterértékek validálása -=========================== - -A paraméterezett lekérdezések a biztonságos adatbázis-kezelés alapkövei. Azonban az értékeknek, amelyeket beléjük illesztünk, több ellenőrzési szinten kell átesniük: - - -Típusellenőrzés ---------------- - -**A legfontosabb a paraméterek helyes adattípusának biztosítása** - ez szükséges feltétele a Nette Database biztonságos használatának. Az adatbázis feltételezi, hogy minden bemeneti adat helyes adattípussal rendelkezik, amely megfelel az adott oszlopnak. - -Például, ha az előző példákban a `$name` váratlanul egy tömb lenne egy string helyett, a Nette Database megpróbálná az összes elemét beilleszteni az SQL lekérdezésbe, ami hibához vezetne. Ezért **soha ne használjon** validálatlan adatokat a `$_GET`, `$_POST` vagy `$_COOKIE` tömbökből közvetlenül az adatbázis lekérdezésekben. - - -Formátumellenőrzés ------------------- - -A második ellenőrzési szinten az adatok formátumát ellenőrizzük - például, hogy a stringek UTF-8 kódolásúak-e, és hosszuk megfelel-e az oszlop definíciójának, vagy hogy a numerikus értékek az adott oszlop adattípusához megengedett tartományban vannak-e. - -Ezen a validálási szinten részben magára az adatbázisra is támaszkodhatunk - sok adatbázis elutasítja az érvénytelen adatokat. Azonban a viselkedés eltérő lehet, némelyik csendben levághatja a hosszú stringeket, vagy a tartományon kívüli számokat. - - -Domain ellenőrzés ------------------ - -A harmadik szint az alkalmazásspecifikus logikai ellenőrzéseket jelenti. Például annak ellenőrzése, hogy a select boxokból származó értékek megfelelnek-e a kínált lehetőségeknek, hogy a számok a várt tartományban vannak-e (pl. életkor 0-150 év), vagy hogy az értékek közötti kölcsönös függőségek értelmesek-e. - - -Ajánlott validálási módszerek ------------------------------ - -- Használjon [Nette Űrlapokat |forms:], amelyek automatikusan biztosítják az összes bemenet helyes validálását -- Használjon [Presentereket |application:] és adja meg az adattípusokat a paramétereknél az `action*()` és `render*()` metódusokban -- Vagy implementáljon saját validálási réteget standard PHP eszközökkel, mint például a `filter_var()` - - -Biztonságos munka az oszlopokkal -================================ - -Az előző szakaszban megmutattuk, hogyan kell helyesen validálni a paraméterértékeket. Azonban az SQL lekérdezésekben tömbök használatakor ugyanolyan figyelmet kell fordítanunk a kulcsaikra is. - -```php -// ❌ VESZÉLYES KÓD - a tömb kulcsai nincsenek kezelve -$database->query('INSERT INTO users', $_POST); -``` - -Az INSERT és UPDATE parancsoknál ez alapvető biztonsági hiba - a támadó bármilyen oszlopot beilleszthet vagy módosíthat az adatbázisban. Például beállíthatná az `is_admin = 1`-et, vagy tetszőleges adatokat illeszthetne be érzékeny oszlopokba (ún. Mass Assignment Vulnerability). - -A WHERE feltételekben ez még veszélyesebb, mivel operátorokat tartalmazhatnak: - -```php -// ❌ VESZÉLYES KÓD - a tömb kulcsai nincsenek kezelve -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// végrehajtja a WHERE (`salary` > 100000) lekérdezést -``` - -A támadó ezt a megközelítést használhatja a munkavállalók fizetésének szisztematikus kiderítésére. Például elkezdheti a 100 000 feletti fizetések lekérdezésével, majd az 50 000 alattiakkal, és a tartomány fokozatos szűkítésével felfedheti az összes munkavállaló hozzávetőleges fizetését. Ezt a támadástípust SQL enumeration-nek nevezik. - -A `where()` és `whereOr()` metódusok még [sokkal rugalmasabbak |explorer#where], és támogatják az SQL kifejezéseket, beleértve az operátorokat és függvényeket a kulcsokban és értékekben. Ez lehetőséget ad a támadónak SQL injection végrehajtására: - -```php -// ❌ VESZÉLYES KÓD - a támadó saját SQL-t illeszthet be -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// végrehajtja a WHERE (0) UNION SELECT name, salary FROM users WHERE (1) lekérdezést -``` - -Ez a támadás lezárja az eredeti feltételt a `0)` segítségével, saját `SELECT`-et csatol a `UNION` segítségével, hogy érzékeny adatokat szerezzen a `users` táblából, és szintaktikailag helyes lekérdezést zár le a `WHERE (1)` segítségével. - - -Oszlopok Whitelistje --------------------- - -Az oszlopnevekkel való biztonságos munkához szükségünk van egy mechanizmusra, amely biztosítja, hogy a felhasználó csak az engedélyezett oszlopokkal dolgozhasson, és ne tudjon sajátokat hozzáadni. Megpróbálhatnánk észlelni és blokkolni a veszélyes oszlopneveket (blacklist), de ez a megközelítés megbízhatatlan - a támadó mindig kitalálhat egy új módszert a veszélyes oszlopnév beírására, amit nem láttunk előre. - -Ezért sokkal biztonságosabb megfordítani a logikát, és explicit módon definiálni az engedélyezett oszlopok listáját (whitelist): - -```php -// Oszlopok, amelyeket a felhasználó módosíthat -$allowedColumns = ['name', 'email', 'active']; - -// Eltávolítjuk az összes nem engedélyezett oszlopot a bemenetből -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Most már biztonságosan használhatjuk a lekérdezésekben, például: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Dinamikus azonosítók -==================== - -Dinamikus tábla- és oszlopnevekhez használja a `?name` helyettesítő szimbólumot. Ez biztosítja az azonosítók helyes escapelését az adott adatbázis szintaxisa szerint (pl. backtickek használatával MySQL-ben): - -```php -// ✅ Megbízható azonosítók biztonságos használata -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Eredmény MySQL-ben: SELECT `name` FROM `users` -``` - -Fontos: a `?name` szimbólumot csak az alkalmazás kódjában definiált, megbízható értékekhez használja. Felhasználótól származó értékekhez használja újra a [whitelistet |#Oszlopok Whitelistje]. Ellenkező esetben biztonsági kockázatoknak teszi ki magát: - -```php -// ❌ VESZÉLYES - soha ne használjon felhasználói bemenetet -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/hu/sql-way.texy b/database/hu/sql-way.texy deleted file mode 100644 index ff318344a3..0000000000 --- a/database/hu/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL megközelítés -**************** - -.[perex] -A Nette Database két utat kínál: írhat SQL lekérdezéseket saját maga (SQL megközelítés), vagy hagyhatja, hogy automatikusan generálódjanak (lásd [Explorer |explorer]). Az SQL megközelítés teljes ellenőrzést biztosít a lekérdezések felett, miközben garantálja azok biztonságos összeállítását. - -.[note] -Az adatbázis csatlakozásának és konfigurálásának részleteit a [Csatlakozás és konfiguráció |guide#Csatlakozás és konfiguráció] fejezetben találja. - - -Alapvető lekérdezés -=================== - -Az adatbázis lekérdezéséhez a `query()` metódus szolgál. Ez egy [ResultSet |api:Nette\Database\ResultSet] objektumot ad vissza, amely a lekérdezés eredményét reprezentálja. Hiba esetén a metódus [kivételt dob|exceptions]. A lekérdezés eredményét `foreach` ciklussal járhatjuk be, vagy használhatunk néhány [segédfüggvényt |#Adatlekérés]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Az értékek biztonságos beillesztéséhez az SQL lekérdezésekbe paraméterezett lekérdezéseket használunk. A Nette Database ezt maximálisan egyszerűvé teszi - elegendő az SQL lekérdezés után egy vesszőt és az értéket hozzáadni: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Több paraméter esetén kétféle írásmód lehetséges. Vagy "átszőheti" az SQL lekérdezést paraméterekkel: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Vagy először megírhatja a teljes SQL lekérdezést, majd csatolhatja az összes paramétert: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Védelem az SQL injection ellen -============================== - -Miért fontos paraméterezett lekérdezéseket használni? Mert megvédenek az SQL injection nevű támadástól, amely során a támadó saját SQL parancsokat csempészhetne be, és ezzel adatokat szerezhetne vagy károsíthatna az adatbázisban. - -.[warning] -**Soha ne illesszen be változókat közvetlenül az SQL lekérdezésbe!** Mindig használjon paraméterezett lekérdezéseket, amelyek megvédenek az SQL injection ellen. - -```php -// ❌ VESZÉLYES KÓD - sebezhető az SQL injection-nel szemben -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Biztonságos paraméterezett lekérdezés -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Ismerkedjen meg a [lehetséges biztonsági kockázatokkal |security]. - - -Lekérdezési technikák -===================== - - -WHERE feltételek ----------------- - -A WHERE feltételeket asszociatív tömbként írhatja le, ahol a kulcsok az oszlopnevek, az értékek pedig az összehasonlítandó adatok. A Nette Database automatikusan kiválasztja a legmegfelelőbb SQL operátort az érték típusa alapján. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -A kulcsban explicit módon is megadhatja az összehasonlítási operátort: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // a > operátort használja - 'name LIKE' => '%John%', // a LIKE operátort használja - 'email NOT LIKE' => '%example.com%', // a NOT LIKE operátort használja -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -A Nette automatikusan kezeli a speciális eseteket, mint a `null` értékek vagy tömbök. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // az = operátort használja - 'category_id' => [1, 2, 3], // az IN-t használja - 'description' => null, // az IS NULL-t használja -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Negatív feltételekhez használja a `NOT` operátort: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // a <> operátort használja - 'category_id NOT' => [1, 2, 3], // a NOT IN-t használja - 'description NOT' => null, // az IS NOT NULL-t használja - 'id' => [], // kihagyja -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -A feltételek összekapcsolásához az `AND` operátor használatos. Ezt a [?or helyettesítő karakterrel |#SQL összeállítási tippek] lehet megváltoztatni. - - -ORDER BY szabályok ------------------- - -Az `ORDER BY` rendezést tömb segítségével lehet leírni. A kulcsokban az oszlopokat adjuk meg, az érték pedig egy logikai érték lesz, amely meghatározza, hogy növekvő sorrendben kell-e rendezni: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // növekvő - 'name' => false, // csökkenő -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Adatbeszúrás (INSERT) ---------------------- - -Rekordok beszúrásához az `INSERT` SQL parancsot használjuk. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -A `getInsertId()` metódus visszaadja az utoljára beszúrt sor ID-jét. Néhány adatbázisnál (pl. PostgreSQL) paraméterként meg kell adni annak a szekvenciának a nevét, amelyből az ID-t generálni kell a `$database->getInsertId($sequenceId)` segítségével. - -Paraméterként átadhatunk [#Speciális értékek] is, mint például fájlokat, DateTime objektumokat vagy enum típusokat. - -Több rekord beszúrása egyszerre: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -A többszörös INSERT sokkal gyorsabb, mert egyetlen adatbázis-lekérdezés hajtódik végre, sok különálló helyett. - -**Biztonsági figyelmeztetés:** Soha ne használjon validálatlan adatokat `$values`-ként. Ismerkedjen meg a [lehetséges kockázatokkal |security#Biztonságos munka az oszlopokkal]. - - -Adatfrissítés (UPDATE) ----------------------- - -Rekordok frissítéséhez az `UPDATE` SQL parancsot használjuk. - -```php -// Egy rekord frissítése -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Az érintett sorok számát a `$result->getRowCount()` adja vissza. - -Az UPDATE-hez használhatjuk a `+=` és `-=` operátorokat: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // a login_count inkrementálása -], 1); -``` - -Példa egy rekord beszúrására vagy módosítására, ha már létezik. Az `ON DUPLICATE KEY UPDATE` technikát használjuk: - -```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 -``` - -Figyelje meg, hogy a Nette Database felismeri, milyen kontextusban illesztjük be a tömböt tartalmazó paramétert az SQL parancsba, és ennek megfelelően állítja össze belőle az SQL kódot. Tehát az első tömbből `(id, name, year) VALUES (123, 'Jim', 1978)`-t állított össze, míg a másodikat `name = 'Jim', year = 1978` formára alakította át. Részletesebben ezzel az [#SQL összeállítási tippek] részben foglalkozunk. - - -Adattörlés (DELETE) -------------------- - -Rekordok törléséhez a `DELETE` SQL parancsot használjuk. Példa a törölt sorok számának lekérésével: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -SQL összeállítási tippek ------------------------- - -A hint egy speciális helyettesítő karakter az SQL lekérdezésben, amely megmondja, hogyan kell a paraméter értékét SQL kifejezéssé átírni: - -| Hint | Leírás | Automatikusan használva -|-----------|-------------------------------------------------|----------------------------- -| `?name` | tábla vagy oszlop nevének beillesztésére használja | - -| `?values` | `(key, ...) VALUES (value, ...)`-t generál | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | `key = value, ...` hozzárendelést generál | `SET ?`, `KEY UPDATE ?` -| `?and` | a tömb feltételeit `AND` operátorral köti össze | `WHERE ?`, `HAVING ?` -| `?or` | a tömb feltételeit `OR` operátorral köti össze | - -| `?order` | `ORDER BY` záradékot generál | `ORDER BY ?`, `GROUP BY ?` - -Táblák és oszlopok nevének dinamikus beillesztéséhez a lekérdezésbe a `?name` helyettesítő karakter szolgál. A Nette Database gondoskodik az azonosítók helyes kezeléséről az adott adatbázis konvenciói szerint (pl. backtickekbe zárás MySQL-ben). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (MySQL-ben) -``` - -**Figyelmeztetés:** a `?name` szimbólumot csak validált bemenetekből származó tábla- és oszlopnevekhez használja, különben [biztonsági kockázatnak |security#Dinamikus azonosítók] teszi ki magát. - -A többi hintet általában nem szükséges megadni, mivel a Nette okos automatikus felismerést használ az SQL lekérdezés összeállításakor (lásd a táblázat harmadik oszlopát). De használhatja például olyan helyzetben, amikor a feltételeket `OR` helyett `AND`-del szeretné összekötni: - -```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ális értékek ------------------ - -A szokásos skalár típusokon (string, int, bool) kívül speciális értékeket is átadhat paraméterként: - -- fájlok: `fopen('image.gif', 'r')` beilleszti a fájl bináris tartalmát -- dátum és idő: a `DateTime` objektumok adatbázis formátumra konvertálódnak -- enum típusok: az `enum` példányok értékükre konvertálódnak -- SQL literálok: a `Connection::literal('NOW()')` segítségével létrehozottak közvetlenül beillesztődnek a lekérdezésbe - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Azoknál az adatbázisoknál, amelyek nem rendelkeznek natív támogatással a `datetime` adattípushoz (mint a SQLite és az Oracle), a `DateTime` az [adatbázis konfigurációjában|configuration] a `formatDateTime` tétellel meghatározott értékre konvertálódik (az alapértelmezett érték `U` - unix timestamp). - - -SQL literálok -------------- - -Néhány esetben szükség van arra, hogy értékként közvetlenül SQL kódot adjunk meg, amelyet azonban nem szabad stringként értelmezni és escapelni. Erre szolgálnak a `Nette\Database\SqlLiteral` osztály objektumai. Ezeket a `Connection::literal()` metódus hozza létre. - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - 'year >' => $database::literal('YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR()) -``` - -Vagy alternatívaként: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Az SQL literálok tartalmazhatnak paramétereket: - -```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) -``` - -Ennek köszönhetően érdekes kombinációkat hozhatunk létre: - -```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') -``` - - -Adatlekérés -=========== - - -Rövidítések SELECT lekérdezésekhez ----------------------------------- - -Az adatbetöltés egyszerűsítésére a `Connection` több rövidítést kínál, amelyek kombinálják a `query()` hívást a következő `fetch*()` hívásokkal. Ezek a metódusok ugyanazokat a paramétereket fogadják el, mint a `query()`, azaz az SQL lekérdezést és az opcionális paramétereket. A `fetch*()` metódusok teljes leírását [alább |#fetch] találja. - -| `fetch($sql, ...$params): ?Row` | Végrehajtja a lekérdezést és visszaadja az első sort `Row` objektumként -| `fetchAll($sql, ...$params): array` | Végrehajtja a lekérdezést és visszaadja az összes sort `Row` objektumok tömbjeként -| `fetchPairs($sql, ...$params): array` | Végrehajtja a lekérdezést és visszaad egy asszociatív tömböt, ahol az első oszlop a kulcs, a második az érték -| `fetchField($sql, ...$params): mixed` | Végrehajtja a lekérdezést és visszaadja az első sor első mezőjének értékét -| `fetchList($sql, ...$params): ?array` | Végrehajtja a lekérdezést és visszaadja az első sort indexelt tömbként - -Példa: - -```php -// fetchField() - visszaadja az első cella értékét -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - iteráció a sorokon ------------------------------- - -A lekérdezés végrehajtása után egy [ResultSet|api:Nette\Database\ResultSet] objektumot kapunk vissza, amely lehetővé teszi az eredmények több módon történő bejárását. A legegyszerűbb módja a lekérdezés végrehajtásának és a sorok lekérésének a `foreach` ciklussal történő iterálás. Ez a módszer a memóriatakarékosabb, mivel az adatokat fokozatosan adja vissza, és nem tárolja őket egyszerre a memóriában. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -A `ResultSet`-et csak egyszer lehet iterálni. Ha ismételten kell iterálni, először be kell tölteni az adatokat egy tömbbe, például a `fetchAll()` metódussal. - - -fetch(): ?Row .[method] ------------------------ - -Visszaad egy sort `Row` objektumként. Ha nincs több sor, `null`-t ad vissza. A belső mutatót a következő sorra mozgatja. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // betölti az első sort -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Visszaadja a `ResultSet`-ből az összes fennmaradó sort `Row` objektumok tömbjeként. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // betölti az összes sort -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Visszaadja az eredményeket asszociatív tömbként. Az első argumentum határozza meg az oszlop nevét, amely a tömb kulcsaként lesz használva, a második argumentum határozza meg az oszlop nevét, amely értékként lesz használva: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Ha csak az első paramétert adjuk meg, az érték a teljes sor lesz, azaz egy `Row` objektum: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -Duplikált kulcsok esetén az utolsó sor értéke lesz használva. Ha `null`-t használunk kulcsként, a tömb numerikusan lesz indexelve nullától kezdve (ekkor nem történik ütközés): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternatívaként megadhat egy callbacket paraméterként, amely minden sorhoz vagy magát az értéket, vagy egy kulcs-érték párt ad vissza. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// A callback visszaadhat egy tömböt is kulcs & érték párral: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Visszaadja az aktuális sor első mezőjének értékét. Ha nincs több sor, `null`-t ad vissza. A belső mutatót a következő sorra mozgatja. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // betölti a nevet az első sorból -``` - - -fetchList(): ?array .[method] ------------------------------ - -Visszaad egy sort indexelt tömbként. Ha nincs több sor, `null`-t ad vissza. A belső mutatót a következő sorra mozgatja. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Visszaadja az utolsó `UPDATE` vagy `DELETE` lekérdezés által érintett sorok számát. `SELECT` esetén ez a visszaadott sorok száma, de ez nem mindig ismert - ebben az esetben a metódus `null`-t ad vissza. - - -getColumnCount(): ?int .[method] --------------------------------- - -Visszaadja az oszlopok számát a `ResultSet`-ben. - - -Információk a lekérdezésekről -============================= - -Debuggolási célokra lekérhetjük az utoljára végrehajtott lekérdezés információit: - -```php -echo $database->getLastQueryString(); // kiírja az SQL lekérdezést - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // kiírja az SQL lekérdezést -echo $result->getTime(); // kiírja a végrehajtási időt másodpercben -``` - -Az eredmény HTML táblázatként való megjelenítéséhez használható: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -A ResultSet információkat kínál az oszloptípusokról: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column típusa $type->type"; // pl. 'id típusa int' -} -``` - - -Lekérdezések naplózása ----------------------- - -Implementálhatunk saját lekérdezés-naplózást. Az `onQuery` esemény egy callback tömb, amely minden végrehajtott lekérdezés után meghívódik: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Lekérdezés: ' . $result->getQueryString()); - $logger->info('Idő: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Nagy eredményhalmaz: ' . $result->getRowCount() . ' sor'); - } -}; -``` diff --git a/database/hu/transactions.texy b/database/hu/transactions.texy deleted file mode 100644 index accc0cc112..0000000000 --- a/database/hu/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Tranzakciók -*********** - -.[perex] -A tranzakciók garantálják, hogy a tranzakción belüli összes művelet végrehajtásra kerül, vagy egyik sem. Hasznosak az adatok konzisztenciájának biztosítására összetettebb műveletek során. - -A tranzakciók használatának legegyszerűbb módja a következő: - -```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; -} -``` - -Ugyanezt sokkal elegánsabban is megírhatja a `transaction()` metódussal. Paraméterként egy callbacket fogad el, amelyet a tranzakcióban hajt végre. Ha a callback kivétel nélkül lefut, a tranzakció automatikusan megerősítésre kerül. Ha kivétel történik, a tranzakció visszavonásra kerül (rollback), és a kivétel tovább terjed. - -```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' - ]); -}); -``` - -A `transaction()` metódus értékeket is visszaadhat: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // visszaadja a frissített sorok számát -}); -``` diff --git a/database/pt/@home.texy b/database/pt/@home.texy deleted file mode 100644 index dc4308eeae..0000000000 --- a/database/pt/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Bancos de dados suportados -========================== - -Nette suporta os seguintes bancos de dados: - -|* Servidor de banco de dados |* Nome DSN |* Suporte no Core |* Suporte no Explorer -| MySQL (>= 5.1) | mysql | SIM | SIM -| PostgreSQL (>= 9.0) | pgsql | SIM | SIM -| Sqlite 3 (>= 3.8) | sqlite | SIM | SIM -| Oracle | oci | SIM | - -| MS SQL (PDO_SQLSRV) | sqlsrv | SIM | SIM -| MS SQL (PDO_DBLIB) | mssql | SIM | - -| ODBC | odbc | SIM | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database simplifica significativamente a obtenção de dados do banco de dados sem a necessidade de escrever consultas SQL. Ele faz consultas eficientes e não transfere dados desnecessários.}} diff --git a/database/pt/@left-menu.texy b/database/pt/@left-menu.texy deleted file mode 100644 index b1a45ab3c0..0000000000 --- a/database/pt/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Introdução |guide] -- [Acesso SQL |sql way] -- [Explorer |Explorer] -- [Transações |transactions] -- [Exceções |exceptions] -- [Reflexão |reflection] -- [Mapeamento |mapping] -- [Configuração |configuration] -- [Riscos de segurança |security] -- [Atualização |en:upgrading] diff --git a/database/pt/@meta.texy b/database/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/database/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/database/pt/configuration.texy b/database/pt/configuration.texy deleted file mode 100644 index b327ef196d..0000000000 --- a/database/pt/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Configuração do banco de dados -****************************** - -.[perex] -Visão geral das opções de configuração para Nette Database. - -Se você não estiver usando o framework completo, mas apenas esta biblioteca, leia [como carregar a configuração|bootstrap:]. - - -Conexão única -------------- - -Configuração de uma única conexão de banco de dados: - -```neon -database: - # DSN, a única chave obrigatória - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Cria os serviços `Nette\Database\Connection` e `Nette\Database\Explorer`, que geralmente passamos por [autowiring |dependency-injection:autowiring], ou por referência ao [seu nome |#Serviços DI]. - -Outras configurações: - -```neon -database: - # exibir o painel do banco de dados na Tracy Bar? - debugger: ... # (bool) padrão é true - - # exibir EXPLAIN das consultas na Tracy Bar? - explain: ... # (bool) padrão é true - - # permitir autowiring para esta conexão? - autowired: ... # (bool) padrão é true na primeira conexão - - # convenções de tabela: discovered, static ou nome da classe - conventions: discovered # (string) padrão é 'discovered' - - options: - # conectar ao banco de dados apenas quando necessário? - lazy: ... # (bool) padrão é false - - # classe PHP do driver do banco de dados - driverClass: # (string) - - # apenas MySQL: define sql_mode - sqlmode: # (string) - - # apenas MySQL: define SET NAMES - charset: # (string) padrão é 'utf8mb4' - - # apenas MySQL: converte TINYINT(1) para bool - convertBoolean: # (bool) padrão é false - - # retorna colunas de data como objetos imutáveis (desde a versão 3.2.1) - newDateTime: # (bool) padrão é false - - # apenas Oracle e SQLite: formato para armazenar data - formatDateTime: # (string) padrão é 'U' -``` - -Na chave `options`, você pode especificar outras opções encontradas na [documentação dos drivers PDO |https://www.php.net/manual/en/pdo.drivers.php], como por exemplo: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Múltiplas conexões ------------------- - -Na configuração, também podemos definir várias conexões de banco de dados dividindo-as em seções nomeadas: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -O autowiring está habilitado apenas para os serviços da primeira seção. Isso pode ser alterado usando `autowired: false` ou `autowired: true`. - - -Serviços DI ------------ - -Estes serviços são adicionados ao contêiner de DI, onde `###` representa o nome da conexão: - -| Nome | Tipo | Descrição -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | conexão com o banco de dados -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Se definirmos apenas uma conexão, os nomes dos serviços serão `database.default.connection` e `database.default.explorer`. Se definirmos várias conexões como no exemplo acima, os nomes corresponderão às seções, ou seja, `database.main.connection`, `database.main.explorer` e também `database.another.connection` e `database.another.explorer`. - -Passamos serviços não autowired explicitamente por referência ao seu nome: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/pt/exceptions.texy b/database/pt/exceptions.texy deleted file mode 100644 index 1882eaf2d8..0000000000 --- a/database/pt/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Exceções -******** - -O Nette Database utiliza uma hierarquia de exceções. A classe base é `Nette\Database\DriverException`, que herda de `PDOException` e fornece opções estendidas para trabalhar com erros do banco de dados: - -- O método `getDriverCode()` retorna o código de erro do driver do banco de dados -- O método `getSqlState()` retorna o código SQLSTATE -- Os métodos `getQueryString()` e `getParameters()` permitem obter a consulta original e os seus parâmetros - -As seguintes exceções especializadas herdam de `DriverException`: - -- `ConnectionException` - sinaliza falha na conexão com o servidor de banco de dados -- `ConstraintViolationException` - classe base para violação de restrições do banco de dados, da qual herdam: - - `ForeignKeyConstraintViolationException` - violação de chave estrangeira - - `NotNullConstraintViolationException` - violação da restrição NOT NULL - - `UniqueConstraintViolationException` - violação da unicidade do valor - - -Exemplo de captura da exceção `UniqueConstraintViolationException`, que ocorre quando tentamos inserir um utilizador com um e-mail que já existe no banco de dados (assumindo que a coluna `email` tenha um índice único). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Um utilizador com este e-mail já existe.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Ocorreu um erro durante o registo: ' . $e->getMessage(); -} -``` diff --git a/database/pt/explorer.texy b/database/pt/explorer.texy deleted file mode 100644 index 714a4d7719..0000000000 --- a/database/pt/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -O Explorer oferece uma forma intuitiva e eficiente de trabalhar com o banco de dados. Ele trata automaticamente das relações entre tabelas e da otimização de consultas, para que você possa se concentrar na sua aplicação. Funciona imediatamente sem configuração. Se precisar de controle total sobre as consultas SQL, pode utilizar o [acesso SQL |sql-way]. - -- O trabalho com dados é natural e fácil de entender -- Gera consultas SQL otimizadas que carregam apenas os dados necessários -- Permite acesso fácil a dados relacionados sem a necessidade de escrever consultas JOIN -- Funciona imediatamente sem qualquer configuração ou geração de entidades - -</div> - - -Começa-se com o Explorer chamando o método `table()` do objeto [api:Nette\Database\Explorer] (detalhes sobre a conexão podem ser encontrados no capítulo [Conexão e configuração |guide#Conexão e configuração]): - -```php -$books = $explorer->table('book'); // 'book' é o nome da tabela -``` - -O método retorna um objeto [Selection |api:Nette\Database\Table\Selection], que representa uma consulta SQL. A este objeto, podemos encadear outros métodos para filtrar e ordenar os resultados. A consulta é construída e executada apenas quando começamos a solicitar os dados, por exemplo, percorrendo um ciclo `foreach`. Cada linha é representada por um objeto [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // exibe a coluna 'title' - echo $book->author_id; // exibe a coluna 'author_id' -} -``` - -O Explorer facilita fundamentalmente o trabalho com [#relações entre tabelas]. O exemplo seguinte mostra como podemos facilmente exibir dados de tabelas relacionadas (livros e seus autores). Note que não precisamos escrever nenhuma consulta JOIN, o Nette cria-as por nós: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Livro: ' . $book->title; - echo 'Autor: ' . $book->author->name; // cria JOIN na tabela 'author' -} -``` - -O Nette Database Explorer otimiza as consultas para serem o mais eficientes possível. O exemplo acima executa apenas duas consultas SELECT, independentemente de estarmos a processar 10 ou 10 000 livros. - -Além disso, o Explorer monitoriza quais colunas são usadas no código e carrega do banco de dados apenas essas, economizando ainda mais desempenho. Este comportamento é totalmente automático e adaptativo. Se modificar o código posteriormente e começar a usar outras colunas, o Explorer ajustará automaticamente as consultas. Não precisa de configurar nada, nem pensar em quais colunas precisará - deixe isso para o Nette. - - -Filtragem e Ordenação -===================== - -A classe `Selection` fornece métodos para filtrar e ordenar a seleção de dados. - -.[language-php] -| `where($condition, ...$params)` | Adiciona uma condição WHERE. Múltiplas condições são unidas pelo operador AND -| `whereOr(array $conditions)` | Adiciona um grupo de condições WHERE unidas pelo operador OR -| `wherePrimary($value)` | Adiciona uma condição WHERE pela chave primária -| `order($columns, ...$params)` | Define a ordenação ORDER BY -| `select($columns, ...$params)` | Especifica as colunas que devem ser carregadas -| `limit($limit, $offset = null)` | Limita o número de linhas (LIMIT) e opcionalmente define OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Define a paginação -| `group($columns, ...$params)` | Agrupa linhas (GROUP BY) -| `having($condition, ...$params)` | Adiciona uma condição HAVING para filtrar linhas agrupadas - -Os métodos podem ser encadeados (a chamada [fluent interface |nette:introduction-to-object-oriented-programming#Interfaces Fluentes]): `$table->where(...)->order(...)->limit(...)`. - -Nestes métodos, também pode usar notação especial para aceder a [dados de tabelas relacionadas |#Consulta através de tabelas relacionadas]. - - -Escaping e Identificadores --------------------------- - -Os métodos escapam automaticamente os parâmetros e colocam aspas nos identificadores (nomes de tabelas e colunas), prevenindo assim a injeção de SQL. Para o funcionamento correto, é necessário seguir algumas regras: - -- Palavras-chave, nomes de funções, procedimentos, etc., escreva em **MAIÚSCULAS**. -- Nomes de colunas e tabelas escreva em **minúsculas**. -- Strings sempre insira através de **parâmetros**. - -```php -where('name = ' . $name); // VULNERABILIDADE CRÍTICA: injeção de SQL -where('name LIKE "%search%"'); // ERRADO: complica o quoting automático -where('name LIKE ?', '%search%'); // CORRETO: valor inserido via parâmetro - -where('name like ?', $name); // ERRADO: gera: `name` `like` ? -where('name LIKE ?', $name); // CORRETO: gera: `name` LIKE ? -where('LOWER(name) = ?', $value);// CORRETO: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtra os resultados usando condições WHERE. A sua força reside no trabalho inteligente com diferentes tipos de valores e na escolha automática de operadores SQL. - -Uso básico: - -```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' -``` - -Graças à deteção automática de operadores apropriados, não precisamos de lidar com vários casos especiais. O Nette resolve-os por nós: - -```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) -// também é possível usar o placeholder de interrogação sem operador: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -O método também processa corretamente condições negativas e arrays vazios: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- não encontra nada -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- encontra tudo -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- encontra tudo -// $table->where('NOT id ?', $ids); Atenção - esta sintaxe não é suportada -``` - -Como parâmetro, também podemos passar o resultado de outra tabela - será criada uma subconsulta: - -```php -// 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')); -``` - -As condições também podem ser passadas como um array, cujos itens são unidos por AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -No array, podemos usar pares chave => valor e o Nette escolherá novamente, de forma automática, os operadores corretos: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -No array, podemos combinar expressões SQL com placeholders de interrogação e múltiplos parâmetros. Isto é adequado para condições complexas com operadores definidos com precisão: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // dois parâmetros passados como array -]); -``` - -Chamadas múltiplas de `where()` unem automaticamente as condições com AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Semelhante a `where()`, adiciona condições, mas com a diferença de que as une usando OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Aqui também podemos usar expressões mais complexas: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Adiciona uma condição para a chave primária da tabela: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Se a tabela tiver uma chave primária composta (por exemplo, `foo_id`, `bar_id`), passamo-la como um 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(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Determina a ordem em que as linhas serão retornadas. Podemos ordenar por uma ou mais colunas, em ordem ascendente ou descendente, ou por uma expressão personalizada: - -```php -$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(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Especifica as colunas que devem ser retornadas do banco de dados. Por padrão, o Nette Database Explorer retorna apenas as colunas que são realmente usadas no código. O método `select()` é, portanto, usado nos casos em que precisamos retornar expressões específicas: - -```php -// SELECT *, DATE_FORMAT(`created_at`, ?) AS formatted_date -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Os aliases definidos usando `AS` ficam então disponíveis como propriedades do objeto ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // acesso ao alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Limita o número de linhas retornadas (LIMIT) e opcionalmente permite definir um offset: - -```php -$table->limit(10); // LIMIT 10 (retorna as primeiras 10 linhas) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Para paginação, é mais adequado usar o método `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Facilita a paginação dos resultados. Aceita o número da página (contado a partir de 1) e o número de itens por página. Opcionalmente, pode-se passar uma referência a uma variável na qual o número total de páginas será armazenado: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Total de páginas: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Agrupa linhas de acordo com as colunas especificadas (GROUP BY). É geralmente usado em conjunto com funções de agregação: - -```php -// Conta o número de produtos em cada categoria -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Define uma condição para filtrar linhas agrupadas (HAVING). Pode ser usado em conjunto com o método `group()` e funções de agregação: - -```php -// Encontra categorias que têm mais de 100 produtos -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Leitura de Dados -================ - -Para ler dados do banco de dados, temos vários métodos úteis disponíveis: - -.[language-php] -| `foreach ($table as $key => $row)` | Itera sobre todas as linhas, `$key` é o valor da chave primária, `$row` é o objeto ActiveRow -| `$row = $table->get($key)` | Retorna uma única linha pela chave primária -| `$row = $table->fetch()` | Retorna a linha atual e move o ponteiro para a próxima -| `$array = $table->fetchPairs()` | Cria um array associativo a partir dos resultados -| `$array = $table->fetchAll()` | Retorna todas as linhas como um array -| `count($table)` | Retorna o número de linhas no objeto Selection - -O objeto [ActiveRow |api:Nette\Database\Table\ActiveRow] destina-se apenas à leitura. Isto significa que não é possível alterar os valores das suas propriedades. Esta restrição garante a consistência dos dados e evita efeitos colaterais inesperados. Os dados são carregados do banco de dados e qualquer alteração deve ser feita explicitamente e de forma controlada. - - -`foreach` - Iteração Sobre Todas as Linhas ------------------------------------------- - -A forma mais fácil de executar uma consulta e obter linhas é iterando num ciclo `foreach`. Ele executa automaticamente a consulta SQL. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key é o valor da chave primária, $book é ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Executa a consulta SQL e retorna a linha pela chave primária, ou `null` se não existir. - -```php -$book = $explorer->table('book')->get(123); // retorna ActiveRow com ID 123 ou null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Retorna uma linha e move o ponteiro interno para a próxima. Se não houver mais linhas, retorna `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Retorna os resultados como um array associativo. O primeiro argumento especifica o nome da coluna que será usada como chave no array, o segundo argumento especifica o nome da coluna que será usada como valor: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Se especificarmos apenas o primeiro parâmetro, o valor será a linha inteira, ou seja, o objeto `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -Em caso de chaves duplicadas, o valor da última linha será usado. Ao usar `null` como chave, o array será indexado numericamente a partir de zero (neste caso, não ocorrem colisões): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativamente, pode fornecer um callback como parâmetro, que retornará para cada linha ou o próprio valor, ou um par chave-valor. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Primeiro livro (Jan Novák)', ...] - -// O callback também pode retornar um array com o par chave & valor: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Primeiro livro' => 'Jan Novák', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Retorna todas as linhas como um array associativo de objetos `ActiveRow`, onde as chaves são os valores das chaves primárias. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -O método `count()` sem parâmetro retorna o número de linhas no objeto `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternativa -``` - -Atenção, `count()` com parâmetro executa a função de agregação COUNT no banco de dados. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Converte o objeto `ActiveRow` num array associativo, onde as chaves são os nomes das colunas e os valores são os dados correspondentes. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray será ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Agregação -========= - -A classe `Selection` fornece métodos para executar facilmente funções de agregação (COUNT, SUM, MIN, MAX, AVG, etc.). - -.[language-php] -| `count($expr)` | Conta o número de linhas -| `min($expr)` | Retorna o valor mínimo na coluna -| `max($expr)` | Retorna o valor máximo na coluna -| `sum($expr)` | Retorna a soma dos valores na coluna -| `aggregation($function)` | Permite executar qualquer função de agregação. Ex: `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Executa uma consulta SQL com a função COUNT e retorna o resultado. O método é usado para descobrir quantas linhas correspondem a uma determinada condição: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Atenção, `count()` sem parâmetro apenas retorna o número de linhas no objeto `Selection`, veja [#count()]. - - -min(string $expr) e max(string $expr) .[method] ------------------------------------------------ - -Os métodos `min()` e `max()` retornam o valor mínimo e máximo na coluna ou expressão especificada: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Retorna a soma dos valores na coluna ou expressão especificada: - -```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] ----------------------------------------------------------------------- - -Permite executar qualquer função de agregação. - -```php -// preço médio dos produtos na categoria -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// concatena as tags do produto em uma única string -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Se precisarmos agregar resultados que já resultaram de alguma função de agregação e agrupamento (por exemplo, `SUM(valor)` sobre linhas agrupadas), como segundo argumento, especificamos a função de agregação que deve ser aplicada a esses resultados intermediários: - -```php -// Calcula o preço total dos produtos em estoque para categorias individuais e, em seguida, soma esses preços. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -Neste exemplo, primeiro calculamos o preço total dos produtos em cada categoria (`SUM(price * stock) AS category_total`) e agrupamos os resultados por `category_id`. Em seguida, usamos `aggregation('SUM(category_total)', 'SUM')` para somar esses subtotais `category_total`. O segundo argumento `'SUM'` diz que a função SUM deve ser aplicada aos resultados intermediários. - - -Inserir, Atualizar & Excluir -============================ - -O Nette Database Explorer simplifica a inserção, atualização e exclusão de dados. Todos os métodos listados abaixo lançarão uma exceção `Nette\Database\DriverException` em caso de erro. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Insere novos registros na tabela. - -**Inserindo um único registro:** - -Passamos o novo registro como um array associativo ou objeto iterável (por exemplo, ArrayHash usado em [formulários |forms:]), onde as chaves correspondem aos nomes das colunas na tabela. - -Se a tabela tiver uma chave primária definida, o método retorna um objeto `ActiveRow`, que é recarregado do banco de dados para refletir quaisquer alterações feitas no nível do banco de dados (gatilhos, valores padrão de colunas, cálculos de colunas auto-increment). Isso garante a consistência dos dados e o objeto sempre contém os dados atuais do banco de dados. Se não houver uma chave primária única, ele retorna os dados passados na forma de um array. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row é uma instância de ActiveRow e contém os dados completos da linha inserida, -// incluindo o ID gerado automaticamente e quaisquer alterações feitas por gatilhos -echo $row->id; // Exibe o ID do usuário recém-inserido -echo $row->created_at; // Exibe a hora de criação, se definida por um gatilho -``` - -**Inserindo múltiplos registros de uma vez:** - -O método `insert()` permite inserir vários registros usando uma única consulta SQL. Neste caso, retorna o número de linhas inseridas. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows será 2 -``` - -Como parâmetro, também pode ser passado um objeto `Selection` com uma seleção de dados. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Inserindo valores especiais:** - -Como valores, também podemos passar arquivos, objetos DateTime ou literais SQL: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // converte para formato de banco de dados - 'avatar' => fopen('image.jpg', 'rb'), // insere o conteúdo binário do arquivo - 'uuid' => $explorer::literal('UUID()'), // chama a função UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Atualiza linhas na tabela de acordo com o filtro especificado. Retorna o número de linhas realmente alteradas. - -Passamos as colunas a serem alteradas como um array associativo ou objeto iterável (por exemplo, ArrayHash usado em [formulários |forms:]), onde as chaves correspondem aos nomes das colunas na tabela: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Para alterar valores numéricos, podemos usar os operadores `+=` e `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // aumenta o valor da coluna 'points' em 1 - 'coins-=' => 1, // diminui o valor da coluna 'coins' em 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Exclui linhas da tabela de acordo com o filtro especificado. Retorna o número de linhas excluídas. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Ao chamar `update()` e `delete()`, não se esqueça de especificar as linhas a serem modificadas/excluídas usando `where()`. Se `where()` não for usado, a operação será realizada em toda a tabela! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Atualiza os dados na linha do banco de dados representada pelo objeto `ActiveRow`. Como parâmetro, aceita um iterável com os dados a serem atualizados (as chaves são os nomes das colunas). Para alterar valores numéricos, podemos usar os operadores `+=` e `-=`: - -Após a execução da atualização, o `ActiveRow` é automaticamente recarregado do banco de dados para refletir quaisquer alterações feitas no nível do banco de dados (por exemplo, gatilhos). O método retorna true apenas se houve uma alteração real nos dados. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // aumentamos o número de visualizações -]); -echo $article->views; // Exibe o número atual de visualizações -``` - -Este método atualiza apenas uma linha específica no banco de dados. Para atualização em massa de várias linhas, use o método [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Exclui a linha do banco de dados representada pelo objeto `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Exclui o livro com ID 1 -``` - -Este método exclui apenas uma linha específica no banco de dados. Para exclusão em massa de várias linhas, use o método [#Selection::delete()]. - - -Relações entre tabelas -====================== - -Em bancos de dados relacionais, os dados são divididos em várias tabelas e interligados por chaves estrangeiras. O Nette Database Explorer traz uma maneira revolucionária de trabalhar com essas relações - sem escrever consultas JOIN e sem a necessidade de configurar ou gerar nada. - -Para ilustrar o trabalho com relações, usaremos o exemplo de um banco de dados de livros ([você pode encontrá-lo no GitHub |https://github.com/nette-examples/books]). No banco de dados, temos as tabelas: - -- `author` - escritores e tradutores (colunas `id`, `name`, `web`, `born`) -- `book` - livros (colunas `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - tags (colunas `id`, `name`) -- `book_tag` - tabela de ligação entre livros e tags (colunas `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Estrutura do banco de dados usada nos exemplos *** - -Em nosso exemplo de banco de dados de livros, encontramos vários tipos de relacionamentos (embora o modelo seja simplificado em comparação com a realidade): - -- Um-para-muitos 1:N – cada livro **tem um** autor, um autor pode escrever **vários** livros -- Zero-para-muitos 0:N – um livro **pode ter** um tradutor, um tradutor pode traduzir **vários** livros -- Zero-para-um 0:1 – um livro **pode ter** uma sequência -- Muitos-para-muitos M:N – um livro **pode ter várias** tags e uma tag pode ser atribuída a **vários** livros - -Nesses relacionamentos, sempre existe uma tabela pai e uma tabela filho. Por exemplo, no relacionamento entre autor e livro, a tabela `author` é a pai e `book` é a filho - podemos imaginar que o livro sempre "pertence" a algum autor. Isso também se reflete na estrutura do banco de dados: a tabela filho `book` contém a chave estrangeira `author_id`, que referencia a tabela pai `author`. - -Se precisarmos listar os livros incluindo os nomes de seus autores, temos duas opções. Ou obtemos os dados com uma única consulta SQL usando JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Ou carregamos os dados em duas etapas - primeiro os livros e depois seus autores - e depois os montamos em PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- ids dos autores dos livros obtidos -``` - -A segunda abordagem é, na verdade, mais eficiente, embora possa ser surpreendente. Os dados são carregados apenas uma vez e podem ser melhor utilizados no cache. É precisamente desta forma que o Nette Database Explorer funciona - ele resolve tudo nos bastidores e oferece uma API elegante: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'título: ' . $book->title; - echo 'escrito por: ' . $book->author->name; // $book->author é o registro da tabela 'author' - echo 'traduzido por: ' . $book->translator?->name; -} -``` - - -Acesso à tabela pai -------------------- - -O acesso à tabela pai é direto. Trata-se de relacionamentos como *livro tem um autor* ou *livro pode ter um tradutor*. Obtemos o registro relacionado através da propriedade do objeto ActiveRow - seu nome corresponde ao nome da coluna com a chave estrangeira sem `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // encontra o autor pela coluna author_id -echo $book->translator?->name; // encontra o tradutor pela coluna translator_id -``` - -Quando acessamos a propriedade `$book->author`, o Explorer procura na tabela `book` por uma coluna cujo nome contenha a string `author` (ou seja, `author_id`). Com base no valor nesta coluna, ele carrega o registro correspondente da tabela `author` e o retorna como `ActiveRow`. Da mesma forma funciona `$book->translator`, que usa a coluna `translator_id`. Como a coluna `translator_id` pode conter `null`, usamos o operador `?->` no código. - -Um caminho alternativo é oferecido pelo método `ref()`, que aceita dois argumentos, o nome da tabela de destino e o nome da coluna de ligação, e retorna uma instância de `ActiveRow` ou `null`: - -```php -echo $book->ref('author', 'author_id')->name; // relação com o autor -echo $book->ref('author', 'translator_id')->name; // relação com o tradutor -``` - -O método `ref()` é útil se o acesso via propriedade não puder ser usado porque a tabela contém uma coluna com o mesmo nome (ou seja, `author`). Nos outros casos, recomenda-se usar o acesso via propriedade, que é mais legível. - -O Explorer otimiza automaticamente as consultas ao banco de dados. Quando percorremos os livros em um loop e acessamos seus registros relacionados (autores, tradutores), o Explorer não gera uma consulta para cada livro separadamente. Em vez disso, ele executa apenas um SELECT para cada tipo de relacionamento, reduzindo significativamente a carga no banco de dados. Por exemplo: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Este código chamará apenas estas três consultas rápidas ao banco de dados: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id da coluna author_id dos livros selecionados -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id da coluna translator_id dos livros selecionados -``` - -.[note] -A lógica para encontrar a coluna de ligação é dada pela implementação de [Conventions |api:Nette\Database\Conventions]. Recomendamos o uso de [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], que analisa as chaves estrangeiras e permite trabalhar facilmente com os relacionamentos existentes entre as tabelas. - - -Acesso à tabela filho ---------------------- - -O acesso à tabela filho funciona na direção oposta. Agora perguntamos *quais livros este autor escreveu* ou *este tradutor traduziu*. Para este tipo de consulta, usamos o método `related()`, que retorna uma `Selection` com os registros relacionados. Vejamos um exemplo: - -```php -$author = $explorer->table('author')->get(1); - -// Exibe todos os livros do autor -foreach ($author->related('book.author_id') as $book) { - echo "Escreveu: $book->title"; -} - -// Exibe todos os livros que o autor traduziu -foreach ($author->related('book.translator_id') as $book) { - echo "Traduziu: $book->title"; -} -``` - -O método `related()` aceita a descrição da ligação como um único argumento com notação de ponto ou como dois argumentos separados: - -```php -$author->related('book.translator_id'); // um argumento -$author->related('book', 'translator_id'); // dois argumentos -``` - -O Explorer pode detectar automaticamente a coluna de ligação correta com base no nome da tabela pai. Neste caso, a ligação é feita através da coluna `book.author_id`, porque o nome da tabela de origem é `author`: - -```php -$author->related('book'); // usa book.author_id -``` - -Se existissem várias ligações possíveis, o Explorer lançaria uma exceção [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -O método `related()` pode, obviamente, ser usado também ao percorrer vários registros em um loop, e o Explorer, neste caso, também otimiza automaticamente as consultas: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' escreveu:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Este código gerará apenas duas consultas SQL rápidas: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id dos autores selecionados -``` - - -Relacionamento Muitos-para-Muitos ---------------------------------- - -Para o relacionamento muitos-para-muitos (M:N), é necessária a existência de uma tabela de ligação (no nosso caso `book_tag`), que contém duas colunas com chaves estrangeiras (`book_id`, `tag_id`). Cada uma dessas colunas referencia a chave primária de uma das tabelas interligadas. Para obter os dados relacionados, primeiro obtemos os registros da tabela de ligação usando `related('book_tag')` e, em seguida, prosseguimos para os dados de destino: - -```php -$book = $explorer->table('book')->get(1); -// exibe os nomes das tags atribuídas ao livro -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // exibe o nome da tag através da tabela de ligação -} - -$tag = $explorer->table('tag')->get(1); -// ou o inverso: exibe os nomes dos livros marcados com esta tag -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // exibe o nome do livro -} -``` - -O Explorer novamente otimiza as consultas SQL para uma forma eficiente: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id dos livros selecionados -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id das tags encontradas em book_tag -``` - - -Consulta através de tabelas relacionadas ----------------------------------------- - -Nos métodos `where()`, `select()`, `order()` e `group()`, podemos usar notações especiais para acessar colunas de outras tabelas. O Explorer cria automaticamente os JOINs necessários. - -**Notação de ponto** (`tabela_pai.coluna`) é usada para o relacionamento 1:N do ponto de vista da tabela filho: - -```php -$books = $explorer->table('book'); - -// Encontra livros cujo autor tem nome começando com 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Ordena os livros pelo nome do autor em ordem decrescente -$books->order('author.name DESC'); - -// Exibe o título do livro e o nome do autor -$books->select('book.title, author.name'); -``` - -**Notação de dois pontos** (`:tabela_filho.coluna`) é usada para o relacionamento 1:N do ponto de vista da tabela pai: - -```php -$authors = $explorer->table('author'); - -// Encontra autores que escreveram um livro com 'PHP' no título -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Conta o número de livros para cada autor -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -No exemplo acima com notação de dois pontos (`:book.title`), a coluna com a chave estrangeira não é especificada. O Explorer detecta automaticamente a coluna correta com base no nome da tabela pai. Neste caso, a ligação é feita através da coluna `book.author_id`, porque o nome da tabela de origem é `author`. Se existissem várias ligações possíveis, o Explorer lançaria uma exceção [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -A coluna de ligação pode ser explicitamente especificada entre parênteses: - -```php -// Encontra autores que traduziram um livro com 'PHP' no título -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -As notações podem ser encadeadas para acesso através de múltiplas tabelas: - -```php -// Encontra autores de livros marcados com a tag 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Extensão de condições para JOIN -------------------------------- - -O método `joinWhere()` estende as condições que são especificadas ao ligar tabelas em SQL após a palavra-chave `ON`. - -Digamos que queremos encontrar livros traduzidos por um tradutor específico: - -```php -// Encontra livros traduzidos pelo tradutor chamado 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -Na condição `joinWhere()`, podemos usar as mesmas construções que no método `where()` - operadores, placeholders de interrogação, arrays de valores ou expressões SQL. - -Para consultas mais complexas com múltiplos JOINs, podemos definir aliases de tabela: - -```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) -``` - -Observe que, enquanto o método `where()` adiciona condições à cláusula `WHERE`, o método `joinWhere()` estende as condições na cláusula `ON` ao ligar tabelas. diff --git a/database/pt/guide.texy b/database/pt/guide.texy deleted file mode 100644 index 366588d064..0000000000 --- a/database/pt/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database é uma camada de banco de dados poderosa e elegante para PHP com ênfase na simplicidade e recursos inteligentes. Oferece duas formas de trabalhar com o banco de dados - [Explorer |explorer] para desenvolvimento rápido de aplicações, ou [Acesso SQL |sql-way] para trabalho direto com consultas. - -<div class="grid gap-3"> -<div> - - -[Acesso SQL |sql-way] -===================== -- Consultas parametrizadas seguras -- Controle preciso sobre a forma das consultas SQL -- Quando você escreve consultas complexas com recursos avançados -- Otimiza o desempenho usando funções SQL específicas - -</div> - -<div> - - -[Explorer |explorer] -==================== -- Desenvolve rapidamente sem escrever SQL -- Trabalho intuitivo com relações entre tabelas -- Você apreciará a otimização automática de consultas -- Adequado para trabalho rápido e confortável com o banco de dados - -</div> - -</div> - - -Instalação -========== - -A biblioteca pode ser baixada e instalada usando a ferramenta [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Bancos de dados suportados -========================== - -Nette Database suporta os seguintes bancos de dados: - -|* Servidor de banco de dados |* Nome DSN |* Suporte no Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | SIM -| PostgreSQL (>= 9.0) | pgsql | SIM -| Sqlite 3 (>= 3.8) | sqlite | SIM -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | SIM -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Duas abordagens ao banco de dados -================================= - -Nette Database oferece uma escolha: você pode escrever consultas SQL diretamente (acesso SQL) ou deixá-las serem geradas automaticamente (Explorer). Vejamos como ambas as abordagens resolvem as mesmas tarefas: - -[Acesso SQL|sql-way] - Consultas SQL - -```php -// inserção de registro -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// obtenção de registros: autores de livros -$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 -'); - -// listagem (não otimizada, gera N consultas adicionais) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Autor $author->name escreveu $author->books_count livros:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Acesso Explorer|explorer] - Geração automática de SQL - -```php -// inserção de registro -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// obtenção de registros: autores de livros -$authors = $database->table('authors') - ->where('active', 1); - -// listagem (gera automaticamente apenas 2 consultas otimizadas) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Autor $author->name escreveu {$books->count()} livros:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -A abordagem Explorer gera e otimiza consultas SQL automaticamente. No exemplo fornecido, a abordagem SQL gera N+1 consultas (uma para os autores e depois uma para os livros de cada autor), enquanto o Explorer otimiza automaticamente as consultas e executa apenas duas - uma para os autores e uma para todos os seus livros. - -Ambas as abordagens podem ser combinadas livremente na aplicação conforme necessário. - - -Conexão e configuração -====================== - -Para conectar ao banco de dados, basta criar uma instância da classe [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -O parâmetro `$dsn` (data source name) é o mesmo [que o PDO usa |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], por exemplo, `host=127.0.0.1;dbname=test`. Em caso de falha, lança a exceção `Nette\Database\ConnectionException`. - -No entanto, uma maneira mais conveniente é oferecida pela [configuração da aplicação |configuration], onde basta adicionar a seção `database` e os objetos necessários serão criados, assim como o painel do banco de dados na barra [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Depois, o objeto de conexão [pode ser obtido como um serviço do contêiner DI |dependency-injection:passing-dependencies], por exemplo: - -```php -class Model -{ - public function __construct( - // ou Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Mais informações sobre a [configuração do banco de dados|configuration]. - - -Criação manual do Explorer --------------------------- - -Se você não usa o contêiner Nette DI, pode criar a instância `Nette\Database\Explorer` manualmente: - -```php -// conexão com o banco de dados -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// armazenamento para cache, implementa Nette\Caching\Storage, por exemplo: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// cuida da reflexão da estrutura do banco de dados -$structure = new Nette\Database\Structure($connection, $storage); -// define regras para mapear nomes de tabelas, colunas e chaves estrangeiras -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Gerenciamento de conexão -======================== - -Ao criar o objeto `Connection`, a conexão é estabelecida automaticamente. Se você deseja adiar a conexão, use o modo lazy - ative-o na [configuração|configuration] definindo `lazy` como `true`, ou assim: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Para gerenciar a conexão, use os métodos `connect()`, `disconnect()` e `reconnect()`. -- `connect()` cria a conexão se ela ainda não existir, podendo lançar a exceção `Nette\Database\ConnectionException`. -- `disconnect()` desconecta a conexão atual com o banco de dados. -- `reconnect()` desconecta e, em seguida, reconecta ao banco de dados. Este método também pode lançar a exceção `Nette\Database\ConnectionException`. - -Além disso, você pode monitorar eventos relacionados à conexão usando o evento `onConnect`, que é um array de callbacks chamados após o estabelecimento da conexão com o banco de dados. - -```php -// ocorre após a conexão com o banco de dados -$database->onConnect[] = function($database) { - echo "Conectado ao banco de dados"; -}; -``` - - -Tracy Debug Bar -=============== - -Se você usa [Tracy |tracy:], o painel Database é ativado automaticamente na Debug Bar, exibindo todas as consultas executadas, seus parâmetros, tempo de execução e o local no código onde foram chamadas. - -[* db-panel.webp *] diff --git a/database/pt/mapping.texy b/database/pt/mapping.texy deleted file mode 100644 index d6c415f5ed..0000000000 --- a/database/pt/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Conversão de tipos -****************** - -.[perex] -Nette Database converte automaticamente os valores retornados do banco de dados para os tipos PHP correspondentes. - - -Data e hora ------------ - -Os dados de tempo são convertidos em objetos `Nette\Utils\DateTime`. Se você deseja que os dados de tempo sejam convertidos em objetos imutáveis `Nette\Database\DateTime`, defina a opção `newDateTime` como `true` na [configuração|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'); -``` - -No caso do MySQL, o tipo de dados `TIME` é convertido em objetos `DateInterval`. - - -Valores booleanos ------------------ - -Os valores booleanos são automaticamente convertidos para `true` ou `false`. No MySQL, `TINYINT(1)` é convertido se definirmos `convertBoolean` como `true` na [configuração|configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Valores numéricos ------------------ - -Os valores numéricos são convertidos para `int` ou `float` de acordo com o tipo da coluna no banco de dados: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Normalização personalizada --------------------------- - -Usando o método `setRowNormalizer(?callable $normalizer)`, você pode definir uma função personalizada para transformar linhas do banco de dados. Isso é útil, por exemplo, para a conversão automática de tipos de dados. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // a conversão de tipos ocorre aqui - return $row; -}); -``` diff --git a/database/pt/reflection.texy b/database/pt/reflection.texy deleted file mode 100644 index fba8302010..0000000000 --- a/database/pt/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Reflexão da estrutura -********************* - -.{data-version:3.2.1} -Nette Database fornece ferramentas para introspecção da estrutura do banco de dados usando a classe [api:Nette\Database\Reflection]. Ela permite obter informações sobre tabelas, colunas, índices e chaves estrangeiras. Você pode usar a reflexão para gerar esquemas, criar aplicações flexíveis que trabalham com o banco de dados ou ferramentas gerais de banco de dados. - -Obtemos o objeto de reflexão da instância de conexão com o banco de dados: - -```php -$reflection = $database->getReflection(); -``` - - -Obtenção de tabelas -------------------- - -A propriedade readonly `$reflection->tables` contém um array associativo de todas as tabelas no banco de dados: - -```php -// Listagem dos nomes de todas as tabelas -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Existem mais dois métodos disponíveis: - -```php -// Verificação da existência da tabela -if ($reflection->hasTable('users')) { - echo "A tabela users existe"; -} - -// Retorna o objeto da tabela; se não existir, lança uma exceção -$table = $reflection->getTable('users'); -``` - - -Informações sobre a tabela --------------------------- - -A tabela é representada pelo objeto [Table|api:Nette\Database\Reflection\Table], que fornece as seguintes propriedades readonly: - -- `$name: string` – nome da tabela -- `$view: bool` – se é uma view -- `$fullName: ?string` – nome completo da tabela incluindo o esquema (se existir) -- `$columns: array<string, Column>` – array associativo das colunas da tabela -- `$indexes: Index[]` – array de índices da tabela -- `$primaryKey: ?Index` – chave primária da tabela ou null -- `$foreignKeys: ForeignKey[]` – array de chaves estrangeiras da tabela - - -Colunas -------- - -A propriedade `columns` da tabela fornece um array associativo de colunas, onde a chave é o nome da coluna e o valor é uma instância de [Column|api:Nette\Database\Reflection\Column] com estas propriedades: - -- `$name: string` – nome da coluna -- `$table: ?Table` – referência à tabela da coluna -- `$nativeType: string` – tipo de dados nativo do banco de dados -- `$size: ?int` – tamanho/comprimento do tipo -- `$nullable: bool` – se a coluna pode conter NULL -- `$default: mixed` – valor padrão da coluna -- `$autoIncrement: bool` – se a coluna é auto-increment -- `$primary: bool` – se faz parte da chave primária -- `$vendor: array` – metadados adicionais específicos do sistema de banco de dados - -```php -foreach ($table->columns as $name => $column) { - echo "Coluna: $name\n"; - echo "Tipo: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Sim' : 'Não') . "\n"; -} -``` - - -Índices -------- - -A propriedade `indexes` da tabela fornece um array de índices, onde cada índice é uma instância de [Index|api:Nette\Database\Reflection\Index] com estas propriedades: - -- `$columns: Column[]` – array de colunas que formam o índice -- `$unique: bool` – se o índice é único -- `$primary: bool` – se é a chave primária -- `$name: ?string` – nome do índice - -A chave primária da tabela pode ser obtida usando a propriedade `primaryKey`, que retorna ou um objeto `Index`, ou `null` caso a tabela não tenha chave primária. - -```php -// Listagem de índices -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Índice" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Colunas: $columns\n"; - echo " Unique: " . ($index->unique ? 'Sim' : 'Não') . "\n"; -} - -// Listagem da chave primária -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Chave primária: $columns\n"; -} -``` - - -Chaves estrangeiras -------------------- - -A propriedade `foreignKeys` da tabela fornece um array de chaves estrangeiras, onde cada chave estrangeira é uma instância de [ForeignKey|api:Nette\Database\Reflection\ForeignKey] com estas propriedades: - -- `$foreignTable: Table` – tabela referenciada -- `$localColumns: Column[]` – array de colunas locais -- `$foreignColumns: Column[]` – array de colunas referenciadas -- `$name: ?string` – nome da chave estrangeira - -```php -// Listagem de chaves estrangeiras -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/pt/security.texy b/database/pt/security.texy deleted file mode 100644 index e225154bed..0000000000 --- a/database/pt/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Riscos de segurança -******************* - -<div class=perex> - -O banco de dados frequentemente contém dados sensíveis e permite a execução de operações perigosas. Para trabalhar com segurança com Nette Database, é crucial: - -- Compreender a diferença entre API segura e insegura -- Usar consultas parametrizadas -- Validar corretamente os dados de entrada - -</div> - - -O que é SQL Injection? -====================== - -SQL injection é o risco de segurança mais grave ao trabalhar com um banco de dados. Ocorre quando uma entrada não tratada do usuário se torna parte de uma consulta SQL. Um invasor pode inserir seus próprios comandos SQL e, assim: -- Obter acesso não autorizado aos dados -- Modificar ou excluir dados no banco de dados -- Contornar a autenticação - -```php -// ❌ CÓDIGO PERIGOSO - vulnerável a SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// O invasor pode inserir, por exemplo, o valor: ' OR '1'='1 -// A consulta resultante será: SELECT * FROM users WHERE name = '' OR '1'='1' -// O que retorna todos os usuários -``` - -O mesmo se aplica ao Database Explorer: - -```php -// ❌ CÓDIGO PERIGOSO - vulnerável a SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Consultas parametrizadas -======================== - -A defesa básica contra SQL injection são as consultas parametrizadas. Nette Database oferece várias maneiras de usá-las. - -A maneira mais simples é usar **placeholders de interrogação**: - -```php -// ✅ Consulta parametrizada segura -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Condição segura no Explorer -$table->where('name = ?', $name); -``` - -Isso se aplica a todos os outros métodos no [Database Explorer|explorer] que permitem inserir expressões com placeholders de interrogação e parâmetros. - -Para comandos INSERT, UPDATE ou a cláusula WHERE, podemos passar valores em um array: - -```php -// ✅ INSERT seguro -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ INSERT seguro no Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Validação dos valores dos parâmetros -==================================== - -Consultas parametrizadas são o pilar fundamental do trabalho seguro com bancos de dados. No entanto, os valores que inserimos nelas devem passar por vários níveis de verificação: - - -Verificação de tipo -------------------- - -**O mais importante é garantir o tipo de dados correto dos parâmetros** - esta é uma condição necessária para o uso seguro do Nette Database. O banco de dados assume que todos os dados de entrada têm o tipo de dados correto correspondente à coluna específica. - -Por exemplo, se `$name` nos exemplos anteriores fosse inesperadamente um array em vez de uma string, o Nette Database tentaria inserir todos os seus elementos na consulta SQL, o que levaria a um erro. Portanto, **nunca use** dados não validados de `$_GET`, `$_POST` ou `$_COOKIE` diretamente em consultas de banco de dados. - - -Verificação de formato ----------------------- - -No segundo nível, verificamos o formato dos dados - por exemplo, se as strings estão na codificação UTF-8 e seu comprimento corresponde à definição da coluna, ou se os valores numéricos estão dentro do intervalo permitido para o tipo de dados da coluna. - -Neste nível de validação, podemos confiar parcialmente no próprio banco de dados - muitos bancos de dados rejeitarão dados inválidos. No entanto, o comportamento pode variar, alguns podem truncar silenciosamente strings longas ou cortar números fora do intervalo. - - -Verificação de domínio ----------------------- - -O terceiro nível representa verificações lógicas específicas da sua aplicação. Por exemplo, verificar se os valores das caixas de seleção correspondem às opções oferecidas, se os números estão no intervalo esperado (por exemplo, idade 0-150 anos) ou se as dependências mútuas entre os valores fazem sentido. - - -Métodos de validação recomendados ---------------------------------- - -- Use [Nette Forms |forms:], que garantem automaticamente a validação correta de todas as entradas -- Use [Presenters |application:] e especifique os tipos de dados para os parâmetros nos métodos `action*()` e `render*()` -- Ou implemente sua própria camada de validação usando ferramentas PHP padrão como `filter_var()` - - -Trabalho seguro com colunas -=========================== - -Na seção anterior, mostramos como validar corretamente os valores dos parâmetros. No entanto, ao usar arrays em consultas SQL, devemos prestar a mesma atenção às suas chaves. - -```php -// ❌ CÓDIGO PERIGOSO - as chaves no array não são tratadas -$database->query('INSERT INTO users', $_POST); -``` - -Para comandos INSERT e UPDATE, isso é uma falha de segurança crítica - um invasor pode inserir ou alterar qualquer coluna no banco de dados. Ele poderia, por exemplo, definir `is_admin = 1` ou inserir dados arbitrários em colunas sensíveis (a chamada Mass Assignment Vulnerability). - -Nas condições WHERE, é ainda mais perigoso, pois podem conter operadores: - -```php -// ❌ CÓDIGO PERIGOSO - as chaves no array não são tratadas -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// executa a consulta WHERE (`salary` > 100000) -``` - -Um invasor pode usar essa abordagem para descobrir sistematicamente os salários dos funcionários. Ele pode começar, por exemplo, com uma consulta por salários acima de 100.000, depois abaixo de 50.000 e, estreitando gradualmente o intervalo, pode revelar os salários aproximados de todos os funcionários. Esse tipo de ataque é chamado de SQL enumeration. - -Os métodos `where()` e `whereOr()` são ainda [muito mais flexíveis |explorer#where] e suportam expressões SQL, incluindo operadores e funções, nas chaves e valores. Isso dá ao invasor a possibilidade de realizar SQL injection: - -```php -// ❌ CÓDIGO PERIGOSO - o invasor pode inserir seu próprio SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// executa a consulta WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Este ataque encerra a condição original com `0)`, anexa seu próprio `SELECT` usando `UNION` para obter dados sensíveis da tabela `users` e fecha a consulta sintaticamente correta com `WHERE (1)`. - - -Whitelist de colunas --------------------- - -Para trabalhar com segurança com nomes de colunas, precisamos de um mecanismo que garanta que o usuário só possa trabalhar com colunas permitidas e não possa adicionar as suas próprias. Poderíamos tentar detectar e bloquear nomes de colunas perigosos (blacklist), mas essa abordagem não é confiável - um invasor sempre pode encontrar uma nova maneira de escrever um nome de coluna perigoso que não previmos. - -Portanto, é muito mais seguro inverter a lógica e definir uma lista explícita de colunas permitidas (whitelist): - -```php -// Colunas que o usuário pode editar -$allowedColumns = ['name', 'email', 'active']; - -// Removemos todas as colunas não permitidas da entrada -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Agora podemos usar com segurança em consultas, como por exemplo: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Identificadores dinâmicos -========================= - -Para nomes dinâmicos de tabelas e colunas, use o placeholder `?name`. Isso garante o escape correto dos identificadores de acordo com a sintaxe do banco de dados específico (por exemplo, usando crases no MySQL): - -```php -// ✅ Uso seguro de identificadores confiáveis -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Resultado no MySQL: SELECT `name` FROM `users` -``` - -Importante: use o símbolo `?name` apenas para valores confiáveis definidos no código da aplicação. Para valores do usuário, use novamente a [whitelist |#Whitelist de colunas]. Caso contrário, você se expõe a riscos de segurança: - -```php -// ❌ PERIGOSO - nunca use entrada do usuário -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/pt/sql-way.texy b/database/pt/sql-way.texy deleted file mode 100644 index 11652311b3..0000000000 --- a/database/pt/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -Acesso SQL -********** - -.[perex] -A Nette Database oferece dois caminhos: você pode escrever consultas SQL você mesmo (acesso SQL), ou deixá-las serem geradas automaticamente (veja [Explorer |explorer]). O acesso SQL dá a você controle total sobre as consultas, garantindo ao mesmo tempo sua construção segura. - -.[note] -Detalhes sobre conexão e configuração do banco de dados podem ser encontrados no capítulo [Conexão e configuração |guide#Conexão e configuração]. - - -Consultas básicas -================= - -Para consultar o banco de dados, use o método `query()`. Ele retorna um objeto [ResultSet |api:Nette\Database\ResultSet], que representa o resultado da consulta. Em caso de falha, o método [lança uma exceção|exceptions]. Podemos percorrer o resultado da consulta usando um loop `foreach`, ou usar uma das [funções auxiliares |#Obtenção de dados]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Para inserir valores com segurança em consultas SQL, usamos consultas parametrizadas. A Nette Database torna isso o mais simples possível - basta adicionar uma vírgula e o valor após a consulta SQL: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Com múltiplos parâmetros, você tem duas opções de escrita. Você pode "intercalar" a consulta SQL com parâmetros: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Ou escrever a consulta SQL inteira primeiro e depois anexar todos os parâmetros: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Proteção contra SQL injection -============================= - -Por que é importante usar consultas parametrizadas? Porque elas protegem você contra um ataque chamado SQL injection, no qual um invasor poderia injetar seus próprios comandos SQL e, assim, obter ou danificar dados no banco de dados. - -.[warning] -**Nunca insira variáveis diretamente na consulta SQL!** Sempre use consultas parametrizadas, que protegem você contra SQL injection. - -```php -// ❌ CÓDIGO PERIGOSO - vulnerável a SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Consulta parametrizada segura -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Familiarize-se com os [possíveis riscos de segurança |security]. - - -Técnicas de consulta -==================== - - -Condições WHERE ---------------- - -As condições WHERE podem ser escritas como um array associativo, onde as chaves são os nomes das colunas e os valores são os dados para comparação. A Nette Database seleciona automaticamente o operador SQL mais apropriado com base no tipo de valor. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -Na chave, você também pode especificar explicitamente o operador para comparação: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // usa o operador > - 'name LIKE' => '%John%', // usa o operador LIKE - 'email NOT LIKE' => '%example.com%', // usa o operador NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette trata automaticamente casos especiais como valores `null` ou arrays. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // usa o operador = - 'category_id' => [1, 2, 3], // usa IN - 'description' => null, // usa IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Para condições negativas, use o operador `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // usa o operador <> - 'category_id NOT' => [1, 2, 3], // usa NOT IN - 'description NOT' => null, // usa IS NOT NULL - 'id' => [], // será omitido -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Para combinar condições, o operador `AND` é usado. Isso pode ser alterado usando o [placeholder ?or |#Dicas para construir SQL]. - - -Regras ORDER BY ---------------- - -A ordenação `ORDER BY` pode ser escrita usando um array. Nas chaves, especificamos as colunas e o valor será um booleano indicando se a ordenação é ascendente: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // ascendente - 'name' => false, // descendente -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Inserção de dados (INSERT) --------------------------- - -Para inserir registros, usa-se o comando SQL `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -O método `getInsertId()` retorna o ID da última linha inserida. Em alguns bancos de dados (por exemplo, PostgreSQL), é necessário especificar como parâmetro o nome da sequência da qual o ID deve ser gerado usando `$database->getInsertId($sequenceId)`. - -Como parâmetros, também podemos passar [#valores especiais] como arquivos, objetos DateTime ou tipos enumerados. - -Inserção de múltiplos registros de uma vez: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Um INSERT múltiplo é muito mais rápido porque uma única consulta ao banco de dados é executada, em vez de muitas individuais. - -**Aviso de segurança:** Nunca use dados não validados como `$values`. Familiarize-se com os [possíveis riscos |security#Trabalho seguro com colunas]. - - -Atualização de dados (UPDATE) ------------------------------ - -Para atualizar registros, usa-se o comando SQL `UPDATE`. - -```php -// Atualização de um único registro -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -O número de linhas afetadas é retornado por `$result->getRowCount()`. - -Para UPDATE, podemos usar os operadores `+=` e `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // incrementa login_count -], 1); -``` - -Exemplo de inserção ou atualização de um registro, se ele já existir. Usamos a técnica `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 -``` - -Observe que a Nette Database reconhece em qual contexto do comando SQL o parâmetro com o array é inserido e constrói o código SQL a partir dele de acordo. Assim, do primeiro array, ele construiu `(id, name, year) VALUES (123, 'Jim', 1978)`, enquanto o segundo foi convertido para a forma `name = 'Jim', year = 1978`. Discutimos isso em mais detalhes na seção [#Dicas para construir SQL]. - - -Exclusão de dados (DELETE) --------------------------- - -Para excluir registros, usa-se o comando SQL `DELETE`. Exemplo com obtenção do número de linhas excluídas: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Dicas para construir SQL ------------------------- - -Uma dica é um placeholder especial na consulta SQL que diz como o valor do parâmetro deve ser reescrito em uma expressão SQL: - -| Dica | Descrição | Usado automaticamente -|-----------|-------------------------------------------------|----------------------------- -| `?name` | usa para inserir nome da tabela ou coluna | - -| `?values` | gera `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | gera atribuição `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | combina condições no array com o operador `AND` | `WHERE ?`, `HAVING ?` -| `?or` | combina condições no array com o operador `OR` | - -| `?order` | gera a cláusula `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Para inserir dinamicamente nomes de tabelas e colunas na consulta, use o placeholder `?name`. A Nette Database cuida do tratamento correto dos identificadores de acordo com as convenções do banco de dados específico (por exemplo, envolvendo em crases no MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (no MySQL) -``` - -**Aviso:** use o símbolo `?name` apenas para nomes de tabelas e colunas de entradas validadas, caso contrário, você se expõe a um [risco de segurança |security#Identificadores dinâmicos]. - -Outras dicas geralmente não precisam ser especificadas, pois Nette usa detecção automática inteligente ao montar a consulta SQL (veja a terceira coluna da tabela). Mas você pode usá-la, por exemplo, em uma situação em que deseja combinar condições usando `OR` em vez de `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' -``` - - -Valores especiais ------------------ - -Além dos tipos escalares comuns (string, int, bool), você também pode passar valores especiais como parâmetros: - -- arquivos: `fopen('image.gif', 'r')` insere o conteúdo binário do arquivo -- data e hora: objetos `DateTime` são convertidos para o formato do banco de dados -- tipos enumerados: instâncias `enum` são convertidas para seu valor -- literais SQL: criados com `Connection::literal('NOW()')` são inseridos diretamente na consulta - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Para bancos de dados que não têm suporte nativo para o tipo de dados `datetime` (como SQLite e Oracle), `DateTime` é convertido para o valor especificado na [configuração do banco de dados|configuration] pelo item `formatDateTime` (o valor padrão é `U` - timestamp Unix). - - -Literais SQL ------------- - -Em alguns casos, você precisa especificar diretamente o código SQL como um valor, que não deve ser entendido como uma string e escapado. Para isso, servem os objetos da classe `Nette\Database\SqlLiteral`. Eles são criados pelo método `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()) -``` - -Ou alternativamente: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Literais SQL podem conter parâmetros: - -```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) -``` - -Graças a isso, podemos criar combinações interessantes: - -```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') -``` - - -Obtenção de dados -================= - - -Atalhos para consultas SELECT ------------------------------ - -Para simplificar a recuperação de dados, `Connection` oferece vários atalhos que combinam a chamada `query()` com a subsequente `fetch*()`. Esses métodos aceitam os mesmos parâmetros que `query()`, ou seja, a consulta SQL e parâmetros opcionais. Uma descrição completa dos métodos `fetch*()` pode ser encontrada [abaixo |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Executa a consulta e retorna a primeira linha como um objeto `Row` -| `fetchAll($sql, ...$params): array` | Executa a consulta e retorna todas as linhas como um array de objetos `Row` -| `fetchPairs($sql, ...$params): array` | Executa a consulta e retorna um array associativo, onde a primeira coluna representa a chave e a segunda o valor -| `fetchField($sql, ...$params): mixed` | Executa a consulta e retorna o valor do primeiro campo da primeira linha -| `fetchList($sql, ...$params): ?array` | Executa a consulta e retorna a primeira linha como um array indexado - -Exemplo: - -```php -// fetchField() - retorna o valor da primeira célula -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - iteração sobre linhas ---------------------------------- - -Após a execução da consulta, é retornado um objeto [ResultSet|api:Nette\Database\ResultSet], que permite percorrer os resultados de várias maneiras. A maneira mais fácil de executar uma consulta e obter linhas é iterando em um loop `foreach`. Este método é o mais eficiente em termos de memória, pois retorna os dados gradualmente e não os armazena na memória de uma vez. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` só pode ser iterado uma vez. Se precisar iterar repetidamente, você deve primeiro carregar os dados em um array, por exemplo, usando o método `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Retorna a linha como um objeto `Row`. Se não houver mais linhas, retorna `null`. Move o ponteiro interno para a próxima linha. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // carrega a primeira linha -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Retorna todas as linhas restantes do `ResultSet` como um array de objetos `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // carrega todas as linhas -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Retorna os resultados como um array associativo. O primeiro argumento especifica o nome da coluna a ser usada como chave no array, o segundo argumento especifica o nome da coluna a ser usada como valor: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Se especificarmos apenas o primeiro parâmetro, o valor será a linha inteira, ou seja, o objeto `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -Em caso de chaves duplicadas, o valor da última linha é usado. Ao usar `null` como chave, o array será indexado numericamente a partir de zero (então não ocorrem colisões): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativamente, você pode fornecer um callback como parâmetro, que retornará para cada linha ou o próprio valor, ou um par chave-valor. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// O callback também pode retornar um array com um par chave & valor: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Retorna o valor do primeiro campo da linha atual. Se não houver mais linhas, retorna `null`. Move o ponteiro interno para a próxima linha. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // carrega o nome da primeira linha -``` - - -fetchList(): ?array .[method] ------------------------------ - -Retorna a linha como um array indexado. Se não houver mais linhas, retorna `null`. Move o ponteiro interno para a próxima linha. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Retorna o número de linhas afetadas pela última consulta `UPDATE` ou `DELETE`. Para `SELECT`, é o número de linhas retornadas, mas isso pode não ser conhecido - nesse caso, o método retorna `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Retorna o número de colunas no `ResultSet`. - - -Informações sobre consultas -=========================== - -Para fins de depuração, podemos obter informações sobre a última consulta executada: - -```php -echo $database->getLastQueryString(); // imprime a consulta SQL - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // imprime a consulta SQL -echo $result->getTime(); // imprime o tempo de execução em segundos -``` - -Para exibir o resultado como uma tabela HTML, pode-se usar: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -ResultSet oferece informações sobre os tipos das colunas: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column é do tipo $type->type"; // por exemplo, 'id é do tipo int' -} -``` - - -Registro de consultas ---------------------- - -Podemos implementar nosso próprio registro de consultas. O evento `onQuery` é um array de callbacks que são chamados após cada consulta executada: - -```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/pt/transactions.texy b/database/pt/transactions.texy deleted file mode 100644 index eaa93de03e..0000000000 --- a/database/pt/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Transações -********** - -.[perex] -As transações garantem que todas as operações dentro de uma transação sejam executadas ou nenhuma delas seja executada. Elas são úteis para garantir a consistência dos dados em operações mais complexas. - -A maneira mais simples de usar transações é assim: - -```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; -} -``` - -Você pode escrever a mesma coisa de forma muito mais elegante usando o método `transaction()`. Ele recebe um callback como parâmetro, que executa dentro da transação. Se o callback for executado sem exceção, a transação é automaticamente confirmada. Se ocorrer uma exceção, a transação é cancelada (rollback) e a exceção é propagada. - -```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' - ]); -}); -``` - -O método `transaction()` também pode retornar valores: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // retorna o número de linhas atualizadas -}); -``` diff --git a/database/ro/@home.texy b/database/ro/@home.texy deleted file mode 100644 index e6e3ddb3ab..0000000000 --- a/database/ro/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Baze de date suportate -====================== - -Nette suportă următoarele baze de date: - -|* Server bază de date |* Nume DSN |* Suport în Core |* Suport în Explorer -| MySQL (>= 5.1) | mysql | DA | DA -| PostgreSQL (>= 9.0) | pgsql | DA | DA -| Sqlite 3 (>= 3.8) | sqlite | DA | DA -| Oracle | oci | DA | - -| MS SQL (PDO_SQLSRV) | sqlsrv | DA | DA -| MS SQL (PDO_DBLIB) | mssql | DA | - -| ODBC | odbc | DA | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database simplifică semnificativ recuperarea datelor din baza de date fără a fi nevoie să scrieți interogări SQL. Execută interogări eficiente și nu transferă date inutile.}} diff --git a/database/ro/@left-menu.texy b/database/ro/@left-menu.texy deleted file mode 100644 index 7e37400986..0000000000 --- a/database/ro/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Introducere |guide] -- [Abordare SQL |sql way] -- [Explorer |Explorer] -- [Tranzacții |transactions] -- [Excepții |exceptions] -- [Reflecție |reflection] -- [Mapare |mapping] -- [Configurație |configuration] -- [Riscuri de securitate |security] -- [Actualizare |en:upgrading] diff --git a/database/ro/@meta.texy b/database/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/database/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/database/ro/configuration.texy b/database/ro/configuration.texy deleted file mode 100644 index c1948e2cb1..0000000000 --- a/database/ro/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Configurarea bazei de date -************************** - -.[perex] -Prezentare generală a opțiunilor de configurare pentru Nette Database. - -Dacă nu utilizați întregul framework, ci doar această bibliotecă, citiți [cum să încărcați configurația |bootstrap:]. - - -O singură conexiune -------------------- - -Configurarea unei singure conexiuni la baza de date: - -```neon -database: - # DSN, singura cheie obligatorie - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Creează serviciile `Nette\Database\Connection` și `Nette\Database\Explorer`, pe care de obicei le transmitem prin [autowiring |dependency-injection:autowiring], eventual prin referință la [numele lor |#Servicii DI]. - -Alte setări: - -```neon -database: - # afișează panoul database în Tracy Bar? - debugger: ... # (bool) implicit este true - - # afișează EXPLAIN pentru interogări în Tracy Bar? - explain: ... # (bool) implicit este true - - # permite autowiring pentru această conexiune? - autowired: ... # (bool) implicit este true la prima conexiune - - # convenții pentru tabele: discovered, static sau numele clasei - conventions: discovered # (string) implicit este 'discovered' - - options: - # conectare la baza de date doar când este necesar? - lazy: ... # (bool) implicit este false - - # clasa PHP a driverului bazei de date - driverClass: # (string) - - # doar MySQL: setează sql_mode - sqlmode: # (string) - - # doar MySQL: setează SET NAMES - charset: # (string) implicit este 'utf8mb4' - - # doar MySQL: convertește TINYINT(1) la bool - convertBoolean: # (bool) implicit este false - - # returnează coloanele cu dată ca obiecte imutabile (de la versiunea 3.2.1) - newDateTime: # (bool) implicit este false - - # doar Oracle și SQLite: format pentru stocarea datei - formatDateTime: # (string) implicit este 'U' -``` - -În cheia `options` se pot specifica și alte opțiuni, pe care le găsiți în [documentația driverelor PDO |https://www.php.net/manual/en/pdo.drivers.php], cum ar fi: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Mai multe conexiuni -------------------- - -În configurație putem defini și mai multe conexiuni la baza de date împărțindu-le în secțiuni numite: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring-ul este activat doar pentru serviciile din prima secțiune. Acest lucru poate fi schimbat folosind `autowired: false` sau `autowired: true`. - - -Servicii DI ------------ - -Aceste servicii sunt adăugate în containerul DI, unde `###` reprezintă numele conexiunii: - -| Nume | Tip | Descriere -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | conexiune la baza de date -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Dacă definim doar o singură conexiune, numele serviciilor vor fi `database.default.connection` și `database.default.explorer`. Dacă definim mai multe conexiuni ca în exemplul de mai sus, numele vor corespunde secțiunilor, adică `database.main.connection`, `database.main.explorer` și apoi `database.another.connection` și `database.another.explorer`. - -Serviciile ne-autowired le transmitem explicit prin referință la numele lor: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/ro/exceptions.texy b/database/ro/exceptions.texy deleted file mode 100644 index 7a2084e0d6..0000000000 --- a/database/ro/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Excepții -******** - -Nette Database utilizează o ierarhie de excepții. Clasa de bază este `Nette\Database\DriverException`, care moștenește din `PDOException` și oferă posibilități extinse pentru lucrul cu erorile bazei de date: - -- Metoda `getDriverCode()` returnează codul de eroare de la driverul bazei de date -- Metoda `getSqlState()` returnează codul SQLSTATE -- Metodele `getQueryString()` și `getParameters()` permit obținerea interogării originale și a parametrilor săi - -Din `DriverException` moștenesc următoarele excepții specializate: - -- `ConnectionException` - semnalează eșecul conexiunii la serverul bazei de date -- `ConstraintViolationException` - clasa de bază pentru încălcarea constrângerilor bazei de date, din care moștenesc: - - `ForeignKeyConstraintViolationException` - încălcarea cheii străine - - `NotNullConstraintViolationException` - încălcarea constrângerii NOT NULL - - `UniqueConstraintViolationException` - încălcarea unicității valorii - - -Exemplu de capturare a excepției `UniqueConstraintViolationException`, care apare atunci când încercăm să inserăm un utilizator cu un email care există deja în baza de date (presupunând că coloana email are un index unic). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Utilizatorul cu acest email există deja.'; - -} catch (Nette\Database\DriverException $e) { - echo 'A apărut o eroare la înregistrare: ' . $e->getMessage(); -} -``` diff --git a/database/ro/explorer.texy b/database/ro/explorer.texy deleted file mode 100644 index f7cb938248..0000000000 --- a/database/ro/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer oferă o modalitate intuitivă și eficientă de a lucra cu baza de date. Se ocupă automat de legăturile dintre tabele și de optimizarea interogărilor, astfel încât să vă puteți concentra pe aplicația dvs. Funcționează imediat fără configurare. Dacă aveți nevoie de control total asupra interogărilor SQL, puteți utiliza [abordarea SQL |sql-way]. - -- Lucrul cu datele este natural și ușor de înțeles -- Generează interogări SQL optimizate, care încarcă doar datele necesare -- Permite accesul facil la datele conexe fără a fi nevoie să scrieți interogări JOIN -- Funcționează imediat fără nicio configurare sau generare de entități - -</div> - - -Cu Explorer începeți prin apelarea metodei `table()` a obiectului [api:Nette\Database\Explorer] (detalii despre conectare găsiți în capitolul [Conectare și configurare |guide#Conectare și configurare]): - -```php -$books = $explorer->table('book'); // 'book' este numele tabelei -``` - -Metoda returnează obiectul [Selection |api:Nette\Database\Table\Selection], care reprezintă o interogare SQL. Pe acest obiect putem înlănțui alte metode pentru filtrarea și sortarea rezultatelor. Interogarea se construiește și se execută abia în momentul în care începem să solicităm date. De exemplu, prin parcurgerea cu ciclul `foreach`. Fiecare rând este reprezentat de obiectul [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // afișarea coloanei 'title' - echo $book->author_id; // afișarea coloanei 'author_id' -} -``` - -Explorer facilitează în mod fundamental lucrul cu [legăturile dintre tabele |#Relații între tabele]. Următorul exemplu arată cât de ușor putem afișa date din tabele legate (cărți și autorii lor). Observați că nu trebuie să scriem nicio interogare JOIN, Nette le creează pentru noi: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Carte: ' . $book->title; - echo 'Autor: ' . $book->author->name; // creează JOIN pe tabela 'author' -} -``` - -Nette Database Explorer optimizează interogările pentru a fi cât mai eficiente. Exemplul de mai sus execută doar două interogări SELECT, indiferent dacă procesăm 10 sau 10 000 de cărți. - -În plus, Explorer urmărește ce coloane sunt utilizate în cod și încarcă din baza de date doar acelea, economisind astfel performanță suplimentară. Acest comportament este complet automat și adaptiv. Dacă modificați ulterior codul și începeți să utilizați alte coloane, Explorer ajustează automat interogările. Nu trebuie să setați nimic, nici să vă gândiți ce coloane veți avea nevoie - lăsați asta pe seama Nette. - - -Filtrare și sortare -=================== - -Clasa `Selection` oferă metode pentru filtrarea și sortarea selecției de date. - -.[language-php] -| `where($condition, ...$params)` | Adaugă condiția WHERE. Mai multe condiții sunt legate cu operatorul AND -| `whereOr(array $conditions)` | Adaugă un grup de condiții WHERE legate cu operatorul OR -| `wherePrimary($value)` | Adaugă condiția WHERE după cheia primară -| `order($columns, ...$params)` | Setează sortarea ORDER BY -| `select($columns, ...$params)` | Specifică coloanele care trebuie încărcate -| `limit($limit, $offset = null)` | Limitează numărul de rânduri (LIMIT) și opțional setează OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Setează paginarea -| `group($columns, ...$params)` | Grupează rândurile (GROUP BY) -| `having($condition, ...$params)` | Adaugă condiția HAVING pentru filtrarea rândurilor grupate - -Metodele pot fi înlănțuite (așa-numitul [fluent interface |nette:introduction-to-object-oriented-programming#Interfețe fluente]): `$table->where(...)->order(...)->limit(...)`. - -În aceste metode puteți utiliza și notația specială pentru accesarea [datelor din tabelele conexe |#Interogarea prin tabele asociate]. - - -Escapare și identificatori --------------------------- - -Metodele escapează automat parametrii și încadrează identificatorii (numele tabelelor și coloanelor) în ghilimele, prevenind astfel SQL injection. Pentru funcționarea corectă este necesar să respectați câteva reguli: - -- Cuvintele cheie, numele funcțiilor, procedurilor etc. scrieți-le cu **majuscule**. -- Numele coloanelor și tabelelor scrieți-le cu **litere mici**. -- Șirurile de caractere introduceți-le întotdeauna prin **parametri**. - -```php -where('name = ' . $name); // VULNERABILITATE CRITICĂ: SQL injection -where('name LIKE "%search%"'); // GREȘIT: complică încadrarea automată în ghilimele -where('name LIKE ?', '%search%'); // CORECT: valoare introdusă prin parametru - -where('name like ?', $name); // GREȘIT: generează: `name` `like` ? -where('name LIKE ?', $name); // CORECT: generează: `name` LIKE ? -where('LOWER(name) = ?', $value);// CORECT: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtrează rezultatele folosind condiții WHERE. Punctul său forte este lucrul inteligent cu diferite tipuri de valori și alegerea automată a operatorilor SQL. - -Utilizare de bază: - -```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' -``` - -Datorită detectării automate a operatorilor potriviți, nu trebuie să ne ocupăm de diverse cazuri speciale. Nette le rezolvă pentru noi: - -```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) -// se poate utiliza și semnul de întrebare substituent fără operator: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Metoda procesează corect și condițiile negative și array-urile goale: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- nu găsește nimic -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- găsește tot -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- găsește tot -// $table->where('NOT id ?', $ids); Atenție - această sintaxă nu este suportată -``` - -Ca parametru putem transmite și rezultatul dintr-o altă tabelă - se va crea o subinterogare: - -```php -// 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')); -``` - -Condițiile le putem transmite și ca array, ale cărui elemente se vor uni cu AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -În array putem folosi perechi cheie => valoare și Nette alege din nou automat operatorii corecți: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -În array putem combina expresii SQL cu semne de întrebare substituente și mai mulți parametri. Acest lucru este potrivit pentru condiții complexe cu operatori definiți precis: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // doi parametri îi transmitem ca array -]); -``` - -Apelurile multiple ale `where()` leagă automat condițiile cu AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Similar cu `where()`, adaugă condiții, dar cu diferența că le leagă cu OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Și aici putem folosi expresii mai complexe: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Adaugă condiția pentru cheia primară a tabelei: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Dacă tabela are o cheie primară compozită (de ex. `foo_id`, `bar_id`), o transmitem ca 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(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Determină ordinea în care vor fi returnate rândurile. Putem sorta după una sau mai multe coloane, în ordine descrescătoare sau crescătoare, sau după o expresie proprie: - -```php -$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(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Specifică coloanele care trebuie returnate din baza de date. În mod implicit, Nette Database Explorer returnează doar acele coloane care sunt utilizate efectiv în cod. Metoda `select()` o folosim deci în cazurile în care avem nevoie să returnăm expresii specifice: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Aliasurile definite cu `AS` sunt apoi disponibile ca proprietăți ale obiectului ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // acces la alias -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Limitează numărul de rânduri returnate (LIMIT) și opțional permite setarea unui offset: - -```php -$table->limit(10); // LIMIT 10 (returnează primele 10 rânduri) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Pentru paginare este mai potrivită utilizarea metodei `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Facilitează paginarea rezultatelor. Acceptă numărul paginii (numărat de la 1) și numărul de elemente pe pagină. Opțional, se poate transmite o referință la o variabilă în care se va stoca numărul total de pagini: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, numOfPages: $numOfPages); -echo "Total pagini: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Grupează rândurile după coloanele specificate (GROUP BY). Se utilizează de obicei în combinație cu funcții de agregare: - -```php -// Calculează numărul de produse din fiecare categorie -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Setează condiția pentru filtrarea rândurilor grupate (HAVING). Poate fi utilizată în combinație cu metoda `group()` și funcții de agregare: - -```php -// Găsește categoriile care au mai mult de 100 de produse -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Citirea datelor -=============== - -Pentru citirea datelor din baza de date avem la dispoziție câteva metode utile: - -.[language-php] -| `foreach ($table as $key => $row)` | Iterează peste toate rândurile, `$key` este valoarea cheii primare, `$row` este obiectul ActiveRow -| `$row = $table->get($key)` | Returnează un rând după cheia primară -| `$row = $table->fetch()` | Returnează rândul curent și mută pointerul la următorul -| `$array = $table->fetchPairs()` | Creează un array asociativ din rezultate -| `$array = $table->fetchAll()` | Returnează toate rândurile ca array -| `count($table)` | Returnează numărul de rânduri din obiectul Selection - -Obiectul [ActiveRow |api:Nette\Database\Table\ActiveRow] este destinat doar citirii. Acest lucru înseamnă că nu se pot modifica valorile proprietăților sale. Această limitare asigură consistența datelor și previne efectele secundare neașteptate. Datele sunt încărcate din baza de date și orice modificare ar trebui efectuată explicit și controlat. - - -`foreach` - iterare peste toate rândurile ------------------------------------------ - -Cel mai simplu mod de a executa o interogare și de a obține rândurile este iterarea într-un ciclu `foreach`. Lansează automat interogarea SQL. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key este valoarea cheii primare, $book este ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Execută interogarea SQL și returnează rândul după cheia primară, sau `null`, dacă nu există. - -```php -$book = $explorer->table('book')->get(123); // returnează ActiveRow cu ID 123 sau null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Returnează rândul și mută pointerul intern la următorul. Dacă nu mai există alte rânduri, returnează `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Returnează rezultatele ca array asociativ. Primul argument specifică numele coloanei care se va utiliza ca cheie în array, al doilea argument specifică numele coloanei care se va utiliza ca valoare: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Dacă specificăm doar primul parametru, valoarea va fi întregul rând, adică obiectul `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -În cazul cheilor duplicate, se va utiliza valoarea din ultimul rând. La utilizarea `null` ca cheie, array-ul va fi indexat numeric de la zero (atunci nu apar coliziuni): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativ, puteți specifica ca parametru un callback, care va returna pentru fiecare rând fie valoarea însăși, fie perechea cheie-valoare. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Prima carte (Jan Novák)', ...] - -// Callback-ul poate returna și un array cu perechea cheie & valoare: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Prima carte' => 'Jan Novák', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Returnează toate rândurile ca array asociativ de obiecte `ActiveRow`, unde cheile sunt valorile cheilor primare. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Metoda `count()` fără parametru returnează numărul de rânduri din obiectul `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternativă -``` - -Atenție, `count()` cu parametru execută funcția de agregare COUNT în baza de date. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Convertește obiectul `ActiveRow` într-un array asociativ, unde cheile sunt numele coloanelor și valorile sunt datele corespunzătoare. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray va fi ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Agregace -======== - -Clasa `Selection` oferă metode pentru executarea ușoară a funcțiilor de agregare (COUNT, SUM, MIN, MAX, AVG etc.). - -.[language-php] -| `count($expr)` | Numără numărul de rânduri -| `min($expr)` | Returnează valoarea minimă din coloană -| `max($expr)` | Returnează valoarea maximă din coloană -| `sum($expr)` | Returnează suma valorilor din coloană -| `aggregation($function)` | Permite executarea oricărei funcții de agregare. De ex. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Execută interogarea SQL cu funcția COUNT și returnează rezultatul. Metoda se utilizează pentru a afla câte rânduri corespund unei anumite condiții: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Atenție, [#count()] fără parametru returnează doar numărul de rânduri din obiectul `Selection`. - - -min(string $expr) și max(string $expr) .[method] ------------------------------------------------- - -Metodele `min()` și `max()` returnează valoarea minimă și maximă din coloana sau expresia specificată: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Returnează suma valorilor din coloana sau expresia specificată: - -```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] ----------------------------------------------------------------------- - -Permite executarea oricărei funcții de agregare. - -```php -// prețul mediu al produselor din categorie -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// unește etichetele produsului într-un singur șir -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Dacă avem nevoie să agregăm rezultate care deja provin dintr-o funcție de agregare și grupare (de ex. `SUM(valoare)` peste rândurile grupate), ca al doilea argument specificăm funcția de agregare care trebuie aplicată acestor rezultate intermediare: - -```php -// Calculează prețul total al produselor din stoc pentru fiecare categorie și apoi adună aceste prețuri. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -În acest exemplu, mai întâi calculăm prețul total al produselor din fiecare categorie (`SUM(price * stock) AS category_total`) și grupăm rezultatele după `category_id`. Apoi folosim `aggregation('SUM(category_total)', 'SUM')` pentru a aduna aceste sume intermediare `category_total`. Al doilea argument `'SUM'` spune că funcția SUM trebuie aplicată rezultatelor intermediare. - - -Insert, Update & Delete -======================= - -Nette Database Explorer simplifică inserarea, actualizarea și ștergerea datelor. Toate metodele menționate aruncă excepția `Nette\Database\DriverException` în caz de eroare. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Inserează înregistrări noi în tabelă. - -**Inserarea unei singure înregistrări:** - -Transmitem noua înregistrare ca array asociativ sau obiect iterabil (de exemplu, ArrayHash utilizat în [formulare |forms:]), unde cheile corespund numelor coloanelor din tabelă. - -Dacă tabela are o cheie primară definită, metoda returnează un obiect `ActiveRow`, care este reîncărcat din baza de date pentru a reflecta eventualele modificări efectuate la nivelul bazei de date (triggere, valori implicite ale coloanelor, calcule ale coloanelor auto-increment). Astfel se asigură consistența datelor și obiectul conține întotdeauna datele actuale din baza de date. Dacă nu are o cheie primară unică, returnează datele transmise sub formă de array. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row este o instanță ActiveRow și conține datele complete ale rândului inserat, -// inclusiv ID-ul generat automat și eventualele modificări efectuate de triggere -echo $row->id; // Afișează ID-ul utilizatorului nou inserat -echo $row->created_at; // Afișează timpul creării, dacă este setat de un trigger -``` - -**Inserarea mai multor înregistrări deodată:** - -Metoda `insert()` permite inserarea mai multor înregistrări printr-o singură interogare SQL. În acest caz, returnează numărul de rânduri inserate. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows va fi 2 -``` - -Ca parametru se poate transmite și un obiect `Selection` cu selecția de date. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Inserarea valorilor speciale:** - -Ca valori putem transmite și fișiere, obiecte DateTime sau literali SQL: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // convertește la formatul bazei de date - 'avatar' => fopen('image.jpg', 'rb'), // inserează conținutul binar al fișierului - 'uuid' => $explorer::literal('UUID()'), // apelează funcția UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Actualizează rândurile din tabelă conform filtrului specificat. Returnează numărul de rânduri efectiv modificate. - -Coloanele modificate le transmitem ca array asociativ sau obiect iterabil (de exemplu, ArrayHash utilizat în [formulare |forms:]), unde cheile corespund numelor coloanelor din tabelă: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Pentru modificarea valorilor numerice putem folosi operatorii `+=` și `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // crește valoarea coloanei 'points' cu 1 - 'coins-=' => 1, // scade valoarea coloanei 'coins' cu 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Șterge rândurile din tabelă conform filtrului specificat. Returnează numărul de rânduri șterse. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -La apelarea `update()` și `delete()`, nu uitați să specificați rândurile care trebuie modificate/șterse folosind `where()`. Dacă nu utilizați `where()`, operația se va efectua pe întreaga tabelă! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Actualizează datele din rândul bazei de date reprezentat de obiectul `ActiveRow`. Ca parametru acceptă un iterabil cu datele care trebuie actualizate (cheile sunt numele coloanelor). Pentru modificarea valorilor numerice putem folosi operatorii `+=` și `-=`: - -După efectuarea actualizării, `ActiveRow` se reîncarcă automat din baza de date pentru a reflecta eventualele modificări efectuate la nivelul bazei de date (de ex. triggere). Metoda returnează true doar dacă a avut loc o modificare efectivă a datelor. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // creștem numărul de vizualizări -]); -echo $article->views; // Afișează numărul curent de vizualizări -``` - -Această metodă actualizează doar un singur rând specific din baza de date. Pentru actualizarea în masă a mai multor rânduri, utilizați metoda [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Șterge rândul din baza de date, care este reprezentat de obiectul `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Șterge cartea cu ID 1 -``` - -Această metodă șterge doar un singur rând specific din baza de date. Pentru ștergerea în masă a mai multor rânduri, utilizați metoda [#Selection::delete()]. - - -Relații între tabele -==================== - -În bazele de date relaționale, datele sunt împărțite în mai multe tabele și interconectate prin chei străine. Nette Database Explorer aduce o modalitate revoluționară de a lucra cu aceste legături - fără a scrie interogări JOIN și fără a fi nevoie să configurați sau să generați ceva. - -Pentru a ilustra lucrul cu legăturile, vom folosi exemplul bazei de date de cărți ([îl găsiți pe GitHub |https://github.com/nette-examples/books]). În baza de date avem tabelele: - -- `author` - scriitori și traducători (coloane `id`, `name`, `web`, `born`) -- `book` - cărți (coloane `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - etichete (coloane `id`, `name`) -- `book_tag` - tabelă de legătură între cărți și etichete (coloane `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Structura bazei de date folosită în exemple .<> - -În exemplul nostru de bază de date de cărți găsim mai multe tipuri de relații (deși modelul este simplificat față de realitate): - -- One-to-many 1:N – fiecare carte **are un** autor, autorul poate scrie **mai multe** cărți -- Zero-to-many 0:N – cartea **poate avea** un traducător, traducătorul poate traduce **mai multe** cărți -- Zero-to-one 0:1 – cartea **poate avea** o continuare -- Many-to-many M:N – cartea **poate avea mai multe** etichete și o etichetă poate fi atribuită **mai multor** cărți - -În aceste relații există întotdeauna o tabelă părinte și una copil. De exemplu, în relația dintre autor și carte, tabela `author` este părinte și `book` este copil - ne putem imagina că o carte "aparține" întotdeauna unui autor. Acest lucru se reflectă și în structura bazei de date: tabela copil `book` conține cheia străină `author_id`, care face referire la tabela părinte `author`. - -Dacă avem nevoie să afișăm cărțile inclusiv numele autorilor lor, avem două opțiuni. Fie obținem datele printr-o singură interogare SQL folosind JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Fie încărcăm datele în doi pași - mai întâi cărțile și apoi autorii lor - și apoi le asamblăm în PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- id-urile autorilor cărților obținute -``` - -A doua abordare este de fapt mai eficientă, deși poate fi surprinzător. Datele sunt încărcate o singură dată și pot fi utilizate mai bine în cache. Exact în acest mod lucrează Nette Database Explorer - rezolvă totul sub capotă și vă oferă o API elegantă: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'titlu: ' . $book->title; - echo 'scris de: ' . $book->author->name; // $book->author este înregistrarea din tabela 'author' - echo 'tradus de: ' . $book->translator?->name; -} -``` - - -Accesul la tabela părinte -------------------------- - -Accesul la tabela părinte este direct. Este vorba despre relații precum *cartea are un autor* sau *cartea poate avea un traducător*. Obținem înregistrarea asociată prin proprietatea obiectului ActiveRow - numele său corespunde numelui coloanei cu cheia străină fără `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // găsește autorul după coloana author_id -echo $book->translator?->name; // găsește traducătorul după translator_id -``` - -Când accesăm proprietatea `$book->author`, Explorer caută în tabela `book` o coloană al cărei nume conține șirul `author` (adică `author_id`). După valoarea din această coloană, încarcă înregistrarea corespunzătoare din tabela `author` și o returnează ca `ActiveRow`. Similar funcționează și `$book->translator`, care utilizează coloana `translator_id`. Deoarece coloana `translator_id` poate conține `null`, folosim în cod operatorul `?->`. - -O cale alternativă o oferă metoda `ref()`, care acceptă doi argumente, numele tabelei țintă și numele coloanei de legătură, și returnează o instanță `ActiveRow` sau `null`: - -```php -echo $book->ref('author', 'author_id')->name; // legătura cu autorul -echo $book->ref('author', 'translator_id')->name; // legătura cu traducătorul -``` - -Metoda `ref()` este utilă dacă nu se poate utiliza accesul prin proprietate, deoarece tabela conține o coloană cu același nume (adică `author`). În celelalte cazuri, se recomandă utilizarea accesului prin proprietate, care este mai lizibil. - -Explorer optimizează automat interogările bazei de date. Când parcurgem cărțile într-un ciclu și accesăm înregistrările lor asociate (autori, traducători), Explorer nu generează o interogare pentru fiecare carte în parte. În schimb, execută doar un singur SELECT pentru fiecare tip de legătură, reducând semnificativ sarcina bazei de date. De exemplu: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Acest cod apelează doar aceste trei interogări fulgerătoare în baza de date: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id din coloana author_id a cărților selectate -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id din coloana translator_id a cărților selectate -``` - -.[note] -Logica de căutare a coloanei de legătură este dată de implementarea [Conventions |api:Nette\Database\Conventions]. Recomandăm utilizarea [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], care analizează cheile străine și permite lucrul simplu cu relațiile existente între tabele. - - -Accesul la tabela copil ------------------------ - -Accesul la tabela copil funcționează în direcția opusă. Acum întrebăm *ce cărți a scris acest autor* sau *a tradus acest traducător*. Pentru acest tip de interogare folosim metoda `related()`, care returnează `Selection` cu înregistrările asociate. Să vedem un exemplu: - -```php -$author = $explorer->table('author')->get(1); - -// Afișează toate cărțile autorului -foreach ($author->related('book.author_id') as $book) { - echo "A scris: $book->title"; -} - -// Afișează toate cărțile pe care autorul le-a tradus -foreach ($author->related('book.translator_id') as $book) { - echo "A tradus: $book->title"; -} -``` - -Metoda `related()` acceptă descrierea legăturii ca un singur argument cu notație cu punct sau ca doi argumente separate: - -```php -$author->related('book.translator_id'); // un argument -$author->related('book', 'translator_id'); // doi argumente -``` - -Explorer poate detecta automat coloana de legătură corectă pe baza numelui tabelei părinte. În acest caz, se leagă prin coloana `book.author_id`, deoarece numele tabelei sursă este `author`: - -```php -$author->related('book'); // utilizează book.author_id -``` - -Dacă ar exista mai multe legături posibile, Explorer ar arunca excepția [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Metoda `related()` o putem folosi, desigur, și la parcurgerea mai multor înregistrări într-un ciclu și Explorer optimizează automat interogările și în acest caz: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' a scris:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Acest cod generează doar două interogări SQL fulgerătoare: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id-urile autorilor selectați -``` - - -Legătura Many-to-many ---------------------- - -Pentru legătura many-to-many (M:N) este necesară existența unei tabele de legătură (în cazul nostru `book_tag`), care conține două coloane cu chei străine (`book_id`, `tag_id`). Fiecare dintre aceste coloane face referire la cheia primară a uneia dintre tabelele legate. Pentru a obține datele asociate, mai întâi obținem înregistrările din tabela de legătură folosind `related('book_tag')` și apoi continuăm către datele țintă: - -```php -$book = $explorer->table('book')->get(1); -// afișează numele etichetelor atribuite cărții -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // afișează numele etichetei prin tabela de legătură -} - -$tag = $explorer->table('tag')->get(1); -// sau invers: afișează numele cărților etichetate cu această etichetă -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // afișează numele cărții -} -``` - -Explorer optimizează din nou interogările SQL într-o formă eficientă: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id-urile cărților selectate -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id-urile etichetelor găsite în book_tag -``` - - -Interogarea prin tabele asociate --------------------------------- - -În metodele `where()`, `select()`, `order()` și `group()` putem folosi notații speciale pentru accesarea coloanelor din alte tabele. Explorer creează automat JOIN-urile necesare. - -**Notația cu punct** (`tabela_parinte.coloana`) se utilizează pentru relația 1:N din perspectiva tabelei copil: - -```php -$books = $explorer->table('book'); - -// Găsește cărțile al căror autor are numele începând cu 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Sortează cărțile după numele autorului descrescător -$books->order('author.name DESC'); - -// Afișează titlul cărții și numele autorului -$books->select('book.title, author.name'); -``` - -**Notația cu două puncte** (`:tabela_copil.coloana`) se utilizează pentru relația 1:N din perspectiva tabelei părinte: - -```php -$authors = $explorer->table('author'); - -// Găsește autorii care au scris o carte cu 'PHP' în titlu -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Calculează numărul de cărți pentru fiecare autor -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -În exemplul de mai sus cu notația cu două puncte (`:book.title`), nu este specificată coloana cu cheia străină. Explorer detectează automat coloana corectă pe baza numelui tabelei părinte. În acest caz, se leagă prin coloana `book.author_id`, deoarece numele tabelei sursă este `author`. Dacă ar exista mai multe legături posibile, Explorer ar arunca excepția [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Coloana de legătură poate fi specificată explicit în paranteză: - -```php -// Găsește autorii care au tradus o carte cu 'PHP' în titlu -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Notațiile pot fi înlănțuite pentru accesul prin mai multe tabele: - -```php -// Găsește autorii cărților etichetate cu 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Extinderea condițiilor pentru JOIN ----------------------------------- - -Metoda `joinWhere()` extinde condițiile care se specifică la legarea tabelelor în SQL după cuvântul cheie `ON`. - -Să presupunem că dorim să găsim cărțile traduse de un anumit traducător: - -```php -// Găsește cărțile traduse de traducătorul numit 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -În condiția `joinWhere()` putem folosi aceleași construcții ca în metoda `where()` - operatori, semne de întrebare substituente, array-uri de valori sau expresii SQL. - -Pentru interogări mai complexe cu mai multe JOIN-uri, putem defini aliasuri pentru tabele: - -```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) -``` - -Observați că, în timp ce metoda `where()` adaugă condiții în clauza `WHERE`, metoda `joinWhere()` extinde condițiile în clauza `ON` la legarea tabelelor. diff --git a/database/ro/guide.texy b/database/ro/guide.texy deleted file mode 100644 index f315aff423..0000000000 --- a/database/ro/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database este un strat de baze de date puternic și elegant pentru PHP, cu accent pe simplitate și funcții inteligente. Oferă două moduri de a lucra cu baza de date - [Explorer |explorer] pentru dezvoltarea rapidă a aplicațiilor sau [abordarea SQL |sql-way] pentru lucrul direct cu interogări. - -<div class="grid gap-3"> -<div> - - -[Abordarea SQL |sql-way] -======================== -- Interogări parametrizate sigure -- Control precis asupra formei interogărilor SQL -- Când scrieți interogări complexe cu funcții avansate -- Optimizați performanța folosind funcții SQL specifice - -</div> - -<div> - - -[Explorer |explorer] -==================== -- Dezvoltați rapid fără a scrie SQL -- Lucru intuitiv cu relațiile dintre tabele -- Apreciați optimizarea automată a interogărilor -- Potrivit pentru lucrul rapid și confortabil cu baza de date - -</div> - -</div> - - -Instalare -========= - -Descărcați și instalați biblioteca folosind [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Baze de date suportate -====================== - -Nette Database suportă următoarele baze de date: - -|* Server de baze de date |* Nume DSN |* Suport în Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | DA -| PostgreSQL (>= 9.0) | pgsql | DA -| Sqlite 3 (>= 3.8) | sqlite | DA -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | DA -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Două abordări ale bazei de date -=============================== - -Nette Database vă oferă o alegere: puteți fie să scrieți interogări SQL direct (abordarea SQL), fie să le lăsați generate automat (Explorer). Să vedem cum ambele abordări rezolvă aceleași sarcini: - -[Abordarea SQL|sql-way] - Interogări SQL - -```php -// inserarea unei înregistrări -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// obținerea înregistrărilor: autorii cărților -$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 -'); - -// listare (nu este optimă, generează N interogări suplimentare) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Autorul $author->name a scris $author->books_count cărți:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Abordarea Explorer|explorer] - generare automată SQL - -```php -// inserarea unei înregistrări -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// obținerea înregistrărilor: autorii cărților -$authors = $database->table('authors') - ->where('active', 1); - -// listare (generează automat doar 2 interogări optimizate) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Autorul $author->name a scris {$books->count()} cărți:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Abordarea Explorer generează și optimizează interogările SQL automat. În exemplul dat, abordarea SQL generează N+1 interogări (una pentru autori și apoi una pentru cărțile fiecărui autor), în timp ce Explorer optimizează automat interogările și execută doar două - una pentru autori și una pentru toate cărțile lor. - -Ambele abordări pot fi combinate liber în aplicație după cum este necesar. - - -Conectare și configurare -======================== - -Pentru a vă conecta la baza de date, trebuie doar să creați o instanță a clasei [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Parametrul `$dsn` (data source name) este același [ca cel utilizat de PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], de ex. `mysql:host=127.0.0.1;dbname=test`. În caz de eșec, aruncă o excepție `Nette\Database\ConnectionException`. - -Cu toate acestea, o modalitate mai convenabilă este oferită de [configurația aplicației |configuration], unde trebuie doar să adăugați secțiunea `database` și se vor crea obiectele necesare, precum și panoul bazei de date în bara [Tracy |tracy:]. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Apoi, [obținem obiectul conexiunii ca serviciu din containerul DI |dependency-injection:passing-dependencies], de exemplu: - -```php -class Model -{ - public function __construct( - // sau Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Mai multe informații despre [configurarea bazei de date |configuration]. - - -Crearea manuală a Explorer --------------------------- - -Dacă nu utilizați containerul Nette DI, puteți crea manual o instanță `Nette\Database\Explorer`: - -```php -// conectare la baza de date -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// stocare pentru cache, implementează Nette\Caching\Storage, de ex.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// se ocupă de reflexia structurii bazei de date -$structure = new Nette\Database\Structure($connection, $storage); -// definește reguli pentru maparea numelor de tabele, coloane și chei străine -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Gestionarea conexiunii -====================== - -La crearea obiectului `Connection`, conexiunea se stabilește automat. Dacă doriți să amânați conexiunea, utilizați modul lazy - îl activați în [configurație |configuration] setând `lazy: true`, sau astfel: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Pentru gestionarea conexiunii, utilizați metodele `connect()`, `disconnect()` și `reconnect()`. -- `connect()` creează conexiunea dacă nu există deja, putând arunca o excepție `Nette\Database\ConnectionException`. -- `disconnect()` deconectează conexiunea curentă la baza de date. -- `reconnect()` efectuează deconectarea și apoi reconectarea la baza de date. Această metodă poate arunca, de asemenea, o excepție `Nette\Database\ConnectionException`. - -În plus, puteți monitoriza evenimentele legate de conexiune folosind evenimentul `onConnect`, care este un array de callback-uri care sunt apelate după stabilirea conexiunii cu baza de date. - -```php -// se execută după conectarea la baza de date -$database->onConnect[] = function($database) { - echo "Conectat la baza de date"; -}; -``` - - -Bara de depanare Tracy -====================== - -Dacă utilizați [Tracy |tracy:], panoul Database se activează automat în bara de depanare, afișând toate interogările executate, parametrii lor, timpul de execuție și locul din cod unde au fost apelate. - -[* db-panel.webp *] diff --git a/database/ro/mapping.texy b/database/ro/mapping.texy deleted file mode 100644 index e64374fb06..0000000000 --- a/database/ro/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Conversia tipurilor -******************* - -.[perex] -Nette Database convertește automat valorile returnate din baza de date în tipurile PHP corespunzătoare. - - -Data și ora ------------ - -Datele de timp sunt convertite în obiecte `Nette\Utils\DateTime`. Dacă doriți ca datele de timp să fie convertite în obiecte imuabile `Nette\Database\DateTime`, setați opțiunea `newDateTime` la true în [configurație|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'); -``` - -În cazul MySQL, convertește tipul de date `TIME` în obiecte `DateInterval`. - - -Valori booleene ---------------- - -Valorile booleene sunt convertite automat în `true` sau `false`. Pentru MySQL, se convertește `TINYINT(1)` dacă setăm `convertBoolean: true` în [configurație |configuration]. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Valori numerice ---------------- - -Valorile numerice sunt convertite în `int` sau `float` în funcție de tipul coloanei din baza de date: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Normalizare personalizată -------------------------- - -Folosind metoda `setRowNormalizer(?callable $normalizer)`, puteți seta o funcție personalizată pentru transformarea rândurilor din baza de date. Acest lucru este util, de exemplu, pentru conversia automată a tipurilor de date. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // aici are loc conversia tipurilor - return $row; -}); -``` diff --git a/database/ro/reflection.texy b/database/ro/reflection.texy deleted file mode 100644 index 33121e9002..0000000000 --- a/database/ro/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Reflexia structurii -******************* - -.{data-version:3.2.1} -Nette Database oferă instrumente pentru introspecția structurii bazei de date folosind clasa [api:Nette\Database\Structure]. Aceasta permite obținerea de informații despre tabele, coloane, indecși și chei străine. Puteți utiliza reflexia pentru a genera scheme, a crea aplicații flexibile care lucrează cu baza de date sau instrumente generale pentru baze de date. - -Obținem obiectul structurii din instanța conexiunii la baza de date: - -```php -$reflection = $database->getReflection(); -``` - - -Obținerea tabelelor -------------------- - -Metoda `getTables()` returnează un array cu informații despre toate tabelele: - -```php -// Listarea numelor tuturor tabelelor -foreach ($structure->getTables() as $table) { - echo $table['name'] . "\n"; -} -``` - -Sunt disponibile încă două metode: - -```php -// Verificarea existenței tabelului -if ($reflection->hasTable('users')) { - echo "Tabelul users există"; -} - -// Returnează obiectul tabelului; dacă nu există, aruncă o excepție -$table = $reflection->getTable('users'); -``` - - -Informații despre tabel ------------------------ - -Tabelul este reprezentat de obiectul [Table|api:Nette\Database\Reflection\Table], care oferă următoarele proprietăți readonly: - -- `$name: string` – numele tabelului -- `$view: bool` – dacă este o vizualizare -- `$fullName: ?string` – numele complet al tabelului, inclusiv schema (dacă există) -- `$columns: array<string, Column>` – array asociativ al coloanelor tabelului -- `$indexes: Index[]` – array de indecși ai tabelului -- `$primaryKey: ?Index` – cheia primară a tabelului sau null -- `$foreignKeys: ForeignKey[]` – array de chei străine ale tabelului - - -Coloane -------- - -Proprietatea `columns` a tabelului oferă un array asociativ de coloane, unde cheia este numele coloanei și valoarea este o instanță [Column|api:Nette\Database\Reflection\Column] cu aceste proprietăți: - -- `$name: string` – numele coloanei -- `$table: ?Table` – referință la tabelul coloanei -- `$nativeType: string` – tipul de date nativ al bazei de date -- `$size: ?int` – dimensiunea/lungimea tipului -- `$nullable: bool` – dacă coloana poate conține NULL -- `$default: mixed` – valoarea implicită a coloanei -- `$autoIncrement: bool` – dacă coloana este auto-increment -- `$primary: bool` – dacă face parte din cheia primară -- `$vendor: array` – metadate suplimentare specifice sistemului de baze de date respectiv - -```php -foreach ($table->columns as $name => $column) { - echo "Coloană: $name\n"; - echo "Tip: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Da' : 'Nu') . "\n"; -} -``` - - -Indecși -------- - -Proprietatea `indexes` a tabelului oferă un array de indecși, unde fiecare index este o instanță [Index|api:Nette\Database\Reflection\Index] cu aceste proprietăți: - -- `$columns: Column[]` – array de coloane care formează indexul -- `$unique: bool` – dacă indexul este unic -- `$primary: bool` – dacă este cheia primară -- `$name: ?string` – numele indexului - -Cheia primară a tabelului poate fi obținută folosind proprietatea `primaryKey`, care returnează fie un obiect `Index`, fie `null` în cazul în care tabelul nu are cheie primară. - -```php -// Listarea indecșilor -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Index" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Coloane: $columns\n"; - echo " Unic: " . ($index->unique ? 'Da' : 'Nu') . "\n"; -} - -// Listarea cheii primare -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Cheie primară: $columns\n"; -} -``` - - -Chei străine ------------- - -Proprietatea `foreignKeys` a tabelului oferă un array de chei străine, unde fiecare cheie străină este o instanță [ForeignKey|api:Nette\Database\Reflection\ForeignKey] cu aceste proprietăți: - -- `$foreignTable: Table` – tabelul referit -- `$localColumns: Column[]` – array de coloane locale -- `$foreignColumns: Column[]` – array de coloane referite -- `$name: ?string` – numele cheii străine - -```php -// Listarea cheilor străine -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/ro/security.texy b/database/ro/security.texy deleted file mode 100644 index fc616b8c1c..0000000000 --- a/database/ro/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Riscuri de securitate -********************* - -<div class=perex> - -Baza de date conține adesea date sensibile și permite efectuarea de operațiuni periculoase. Pentru a lucra în siguranță cu Nette Database, este esențial să: - -- Înțelegeți diferența dintre API-ul sigur și cel nesigur -- Utilizați interogări parametrizate -- Validați corect datele de intrare - -</div> - - -Ce este SQL Injection? -====================== - -SQL injection este cel mai grav risc de securitate atunci când lucrați cu o bază de date. Apare atunci când intrarea nesanitizată de la utilizator devine parte a unei interogări SQL. Atacatorul poate introduce propriile comenzi SQL și astfel: -- Obține acces neautorizat la date -- Modifică sau șterge datele din baza de date -- Ocolește autentificarea - -```php -// ❌ COD PERICULOS - vulnerabil la SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Atacatorul poate introduce, de exemplu, valoarea: ' OR '1'='1 -// Interogarea rezultată va fi: SELECT * FROM users WHERE name = '' OR '1'='1' -// Ceea ce returnează toți utilizatorii -``` - -Același lucru este valabil și pentru Database Explorer: - -```php -// ❌ COD PERICULOS - vulnerabil la SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Interogări parametrizate -======================== - -Apărarea de bază împotriva SQL injection sunt interogările parametrizate. Nette Database oferă mai multe moduri de a le utiliza. - -Cel mai simplu mod este utilizarea **semnelor de întrebare placeholder**: - -```php -// ✅ Interogare parametrizată sigură -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Condiție sigură în Explorer -$table->where('name = ?', $name); -``` - -Acest lucru este valabil pentru toate celelalte metode din [Database Explorer |explorer], care permit inserarea de expresii cu semne de întrebare placeholder și parametri. - -Pentru comenzile INSERT, UPDATE sau clauza WHERE, putem transmite valorile într-un array: - -```php -// ✅ INSERT sigur -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ INSERT sigur în Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Validarea valorilor parametrilor -================================ - -Interogările parametrizate sunt piatra de temelie a lucrului sigur cu baza de date. Cu toate acestea, valorile pe care le introducem în ele trebuie să treacă prin mai multe niveluri de control: - - -Controlul tipului ------------------ - -**Cel mai important este să se asigure tipul corect de date al parametrilor** - aceasta este o condiție necesară pentru utilizarea sigură a Nette Database. Baza de date presupune că toate datele de intrare au tipul de date corect corespunzător coloanei respective. - -De exemplu, dacă `$name` din exemplele anterioare ar fi în mod neașteptat un array în loc de un șir, Nette Database ar încerca să insereze toate elementele sale în interogarea SQL, ceea ce ar duce la o eroare. Prin urmare, **nu utilizați niciodată** date nevalidate din `$_GET`, `$_POST` sau `$_COOKIE` direct în interogările bazei de date. - - -Controlul formatului --------------------- - -La al doilea nivel, verificăm formatul datelor - de exemplu, dacă șirurile sunt în codificare UTF-8 și lungimea lor corespunde definiției coloanei, sau dacă valorile numerice se încadrează în intervalul permis pentru tipul de date al coloanei respective. - -La acest nivel de validare, ne putem baza parțial și pe baza de date însăși - multe baze de date vor refuza datele nevalide. Cu toate acestea, comportamentul poate varia, unele pot scurta în tăcere șirurile lungi sau pot trunchia numerele în afara intervalului. - - -Controlul domeniului --------------------- - -Al treilea nivel constă în controale logice specifice aplicației dvs. De exemplu, verificarea faptului că valorile din casetele de selecție corespund opțiunilor oferite, că numerele se încadrează în intervalul așteptat (de exemplu, vârsta 0-150 de ani) sau că dependențele reciproce dintre valori au sens. - - -Metode de validare recomandate ------------------------------- - -- Utilizați [Formulare Nette |forms:], care asigură automat validarea corectă a tuturor intrărilor -- Utilizați [Presentere |application:presenters] și specificați tipurile de date pentru parametrii din metodele `action*()` și `render*()` -- Sau implementați propriul strat de validare folosind instrumente PHP standard precum `filter_var()` - - -Lucrul sigur cu coloanele -========================= - -În secțiunea anterioară, am arătat cum să validăm corect valorile parametrilor. Cu toate acestea, atunci când folosim array-uri în interogările SQL, trebuie să acordăm aceeași atenție și cheilor lor. - -```php -// ❌ COD PERICULOS - cheile din array nu sunt tratate -$database->query('INSERT INTO users', $_POST); -``` - -Pentru comenzile INSERT și UPDATE, aceasta este o eroare de securitate fundamentală - atacatorul poate introduce sau modifica orice coloană în baza de date. Ar putea, de exemplu, să seteze `is_admin = 1` sau să introducă date arbitrare în coloane sensibile (așa-numita Mass Assignment Vulnerability). - -În condițiile WHERE, este și mai periculos, deoarece pot conține operatori: - -```php -// ❌ COD PERICULOS - cheile din array nu sunt tratate -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// execută interogarea WHERE (`salary` > 100000) -``` - -Atacatorul poate folosi această abordare pentru a descoperi sistematic salariile angajaților. De exemplu, începe cu o interogare pentru salarii peste 100.000, apoi sub 50.000 și, prin restrângerea treptată a intervalului, poate descoperi salariile aproximative ale tuturor angajaților. Acest tip de atac se numește SQL enumeration. - -Metodele `where()` și `whereOr()` sunt și [mult mai flexibile |explorer#where] și suportă expresii SQL în chei și valori, inclusiv operatori și funcții. Acest lucru îi oferă atacatorului posibilitatea de a efectua SQL injection: - -```php -// ❌ COD PERICULOS - atacatorul poate introduce propriul SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// execută interogarea WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Acest atac încheie condiția originală folosind `0)`, adaugă propriul `SELECT` folosind `UNION` pentru a obține date sensibile din tabelul `users` și închide interogarea sintactic corectă folosind `WHERE (1)`. - - -Lista albă a coloanelor ------------------------ - -Pentru a lucra în siguranță cu numele coloanelor, avem nevoie de un mecanism care să asigure că utilizatorul poate lucra doar cu coloanele permise și nu poate adăuga propriile coloane. Am putea încerca să detectăm și să blocăm numele de coloane periculoase (lista neagră), dar această abordare nu este fiabilă - atacatorul poate găsi întotdeauna o nouă modalitate de a scrie un nume de coloană periculos pe care nu l-am prevăzut. - -Prin urmare, este mult mai sigur să inversăm logica și să definim o listă explicită de coloane permise (lista albă): - -```php -// Coloane pe care utilizatorul le poate modifica -$allowedColumns = ['name', 'email', 'active']; - -// Eliminăm toate coloanele nepermise din intrare -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Acum putem folosi în siguranță în interogări, cum ar fi: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Identificatori dinamici -======================= - -Pentru numele dinamice de tabele și coloane, utilizați substituentul `?name`. Acesta asigură escaparea corectă a identificatorilor conform sintaxei bazei de date respective (de exemplu, folosind ghilimele inverse în MySQL): - -```php -// ✅ Utilizare sigură a identificatorilor de încredere -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Rezultat în MySQL: SELECT `name` FROM `users` -``` - -Important: utilizați simbolul `?name` numai pentru valori de încredere definite în codul aplicației. Pentru valorile de la utilizator, utilizați din nou [lista albă |#Lista albă a coloanelor]. Altfel, vă expuneți riscurilor de securitate: - -```php -// ❌ PERICULOS - nu utilizați niciodată intrarea de la utilizator -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/ro/sql-way.texy b/database/ro/sql-way.texy deleted file mode 100644 index 3276671a8d..0000000000 --- a/database/ro/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -Abordarea SQL -************* - -.[perex] -Nette Database oferă două abordări: puteți scrie interogări SQL singur (abordarea SQL) sau le puteți lăsa generate automat (vezi [Explorer |explorer]). Abordarea SQL vă oferă control complet asupra interogărilor și, în același timp, asigură construirea lor în siguranță. - -.[note] -Detalii despre conectarea și configurarea bazei de date găsiți în capitolul [Conectare și configurare |guide#Conectare și configurare]. - - -Interogare de bază -================== - -Pentru interogarea bazei de date se folosește metoda `query()`. Aceasta returnează un obiect [ResultSet |api:Nette\Database\ResultSet], care reprezintă rezultatul interogării. În caz de eșec, metoda [aruncă o excepție |exceptions]. Putem parcurge rezultatul interogării folosind bucla `foreach` sau putem folosi una dintre [funcțiile auxiliare |#Obținerea datelor]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Pentru inserarea sigură a valorilor în interogările SQL, folosim interogări parametrizate. Nette Database le face extrem de simple - trebuie doar să adăugați o virgulă și valoarea după interogarea SQL: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Pentru mai mulți parametri, aveți două opțiuni de scriere. Fie puteți "intercala" interogarea SQL cu parametri: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Fie scrieți mai întâi întreaga interogare SQL și apoi adăugați toți parametrii: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Protecție împotriva SQL injection -================================= - -De ce este important să folosim interogări parametrizate? Deoarece vă protejează împotriva atacului numit SQL injection, în care un atacator ar putea introduce propriile comenzi SQL și astfel să obțină sau să deterioreze datele din baza de date. - -.[warning] -**Nu introduceți niciodată variabile direct în interogarea SQL!** Folosiți întotdeauna interogări parametrizate, care vă protejează împotriva SQL injection. - -```php -// ❌ COD PERICULOS - vulnerabil la SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Interogare parametrizată sigură -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Familiarizați-vă cu [posibilele riscuri de securitate |security]. - - -Tehnici de interogare -===================== - - -Condiții WHERE --------------- - -Condițiile WHERE pot fi scrise ca un array asociativ, unde cheile sunt numele coloanelor și valorile sunt datele pentru comparație. Nette Database selectează automat operatorul SQL cel mai potrivit în funcție de tipul valorii. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -În cheie, puteți specifica explicit și operatorul pentru comparație: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // folosește operatorul > - 'name LIKE' => '%John%', // folosește operatorul LIKE - 'email NOT LIKE' => '%example.com%', // folosește operatorul NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette tratează automat cazurile speciale precum valorile `null` sau array-urile. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // folosește operatorul = - 'category_id' => [1, 2, 3], // folosește IN - 'description' => null, // folosește IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Pentru condiții negative, utilizați operatorul `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // folosește operatorul <> - 'category_id NOT' => [1, 2, 3], // folosește NOT IN - 'description NOT' => null, // folosește IS NOT NULL - 'id' => [], // se omite -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Pentru combinarea condițiilor se folosește operatorul `AND`. Acest lucru poate fi schimbat folosind [substituentul ?or |#Indicații pentru construirea SQL]. - - -Reguli ORDER BY ---------------- - -Sortarea `ORDER BY` poate fi scrisă folosind un array. În chei specificăm coloanele, iar valoarea va fi un boolean care determină dacă se sortează ascendent: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // ascendent - 'name' => false, // descendent -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Inserarea datelor (INSERT) --------------------------- - -Pentru inserarea înregistrărilor se folosește comanda SQL `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Metoda `getInsertId()` returnează ID-ul ultimului rând inserat. Pentru unele baze de date (de ex. PostgreSQL), este necesar să specificați ca parametru numele secvenței din care trebuie generat ID-ul folosind `$database->getInsertId($sequenceId)`. - -Ca parametri putem transmite și [#valori speciale] precum fișiere, obiecte DateTime sau tipuri enum. - -Inserarea mai multor înregistrări simultan: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -INSERT-ul multiplu este mult mai rapid, deoarece se execută o singură interogare la baza de date, în loc de multe interogări individuale. - -**Avertisment de securitate:** Nu utilizați niciodată date nevalidate ca `$values`. Familiarizați-vă cu [posibilele riscuri |security#Lucrul sigur cu coloanele]. - - -Actualizarea datelor (UPDATE) ------------------------------ - -Pentru actualizarea înregistrărilor se folosește comanda SQL `UPDATE`. - -```php -// Actualizarea unei singure înregistrări -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Numărul de rânduri afectate este returnat de `$result->getRowCount()`. - -Pentru UPDATE putem folosi operatorii `+=` și `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // incrementarea login_count -], 1); -``` - -Exemplu de inserare sau modificare a unei înregistrări, dacă aceasta există deja. Folosim tehnica `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 -``` - -Observați că Nette Database recunoaște în ce context al comenzii SQL este inserat parametrul cu array-ul și, în funcție de aceasta, construiește codul SQL din el. Astfel, din primul array a construit `(id, name, year) VALUES (123, 'Jim', 1978)`, în timp ce al doilea l-a convertit în forma `name = 'Jim', year = 1978`. Detaliem acest aspect în secțiunea [#Indicații pentru construirea SQL]. - - -Ștergerea datelor (DELETE) --------------------------- - -Pentru ștergerea înregistrărilor se folosește comanda SQL `DELETE`. Exemplu cu obținerea numărului de rânduri șterse: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Indicații pentru construirea SQL --------------------------------- - -O indicație este un substituent special în interogarea SQL care specifică modul în care valoarea parametrului trebuie rescrisă într-o expresie SQL: - -| Indicație | Descriere | Se utilizează automat -|-----------|-------------------------------------------------|----------------------------- -| `?name` | se utilizează pentru inserarea numelui tabelului sau coloanei | - -| `?values` | generează `(cheie, ...) VALUES (valoare, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | generează atribuirea `cheie = valoare, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | combină condițiile din array cu operatorul `AND` | `WHERE ?`, `HAVING ?` -| `?or` | combină condițiile din array cu operatorul `OR` | - -| `?order` | generează clauza `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Pentru inserarea dinamică a numelor de tabele și coloane în interogare se folosește substituentul `?name`. Nette Database se ocupă de tratarea corectă a identificatorilor conform convențiilor bazei de date respective (de ex. încadrarea în ghilimele inverse în MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (în MySQL) -``` - -**Avertisment:** utilizați simbolul `?name` numai pentru numele de tabele și coloane din intrări validate, altfel vă expuneți unui [risc de securitate |security#Identificatori dinamici]. - -Celelalte indicații de obicei nu trebuie specificate, deoarece Nette folosește o autodetecție inteligentă la construirea interogării SQL (vezi a treia coloană a tabelului). Dar le puteți utiliza, de exemplu, într-o situație în care doriți să combinați condițiile folosind `OR` în loc de `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' -``` - - -Valori speciale ---------------- - -Pe lângă tipurile scalare obișnuite (string, int, bool), puteți transmite ca parametri și valori speciale: - -- fișiere: `fopen('image.gif', 'r')` inserează conținutul binar al fișierului -- data și ora: obiectele `DateTime` sunt convertite în formatul bazei de date -- tipuri enum: instanțele `enum` sunt convertite în valoarea lor -- literali SQL: creați folosind `Connection::literal('NOW()')` sunt inserați direct în interogare - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Pentru bazele de date care nu au suport nativ pentru tipul de date `datetime` (precum SQLite și Oracle), `DateTime` este convertit în valoarea specificată în [configurația bazei de date |configuration] prin elementul `formatDateTime` (valoarea implicită este `U` - timestamp unix). - - -Literali SQL ------------- - -În unele cazuri, trebuie să specificați direct cod SQL ca valoare, care însă nu trebuie interpretat ca șir și escapat. Pentru aceasta se folosesc obiectele clasei `Nette\Database\SqlLiteral`. Acestea sunt create de 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()) -``` - -Sau alternativ: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -Literalii SQL pot conține parametri: - -```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) -``` - -Datorită cărora putem crea combinații interesante: - -```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') -``` - - -Obținerea datelor -================= - - -Scurtături pentru interogări SELECT ------------------------------------ - -Pentru a simplifica încărcarea datelor, `Connection` oferă câteva scurtături care combină apelul `query()` cu următorul `fetch*()`. Aceste metode acceptă aceiași parametri ca `query()`, adică interogarea SQL și parametrii opționali. O descriere completă a metodelor `fetch*()` găsiți [mai jos |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Execută interogarea și returnează primul rând ca obiect `Row` -| `fetchAll($sql, ...$params): array` | Execută interogarea și returnează toate rândurile ca array de obiecte `Row` -| `fetchPairs($sql, ...$params): array` | Execută interogarea și returnează un array asociativ, unde prima coloană reprezintă cheia și a doua valoarea -| `fetchField($sql, ...$params): mixed` | Execută interogarea și returnează valoarea primului câmp din primul rând -| `fetchList($sql, ...$params): ?array` | Execută interogarea și returnează primul rând ca array indexat - -Exemplu: - -```php -// fetchField() - returnează valoarea primei celule -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - iterarea prin rânduri ---------------------------------- - -După executarea interogării, se returnează obiectul [ResultSet|api:Nette\Database\ResultSet], care permite parcurgerea rezultatelor în mai multe moduri. Cel mai simplu mod de a executa o interogare și de a obține rânduri este prin iterarea într-o buclă `foreach`. Această metodă este cea mai eficientă din punct de vedere al memoriei, deoarece returnează datele treptat și nu le stochează pe toate în memorie simultan. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` poate fi iterat o singură dată. Dacă aveți nevoie să iterați în mod repetat, trebuie mai întâi să încărcați datele într-un array, de exemplu folosind metoda `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Returnează un rând ca obiect `Row`. Dacă nu mai există alte rânduri, returnează `null`. Mută pointerul intern la următorul rând. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // încarcă primul rând -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Returnează toate rândurile rămase din `ResultSet` ca un array de obiecte `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // încarcă toate rândurile -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Returnează rezultatele ca un array asociativ. Primul argument specifică numele coloanei care va fi folosită ca cheie în array, al doilea argument specifică numele coloanei care va fi folosită ca valoare: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Dacă specificăm doar primul parametru, valoarea va fi întregul rând, adică obiectul `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -În cazul cheilor duplicate, se va folosi valoarea din ultimul rând. La utilizarea `null` ca cheie, array-ul va fi indexat numeric începând de la zero (atunci nu apar coliziuni): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativ, puteți specifica ca parametru un callback care va returna pentru fiecare rând fie valoarea însăși, fie o pereche cheie-valoare. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback-ul poate returna și un array cu perechea cheie & valoare: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Returnează valoarea primului câmp din rândul curent. Dacă nu mai există alte rânduri, returnează `null`. Mută pointerul intern la următorul rând. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // încarcă numele din primul rând -``` - - -fetchList(): ?array .[method] ------------------------------ - -Returnează un rând ca array indexat. Dacă nu mai există alte rânduri, returnează `null`. Mută pointerul intern la următorul rând. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Returnează numărul de rânduri afectate de ultima interogare `UPDATE` sau `DELETE`. Pentru `SELECT`, este numărul de rânduri returnate, dar acesta poate să nu fie cunoscut - în acest caz, metoda returnează `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Returnează numărul de coloane din `ResultSet`. - - -Informații despre interogări -============================ - -În scopuri de depanare, putem obține informații despre ultima interogare executată: - -```php -echo $database->getLastQueryString(); // afișează interogarea SQL - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // afișează interogarea SQL -echo $result->getTime(); // afișează timpul de execuție în secunde -``` - -Pentru a afișa rezultatul ca tabel HTML, se poate folosi: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -ResultSet oferă informații despre tipurile coloanelor: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column este de tip $type->type"; // de ex. 'id este de tip int' -} -``` - - -Logarea interogărilor ---------------------- - -Putem implementa propria logare a interogărilor. Evenimentul `onQuery` este un array de callback-uri care sunt apelate după fiecare interogare executată: - -```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/ro/transactions.texy b/database/ro/transactions.texy deleted file mode 100644 index 86fa0b3bf8..0000000000 --- a/database/ro/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Tranzacții -********** - -.[perex] -Tranzacțiile garantează că fie toate operațiunile din cadrul tranzacției sunt efectuate, fie niciuna nu este efectuată. Acestea sunt utile pentru a asigura consistența datelor în cazul operațiunilor mai complexe. - -Cel mai simplu mod de a utiliza tranzacțiile arată astfel: - -```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; -} -``` - -Puteți scrie același lucru mult mai elegant folosind metoda `transaction()`. Aceasta acceptă un callback ca parametru, pe care îl execută în cadrul tranzacției. Dacă callback-ul se execută fără excepții, tranzacția este confirmată automat. Dacă apare o excepție, tranzacția este anulată (rollback), iar excepția este propagată mai departe. - -```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()` poate returna și valori: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // returnează numărul de rânduri actualizate -}); -``` diff --git a/database/sl/@home.texy b/database/sl/@home.texy deleted file mode 100644 index 5c0ffe069e..0000000000 --- a/database/sl/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Podprte podatkovne baze -======================= - -Nette podpira naslednje podatkovne baze: - -|* Strežnik podatkovne baze |* Ime DSN |* Podpora v Core |* Podpora v Explorer -| MySQL (>= 5.1) | mysql | DA | DA -| PostgreSQL (>= 9.0) | pgsql | DA | DA -| Sqlite 3 (>= 3.8) | sqlite | DA | DA -| Oracle | oci | DA | - -| MS SQL (PDO_SQLSRV) | sqlsrv | DA | DA -| MS SQL (PDO_DBLIB) | mssql | DA | - -| ODBC | odbc | DA | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database bistveno poenostavlja pridobivanje podatkov iz podatkovne baze brez potrebe po pisanju SQL poizvedb. Postavlja učinkovite poizvedbe in ne prenaša nepotrebnih podatkov.}} diff --git a/database/sl/@left-menu.texy b/database/sl/@left-menu.texy deleted file mode 100644 index 280bbea034..0000000000 --- a/database/sl/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Uvod |guide] -- [SQL pristop |sql way] -- [Explorer |Explorer] -- [Transakcije |transactions] -- [Izjeme |exceptions] -- [Refleksija |reflection] -- [Preslikava |mapping] -- [Konfiguracija |configuration] -- [Varnostna tveganja |security] -- [Nadgradnja |en:upgrading] diff --git a/database/sl/@meta.texy b/database/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/database/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/database/sl/configuration.texy b/database/sl/configuration.texy deleted file mode 100644 index 1fd404c6d1..0000000000 --- a/database/sl/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Konfiguracija podatkovne baze -***************************** - -.[perex] -Pregled konfiguracijskih možnosti za Nette Database. - -Če ne uporabljate celotnega ogrodja, ampak samo to knjižnico, preberite, [kako naložiti konfiguracijo|bootstrap:]. - - -Ena povezava ------------- - -Konfiguracija ene podatkovne povezave: - -```neon -database: - # DSN, edini obvezni ključ - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Ustvari storitvi `Nette\Database\Connection` in `Nette\Database\Explorer`, ki si jih običajno posredujemo z [autowiringom |dependency-injection:autowiring], ali pa s sklicem na [njihovo ime |#Storitve DI]. - -Druge nastavitve: - -```neon -database: - # prikazati ploščo podatkovne baze v Tracy Bar? - debugger: ... # (bool) privzeto je true - - # prikazati EXPLAIN poizvedb v Tracy Bar? - explain: ... # (bool) privzeto je true - - # dovoliti autowiring za to povezavo? - autowired: ... # (bool) privzeto je true pri prvi povezavi - - # konvencije tabel: discovered, static ali ime razreda - conventions: discovered # (string) privzeto je 'discovered' - - options: - # povezati se s podatkovno bazo šele, ko je potrebno? - lazy: ... # (bool) privzeto je false - - # PHP razred gonilnika podatkovne baze - driverClass: # (string) - - # samo MySQL: nastavi sql_mode - sqlmode: # (string) - - # samo MySQL: nastavi SET NAMES - charset: # (string) privzeto je 'utf8mb4' - - # samo MySQL: pretvori TINYINT(1) v bool - convertBoolean: # (bool) privzeto je false - - # vrača stolpce z datumom kot nespremenljive objekte (od različice 3.2.1) - newDateTime: # (bool) privzeto je false - - # samo Oracle in SQLite: format za shranjevanje datuma - formatDateTime: # (string) privzeto je 'U' -``` - -V ključu `options` lahko navajate druge možnosti, ki jih najdete v [dokumentaciji gonilnikov PDO |https://www.php.net/manual/en/pdo.drivers.php], kot na primer: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Več povezav ------------ - -V konfiguraciji lahko definiramo tudi več podatkovnih povezav z razdelitvijo na poimenovane sekcije: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring je vklopljen samo pri storitvah iz prve sekcije. To lahko spremenite s pomočjo `autowired: false` ali `autowired: true`. - - -Storitve DI ------------ - -Te storitve se dodajo v DI vsebnik, kjer `###` predstavlja ime povezave: - -| Ime | Tip | Opis -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | povezava s podatkovno bazo -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Če definiramo samo eno povezavo, bosta imeni storitev `database.default.connection` in `database.default.explorer`. Če definiramo več povezav kot v zgornjem primeru, bodo imena ustrezala sekcijam, tj. `database.main.connection`, `database.main.explorer` in naprej `database.another.connection` ter `database.another.explorer`. - -Ne-autowirane storitve posredujemo eksplicitno s sklicem na njihovo ime: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/sl/exceptions.texy b/database/sl/exceptions.texy deleted file mode 100644 index 07d3bd8871..0000000000 --- a/database/sl/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Izjeme -****** - -Nette Database uporablja hierarhijo izjem. Osnovni razred je `Nette\Database\DriverException`, ki deduje iz `PDOException` in nudi razširjene možnosti za delo z napakami podatkovne baze: - -- Metoda `getDriverCode()` vrača kodo napake od gonilnika podatkovne baze -- Metoda `getSqlState()` vrača kodo SQLSTATE -- Metodi `getQueryString()` in `getParameters()` omogočata pridobitev prvotne poizvedbe in njenih parametrov - -Iz `DriverException` dedujejo naslednje specializirane izjeme: - -- `ConnectionException` - signalizira neuspeh povezave s podatkovnim strežnikom -- `ConstraintViolationException` - osnovni razred za kršitve podatkovnih omejitev, iz katerega dedujejo: - - `ForeignKeyConstraintViolationException` - kršitev tujega ključa - - `NotNullConstraintViolationException` - kršitev omejitve NOT NULL - - `UniqueConstraintViolationException` - kršitev edinstvenosti vrednosti - - -Primer lovljenja izjeme `UniqueConstraintViolationException`, ki nastane, ko poskušamo vstaviti uporabnika z e-pošto, ki že obstaja v podatkovni bazi (ob predpostavki, da ima stolpec email edinstven indeks). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Uporabnik s tem e-naslovom že obstaja.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Pri registraciji je prišlo do napake: ' . $e->getMessage(); -} -``` diff --git a/database/sl/explorer.texy b/database/sl/explorer.texy deleted file mode 100644 index 1be13e409f..0000000000 --- a/database/sl/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer ponuja intuitiven in učinkovit način dela s podatkovno bazo. Samodejno skrbi za relacije med tabelami in optimizacijo poizvedb, tako da se lahko osredotočite na svojo aplikacijo. Deluje takoj brez nastavljanja. Če potrebujete popoln nadzor nad SQL poizvedbami, lahko uporabite [SQL pristop |SQL way]. - -- Delo s podatki je naravno in enostavno razumljivo -- Generira optimizirane SQL poizvedbe, ki nalagajo samo potrebne podatke -- Omogoča enostaven dostop do povezanih podatkov brez potrebe po pisanju JOIN poizvedb -- Deluje takoj brez kakršnekoli konfiguracije ali generiranja entitet - -</div> - - -Z Explorerjem začnete s klicem metode `table()` objekta [api:Nette\Database\Explorer] (podrobnosti o povezavi najdete v poglavju [Povezava in konfiguracija |guide#Povezava in konfiguracija]): - -```php -$books = $explorer->table('book'); // 'book' je ime tabele -``` - -Metoda vrača objekt [Selection |api:Nette\Database\Table\Selection], ki predstavlja SQL poizvedbo. Na ta objekt lahko navezujemo nadaljnje metode za filtriranje in razvrščanje rezultatov. Poizvedba se sestavi in zažene šele v trenutku, ko začnemo zahtevati podatke. Na primer s prehajanjem skozi zanko `foreach`. Vsaka vrstica je predstavljena z objektom [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // izpis stolpca 'title' - echo $book->author_id; // izpis stolpca 'author_id' -} -``` - -Explorer bistveno olajša delo s [povezavami med tabelami |#Povezave med tabelami]. Naslednji primer prikazuje, kako enostavno lahko izpišemo podatke iz povezanih tabel (knjige in njihovi avtorji). Opazite, da nam ni treba pisati nobenih JOIN poizvedb, Nette jih ustvari za nas: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Knjiga: ' . $book->title; - echo 'Avtor: ' . $book->author->name; // ustvari JOIN na tabelo 'author' -} -``` - -Nette Database Explorer optimizira poizvedbe, da so čim bolj učinkovite. Zgornji primer izvede samo dve SELECT poizvedbi, ne glede na to, ali obdelujemo 10 ali 10.000 knjig. - -Poleg tega Explorer spremlja, kateri stolpci se v kodi uporabljajo, in nalaga iz podatkovne baze samo te, s čimer prihrani dodatno zmogljivost. To obnašanje je popolnoma samodejno in prilagodljivo. Če kasneje prilagodite kodo in začnete uporabljati druge stolpce, Explorer samodejno prilagodi poizvedbe. Ničesar vam ni treba nastavljati, niti razmišljati o tem, katere stolpce boste potrebovali - prepustite to Nette. - - -Filtriranje in razvrščanje -========================== - -Razred `Selection` ponuja metode za filtriranje in razvrščanje izbora podatkov. - -.[language-php] -| `where($condition, ...$params)` | Doda pogoj WHERE. Več pogojev je povezanih z operatorjem AND -| `whereOr(array $conditions)` | Doda skupino pogojev WHERE, povezanih z operatorjem OR -| `wherePrimary($value)` | Doda pogoj WHERE po primarnem ključu -| `order($columns, ...$params)` | Nastavi razvrščanje ORDER BY -| `select($columns, ...$params)` | Določi stolpce, ki naj se naložijo -| `limit($limit, $offset = null)` | Omeji število vrstic (LIMIT) in po želji nastavi OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Nastavi stranskanje -| `group($columns, ...$params)` | Združi vrstice (GROUP BY) -| `having($condition, ...$params)` | Doda pogoj HAVING za filtriranje združenih vrstic - -Metode lahko verižimo (t.i. [fluent interface |nette:introduction-to-object-oriented-programming#Tekoči vmesniki]): `$table->where(...)->order(...)->limit(...)`. - -V teh metodah lahko uporabljate tudi posebno notacijo za dostop do [podatkov iz povezanih tabel |#Poizvedovanje prek povezanih tabel]. - - -Ubežanje znakov in identifikatorji ----------------------------------- - -Metode samodejno ubežijo parametre in navajajo identifikatorje (imena tabel in stolpcev), s čimer preprečujejo SQL injection. Za pravilno delovanje je treba upoštevati nekaj pravil: - -- Ključne besede, imena funkcij, procedur ipd. pišite **z velikimi črkami**. -- Imena stolpcev in tabel pišite **z malimi črkami**. -- Nize vedno vstavljajte prek **parametrov**. - -```php -where('name = ' . $name); // KRITIČNA RANLJIVOST: SQL injection -where('name LIKE "%search%"'); // NAPAKA: otežuje samodejno navajanje -where('name LIKE ?', '%search%'); // PRAVILNO: vrednost vstavljena prek parametra - -where('name like ?', $name); // NAPAKA: generira: `name` `like` ? -where('name LIKE ?', $name); // PRAVILNO: generira: `name` LIKE ? -where('LOWER(name) = ?', $value);// PRAVILNO: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Filtrira rezultate s pomočjo pogojev WHERE. Njena močna stran je inteligentno delo z različnimi tipi vrednosti in samodejna izbira SQL operatorjev. - -Osnovna uporaba: - -```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' -``` - -Zahvaljujoč samodejnemu zaznavanju ustreznih operatorjev nam ni treba reševati različnih posebnih primerov. Nette jih reši za nas: - -```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) -// lahko se uporabi tudi nadomestni vprašaj brez operatorja: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Metoda pravilno obdela tudi negativne pogoje in prazno polje: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- ničesar ne najde -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- najde vse -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- najde vse -// $table->where('NOT id ?', $ids); Pozor - ta sintaksa ni podprta -``` - -Kot parameter lahko posredujemo tudi rezultat iz druge tabele - ustvari se podpoizvedba: - -```php -// 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')); -``` - -Pogoje lahko posredujemo tudi kot polje, katerega elementi se združijo s pomočjo AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -V polju lahko uporabimo pare ključ => vrednost in Nette spet samodejno izbere pravilne operatorje: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -V polju lahko kombiniramo SQL izraze z nadomestnimi vprašaji in več parametri. To je primerno za kompleksne pogoje z natančno določenimi operatorji: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // dva parametra posredujemo kot polje -]); -``` - -Večkratni klic `where()` pogoje samodejno združuje s pomočjo AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Podobno kot `where()` dodaja pogoje, vendar s to razliko, da jih združuje s pomočjo OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Tudi tukaj lahko uporabimo kompleksnejše izraze: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Doda pogoj za primarni ključ tabele: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Če ima tabela sestavljen primarni ključ (npr. `foo_id`, `bar_id`), ga posredujemo kot polje: - -```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(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Določa vrstni red, v katerem bodo vrnjene vrstice. Lahko razvrščamo po enem ali več stolpcih, v padajočem ali naraščajočem vrstnem redu, ali po lastnem izrazu: - -```php -$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(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Določa stolpce, ki naj se vrnejo iz podatkovne baze. V privzetem stanju Nette Database Explorer vrača samo tiste stolpce, ki se dejansko uporabijo v kodi. Metodo `select()` tako uporabljamo v primerih, ko moramo vrniti specifične izraze: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Aliasi, definirani s pomočjo `AS`, so nato dostopni kot lastnosti objekta ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // dostop do aliasa -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Omejuje število vrnjenih vrstic (LIMIT) in po želji omogoča nastavitev odmika (offset): - -```php -$table->limit(10); // LIMIT 10 (vrne prvih 10 vrstic) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Za stranskanje je primernejša uporaba metode `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Olajša stranskanje rezultatov. Sprejme številko strani (šteto od 1) in število postavk na stran. Po želji lahko posredujemo referenco na spremenljivko, v katero se shrani skupno število strani: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Skupaj strani: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Združuje vrstice po navedenih stolpcih (GROUP BY). Uporablja se običajno v povezavi z agregatnimi funkcijami: - -```php -// Prešteje število izdelkov v vsaki kategoriji -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Nastavi pogoj za filtriranje združenih vrstic (HAVING). Lahko se uporablja v povezavi z metodo `group()` in agregatnimi funkcijami: - -```php -// Najde kategorije, ki imajo več kot 100 izdelkov -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Branje podatkov -=============== - -Za branje podatkov iz podatkovne baze imamo na voljo več uporabnih metod: - -.[language-php] -| `foreach ($table as $key => $row)` | Iterira čez vse vrstice, `$key` je vrednost primarnega ključa, `$row` je objekt ActiveRow -| `$row = $table->get($key)` | Vrne eno vrstico po primarnem ključu -| `$row = $table->fetch()` | Vrne trenutno vrstico in premakne kazalec na naslednjo -| `$array = $table->fetchPairs()` | Ustvari asociativno polje iz rezultatov -| `$array = $table->fetchAll()` | Vrne vse vrstice kot polje -| `count($table)` | Vrne število vrstic v objektu Selection - -Objekt [ActiveRow |api:Nette\Database\Table\ActiveRow] je namenjen samo za branje. To pomeni, da ni mogoče spreminjati vrednosti njegovih lastnosti. Ta omejitev zagotavlja doslednost podatkov in preprečuje nepričakovane stranske učinke. Podatki se nalagajo iz podatkovne baze in vsaka sprememba bi morala biti izvedena eksplicitno in nadzorovano. - - -`foreach` - iteracija čez vse vrstice -------------------------------------- - -Najlažji način za izvedbo poizvedbe in pridobitev vrstic je iteriranje v zanki `foreach`. Samodejno zažene SQL poizvedbo. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key je vrednost primarnega ključa, $book je ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Izvede SQL poizvedbo in vrne vrstico po primarnem ključu, ali `null`, če ne obstaja. - -```php -$book = $explorer->table('book')->get(123); // vrne ActiveRow z ID 123 ali null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Vrne vrstico in premakne notranji kazalec na naslednjo. Če ni več vrstic, vrne `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Vrne rezultate kot asociativno polje. Prvi argument določa ime stolpca, ki se uporabi kot ključ v polju, drugi argument določa ime stolpca, ki se uporabi kot vrednost: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Če navedemo samo prvi parameter, bo vrednost celotna vrstica, torej objekt `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -V primeru podvojenih ključev se uporabi vrednost iz zadnje vrstice. Pri uporabi `null` kot ključa bo polje indeksirano numerično od nič (takrat do kolizij ne pride): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativno lahko kot parameter navedete povratni klic (callback), ki bo za vsako vrstico vračal bodisi samo vrednost, bodisi par ključ-vrednost. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Prva knjiga (Jan Novak)', ...] - -// Callback lahko vrne tudi polje s parom ključ & vrednost: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Prva knjiga' => 'Jan Novak', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Vrne vse vrstice kot asociativno polje objektov `ActiveRow`, kjer so ključi vrednosti primarnih ključev. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Metoda `count()` brez parametra vrne število vrstic v objektu `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // alternativa -``` - -Pozor, `count()` s parametrom izvaja agregatno funkcijo COUNT v podatkovni bazi, glej spodaj. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Pretvori objekt `ActiveRow` v asociativno polje, kjer so ključi imena stolpcev in vrednosti ustrezni podatki. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray bo ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Agregacija -========== - -Razred `Selection` ponuja metode za enostavno izvajanje agregatnih funkcij (COUNT, SUM, MIN, MAX, AVG itd.). - -.[language-php] -| `count($expr)` | Prešteje število vrstic -| `min($expr)` | Vrne minimalno vrednost v stolpcu -| `max($expr)` | Vrne maksimalno vrednost v stolpcu -| `sum($expr)` | Vrne vsoto vrednosti v stolpcu -| `aggregation($function)` | Omogoča izvedbo poljubne agregatne funkcije. Npr. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Izvede SQL poizvedbo s funkcijo COUNT in vrne rezultat. Metoda se uporablja za ugotavljanje, koliko vrstic ustreza določenemu pogoju: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Pozor, [#count()] brez parametra samo vrača število vrstic v objektu `Selection`. - - -min(string $expr) a max(string $expr) .[method] ------------------------------------------------ - -Metodi `min()` in `max()` vračata minimalno in maksimalno vrednost v določenem stolpcu ali izrazu: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Vrne vsoto vrednosti v določenem stolpcu ali izrazu: - -```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] ----------------------------------------------------------------------- - -Omogoča izvedbo poljubne agregatne funkcije. - -```php -// povprečna cena izdelkov v kategoriji -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// združi oznake izdelka v en niz -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Če moramo agregirati rezultate, ki so že sami po sebi nastali iz neke agregatne funkcije in združevanja (npr. `SUM(vrednost)` čez združene vrstice), kot drugi argument navedemo agregatno funkcijo, ki naj se uporabi na teh vmesnih rezultatih: - -```php -// Izračuna skupno ceno izdelkov na zalogi za posamezne kategorije in nato sešteje te cene skupaj. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -V tem primeru najprej izračunamo skupno ceno izdelkov v vsaki kategoriji (`SUM(price * stock) AS category_total`) in združimo rezultate po `category_id`. Nato uporabimo `aggregation('SUM(category_total)', 'SUM')` za seštevanje teh vmesnih vsot `category_total`. Drugi argument `'SUM'` pove, da naj se na vmesne rezultate uporabi funkcija SUM. - - -Insert, Update & Delete -======================= - -Nette Database Explorer poenostavlja vstavljanje, posodabljanje in brisanje podatkov. Vse navedene metode v primeru napake vržejo izjemo `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Vstavi nove zapise v tabelo. - -**Vstavljanje enega zapisa:** - -Nov zapis posredujemo kot asociativno polje ali iterable objekt (na primer ArrayHash, ki se uporablja v [obrazcih |forms:]), kjer ključi ustrezajo imenom stolpcev v tabeli. - -Če ima tabela definiran primarni ključ, metoda vrne objekt `ActiveRow`, ki se ponovno naloži iz podatkovne baze, da se upoštevajo morebitne spremembe, izvedene na ravni podatkovne baze (sprožilci, privzete vrednosti stolpcev, izračuni auto-increment stolpcev). S tem je zagotovljena doslednost podatkov in objekt vedno vsebuje aktualne podatke iz podatkovne baze. Če enoličnega primarnega ključa nima, vrne posredovane podatke v obliki polja. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row je instanca ActiveRow in vsebuje celotne podatke vstavljene vrstice, -// vključno s samodejno generiranim ID-jem in morebitnimi spremembami, izvedenimi s sprožilci (triggerji) -echo $row->id; // Izpiše ID novo vstavljenega uporabnika -echo $row->created_at; // Izpiše čas ustvarjanja, če je nastavljen s sprožilcem -``` - -**Vstavljanje več zapisov hkrati:** - -Metoda `insert()` omogoča vstavljanje več zapisov z eno samo SQL poizvedbo. V tem primeru vrne število vstavljenih vrstic. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows bo 2 -``` - -Kot parameter lahko posredujemo tudi objekt `Selection` z izborom podatkov. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Vstavljanje posebnih vrednosti:** - -Kot vrednosti lahko posredujemo tudi datoteke, objekte DateTime ali SQL literale: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // pretvori v format podatkovne baze - 'avatar' => fopen('image.jpg', 'rb'), // vstavi binarno vsebino datoteke - 'uuid' => $explorer::literal('UUID()'), // pokliče funkcijo UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Posodobi vrstice v tabeli po navedenem filtru. Vrne število dejansko spremenjenih vrstic. - -Spremenjene stolpce posredujemo kot asociativno polje ali iterable objekt (na primer ArrayHash, ki se uporablja v [obrazcih |forms:]), kjer ključi ustrezajo imenom stolpcev v tabeli: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Za spremembo številskih vrednosti lahko uporabimo operatorja `+=` in `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // poveča vrednost stolpca 'points' za 1 - 'coins-=' => 1, // zmanjša vrednost stolpca 'coins' za 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Briše vrstice iz tabele po navedenem filtru. Vrne število izbrisanih vrstic. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -Pri klicu `update()` in `delete()` ne pozabite s pomočjo `where()` določiti vrstic, ki naj se uredijo/izbrišejo. Če `where()` ne uporabite, se operacija izvede na celotni tabeli! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Posodobi podatke v podatkovni vrstici, ki jo predstavlja objekt `ActiveRow`. Kot parameter sprejme iterable s podatki, ki naj se posodobijo (ključi so imena stolpcev). Za spremembo številskih vrednosti lahko uporabimo operatorja `+=` in `-=`: - -Po izvedbi posodobitve se `ActiveRow` samodejno ponovno naloži iz podatkovne baze, da se upoštevajo morebitne spremembe, izvedene na ravni podatkovne baze (npr. sprožilci). Metoda vrne true samo, če je prišlo do dejanske spremembe podatkov. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // povečamo število prikazov -]); -echo $article->views; // Izpiše trenutno število prikazov -``` - -Ta metoda posodobi samo eno določeno vrstico v podatkovni bazi. Za množično posodabljanje več vrstic uporabite metodo [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Izbriše vrstico iz podatkovne baze, ki jo predstavlja objekt `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Izbriše knjigo z ID 1 -``` - -Ta metoda briše samo eno določeno vrstico v podatkovni bazi. Za množično brisanje več vrstic uporabite metodo [#Selection::delete()]. - - -Povezave med tabelami -===================== - -V relacijskih podatkovnih bazah so podatki razdeljeni na več tabel in medsebojno povezani s pomočjo tujih ključev. Nette Database Explorer prinaša revolucionaren način dela s temi povezavami - brez pisanja JOIN poizvedb in potrebe po kakršnikoli konfiguraciji ali generiranju. - -Za ilustracijo dela s povezavami bomo uporabili primer podatkovne baze knjig ([najdete ga na GitHubu |https://github.com/nette-examples/books]). V podatkovni bazi imamo tabele: - -- `author` - pisatelji in prevajalci (stolpci `id`, `name`, `web`, `born`) -- `book` - knjige (stolpci `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - oznake (stolpci `id`, `name`) -- `book_tag` - povezovalna tabela med knjigami in oznakami (stolpci `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Struktura podatkovne baze .<> - -V našem primeru podatkovne baze knjig najdemo več tipov odnosov (čeprav je model poenostavljen v primerjavi z realnostjo): - -- Ena-proti-mnogo 1:N – vsaka knjiga **ima enega** avtorja, avtor lahko napiše **več** knjig -- Nič-proti-mnogo 0:N – knjiga **lahko ima** prevajalca, prevajalec lahko prevede **več** knjig -- Nič-proti-ena 0:1 – knjiga **lahko ima** naslednji del -- Mnogo-proti-mnogo M:N – knjiga **lahko ima več** oznak in oznaka je lahko dodeljena **več** knjigam - -V teh odnosih vedno obstaja nadrejena in podrejena tabela. Na primer v odnosu med avtorjem in knjigo je tabela `author` nadrejena in `book` podrejena - lahko si predstavljamo, da knjiga vedno »pripada« nekemu avtorju. To se odraža tudi v strukturi podatkovne baze: podrejena tabela `book` vsebuje tuji ključ `author_id`, ki se nanaša na nadrejeno tabelo `author`. - -Če moramo izpisati knjige vključno z imeni njihovih avtorjev, imamo dve možnosti. Ali podatke pridobimo z eno samo SQL poizvedbo s pomočjo JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Ali pa podatke naložimo v dveh korakih - najprej knjige in nato njihove avtorje - in jih nato v PHP sestavimo: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- id-ji avtorjev pridobljenih knjig -``` - -Drugi pristop je dejansko učinkovitejši, čeprav se to morda zdi presenetljivo. Podatki so naloženi samo enkrat in jih je mogoče bolje izkoristiti v predpomnilniku. Prav na ta način deluje Nette Database Explorer - vse rešuje pod površjem in vam ponuja eleganten API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author je zapis iz tabele 'author' - echo 'translated by: ' . $book->translator?->name; -} -``` - - -Dostop do nadrejene tabele --------------------------- - -Dostop do nadrejene tabele je neposreden. Gre za odnose kot *knjiga ima avtorja* ali *knjiga lahko ima prevajalca*. Povezan zapis pridobimo prek lastnosti objekta ActiveRow - njeno ime ustreza imenu stolpca s tujim ključem brez `_id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // najde avtorja po stolpcu author_id -echo $book->translator?->name; // najde prevajalca po stolpcu translator_id -``` - -Ko dostopamo do lastnosti `$book->author`, Explorer v tabeli `book` išče stolpec, katerega ime vsebuje niz `author` (torej `author_id`). Po vrednosti v tem stolpcu naloži ustrezen zapis iz tabele `author` in ga vrne kot `ActiveRow`. Podobno deluje tudi `$book->translator`, ki uporabi stolpec `translator_id`. Ker stolpec `translator_id` lahko vsebuje `null`, v kodi uporabimo operator `?->`. - -Alternativno pot ponuja metoda `ref()`, ki sprejme dva argumenta, ime ciljne tabele in ime povezovalnega stolpca, ter vrne instanco `ActiveRow` ali `null`: - -```php -echo $book->ref('author', 'author_id')->name; // povezava na avtorja -echo $book->ref('author', 'translator_id')->name; // povezava na prevajalca -``` - -Metoda `ref()` je koristna, če ni mogoče uporabiti dostopa prek lastnosti, ker tabela vsebuje stolpec z istim imenom (tj. `author`). V ostalih primerih je priporočljivo uporabljati dostop prek lastnosti, ki je bolj berljiv. - -Explorer samodejno optimizira podatkovne poizvedbe. Ko prehajamo skozi knjige v zanki in dostopamo do njihovih povezanih zapisov (avtorjev, prevajalcev), Explorer ne generira poizvedbe za vsako knjigo posebej. Namesto tega izvede samo en SELECT za vsak tip povezave, s čimer bistveno zmanjša obremenitev podatkovne baze. Na primer: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Ta koda pokliče samo te tri bliskovite poizvedbe v podatkovno bazo: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id iz stolpca author_id izbranih knjig -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id iz stolpca translator_id izbranih knjig -``` - -.[note] -Logika iskanja povezovalnega stolpca je določena z implementacijo [Conventions |api:Nette\Database\Conventions]. Priporočamo uporabo [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], ki analizira tuje ključe in omogoča enostavno delo z obstoječimi relacijami med tabelami. - - -Dostop do podrejene tabele --------------------------- - -Dostop do podrejene tabele deluje v obratni smeri. Zdaj se sprašujemo *katere knjige je napisal ta avtor* ali *prevedel ta prevajalec*. Za ta tip poizvedbe uporabljamo metodo `related()`, ki vrne `Selection` s povezanimi zapisi. Poglejmo si primer: - -```php -$author = $explorer->table('author')->get(1); - -// Izpiše vse knjige avtorja -foreach ($author->related('book.author_id') as $book) { - echo "Napisal: $book->title"; -} - -// Izpiše vse knjige, ki jih je avtor prevedel -foreach ($author->related('book.translator_id') as $book) { - echo "Prevedel: $book->title"; -} -``` - -Metoda `related()` sprejme opis povezave kot en argument s pikčasto notacijo ali kot dva ločena argumenta: - -```php -$author->related('book.translator_id'); // en argument -$author->related('book', 'translator_id'); // dva argumenta -``` - -Explorer zna samodejno zaznati pravilen povezovalni stolpec na podlagi imena nadrejene tabele. V tem primeru se povezuje prek stolpca `book.author_id`, ker je ime izvorne tabele `author`: - -```php -$author->related('book'); // uporabi book.author_id -``` - -Če bi obstajalo več možnih povezav, Explorer vrže izjemo [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Metodo `related()` lahko seveda uporabimo tudi pri prehajanju skozi več zapisov v zanki in Explorer tudi v tem primeru samodejno optimizira poizvedbe: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' napisal:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Ta koda generira samo dve bliskoviti SQL poizvedbi: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id izbranih avtorjev -``` - - -Povezava Mnogo-proti-mnogo --------------------------- - -Za povezavo mnogo-proti-mnogo (M:N) je potrebna obstoj povezovalne tabele (v našem primeru `book_tag`), ki vsebuje dva stolpca s tujima ključema (`book_id`, `tag_id`). Vsak od teh stolpcev se nanaša na primarni ključ ene od povezanih tabel. Za pridobitev povezanih podatkov najprej pridobimo zapise iz povezovalne tabele s pomočjo `related('book_tag')` in nato nadaljujemo k ciljnim podatkom: - -```php -$book = $explorer->table('book')->get(1); -// izpiše imena oznak, dodeljenih knjigi -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // izpiše ime oznake prek povezovalne tabele -} - -$tag = $explorer->table('tag')->get(1); -// ali obratno: izpiše imena knjig, označenih s to oznako -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // izpiše ime knjige -} -``` - -Explorer spet optimizira SQL poizvedbe v učinkovito obliko: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id izbranih knjig -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id oznak, najdenih v book_tag -``` - - -Poizvedovanje prek povezanih tabel ----------------------------------- - -V metodah `where()`, `select()`, `order()` in `group()` lahko uporabljamo posebne notacije za dostop do stolpcev iz drugih tabel. Explorer samodejno ustvari potrebne JOINe. - -**Pikčasta notacija** (`nadrejena_tabela.stolpec`) se uporablja za odnos 1:N z vidika podrejene tabele: - -```php -$books = $explorer->table('book'); - -// Najde knjige, katerih avtor ima ime, ki se začne na 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Razvrsti knjige po imenu avtorja padajoče -$books->order('author.name DESC'); - -// Izpiše naslov knjige in ime avtorja -$books->select('book.title, author.name'); -``` - -**Dvopična notacija** (`:podrejena_tabela.stolpec`) se uporablja za odnos 1:N z vidika nadrejene tabele: - -```php -$authors = $explorer->table('author'); - -// Najde avtorje, ki so napisali knjigo s 'PHP' v naslovu -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Prešteje število knjig za vsakega avtorja -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -V zgornjem primeru z dvopično notacijo (`:book.title`) ni določen stolpec s tujim ključem. Explorer samodejno zazna pravilen stolpec na podlagi imena nadrejene tabele. V tem primeru se povezuje prek stolpca `book.author_id`, ker je ime izvorne tabele `author`. Če bi obstajalo več možnih povezav, Explorer vrže izjemo [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Povezovalni stolpec lahko eksplicitno navedemo v oklepaju: - -```php -// Najde avtorje, ki so prevedli knjigo s 'PHP' v naslovu -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Notacije lahko verižimo za dostop prek več tabel: - -```php -// Najde avtorje knjig, označenih z oznako 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Razširitev pogojev za JOIN --------------------------- - -Metoda `joinWhere()` razširja pogoje, ki se navajajo pri povezovanju tabel v SQL za ključno besedo `ON`. - -Recimo, da želimo najti knjige, prevedene s strani določenega prevajalca: - -```php -// Najde knjige, prevedene s strani prevajalca z imenom '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 pogoju `joinWhere()` lahko uporabljamo enake konstrukcije kot v metodi `where()` - operatorje, nadomestne vprašaje, polja vrednosti ali SQL izraze. - -Za kompleksnejše poizvedbe z več JOINi lahko definiramo aliase tabel: - -```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) -``` - -Opazite, da medtem ko metoda `where()` dodaja pogoje v klavzulo `WHERE`, metoda `joinWhere()` razširja pogoje v klavzuli `ON` pri povezovanju tabel. diff --git a/database/sl/guide.texy b/database/sl/guide.texy deleted file mode 100644 index 4ef0012d4d..0000000000 --- a/database/sl/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database je zmogljiva in elegantna podatkovna plast za PHP s poudarkom na preprostosti in pametnih funkcijah. Ponuja dva načina dela z bazo podatkov - [Explorer] za hiter razvoj aplikacij ali [SQL pristop |SQL way] za neposredno delo s poizvedbami. - -<div class="grid gap-3"> -<div> - - -[SQL pristop |SQL way] -====================== -- Varne parametrizirane poizvedbe -- Natančen nadzor nad obliko SQL poizvedb -- Ko pišete kompleksne poizvedbe z naprednimi funkcijami -- Optimizirate zmogljivost s specifičnimi SQL funkcijami - -</div> - -<div> - - -[Explorer] -========== -- Hitro razvijate brez pisanja SQL -- Intuitivno delo z relacijami med tabelami -- Cenili boste samodejno optimizacijo poizvedb -- Primerno za hitro in udobno delo z bazo podatkov - -</div> - -</div> - - -Namestitev -========== - -Knjižnico prenesete in namestite z orodjem [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Podprte podatkovne baze -======================= - -Nette Database podpira naslednje podatkovne baze: - -|* Podatkovni strežnik |* Ime DSN |* Podpora v Explorerju -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | DA -| PostgreSQL (>= 9.0) | pgsql | DA -| Sqlite 3 (>= 3.8) | sqlite | DA -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | DA -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Dva pristopa k bazi podatkov -============================ - -Nette Database vam daje izbiro: lahko pišete SQL poizvedbe neposredno (SQL pristop) ali pa jih pustite samodejno generirati (Explorer). Poglejmo, kako oba pristopa rešujeta enake naloge: - -[SQL pristop|sql way] - SQL poizvedbe - -```php -// vstavljanje zapisa -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// pridobivanje zapisov: avtorji knjig -$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 -'); - -// izpis (ni optimalno, generira N dodatnih poizvedb) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Avtor $author->name je napisal $author->books_count knjig:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Pristop Explorer|explorer] - samodejno generiranje SQL - -```php -// vstavljanje zapisa -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// pridobivanje zapisov: avtorji knjig -$authors = $database->table('authors') - ->where('active', 1); - -// izpis (samodejno generira samo 2 optimizirani poizvedbi) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Avtor $author->name je napisal {$books->count()} knjig:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Pristop Explorer samodejno generira in optimizira SQL poizvedbe. V navedenem primeru SQL pristop generira N+1 poizvedb (eno za avtorje in nato eno za knjige vsakega avtorja), medtem ko Explorer samodejno optimizira poizvedbe in izvede samo dve - eno za avtorje in eno za vse njihove knjige. - -Oba pristopa lahko v aplikaciji poljubno kombinirate po potrebi. - - -Povezava in konfiguracija -========================= - -Za povezavo z bazo podatkov zadostuje ustvariti instanco razreda [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Parameter `$dsn` (data source name) je enak, [kot ga uporablja PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], npr. `host=127.0.0.1;dbname=test`. V primeru napake vrže izjemo `Nette\Database\ConnectionException`. - -Vendar pa spretnejši način ponuja [konfiguracija aplikacije |configuration], kamor zadostuje dodati sekcijo `database` in ustvarijo se potrebni objekti ter tudi podatkovna plošča v [Tracy |tracy:] baru. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Nato objekt povezave [pridobimo kot storitev iz DI vsebnika |dependency-injection:passing-dependencies], npr.: - -```php -class Model -{ - public function __construct( - // ali Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Več informacij o [konfiguraciji baze podatkov|configuration]. - - -Ročno ustvarjanje Explorerja ----------------------------- - -Če ne uporabljate Nette DI vsebnika, lahko instanco `Nette\Database\Explorer` ustvarite ročno: - -```php -// povezava z bazo podatkov -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// shramba za predpomnilnik, implementira Nette\Caching\Storage, npr.: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// skrbi za refleksijo strukture baze podatkov -$structure = new Nette\Database\Structure($connection, $storage); -// definira pravila za preslikavo imen tabel, stolpcev in tujih ključev -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Upravljanje povezave -==================== - -Pri ustvarjanju objekta `Connection` se samodejno vzpostavi povezava. Če želite povezavo odložiti, uporabite lazy način - tega vklopite v [konfiguraciji|configuration] z nastavitvijo `lazy` ali takole: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Za upravljanje povezave uporabite metode `connect()`, `disconnect()` in `reconnect()`. -- `connect()` ustvari povezavo, če še ne obstaja, pri čemer lahko vrže izjemo `Nette\Database\ConnectionException`. -- `disconnect()` prekine trenutno povezavo z bazo podatkov. -- `reconnect()` izvede prekinitev in nato ponovno vzpostavitev povezave z bazo podatkov. Ta metoda lahko prav tako vrže izjemo `Nette\Database\ConnectionException`. - -Poleg tega lahko spremljate dogodke, povezane s povezavo, z uporabo dogodka `onConnect`, ki je polje povratnih klicev (callback), ki se pokličejo po vzpostavitvi povezave z bazo podatkov. - -```php -// izvede se po povezavi z bazo podatkov -$database->onConnect[] = function($database) { - echo "Povezano z bazo podatkov"; -}; -``` - - -Tracy Debug Bar -=============== - -Če uporabljate [Tracy |tracy:], se samodejno aktivira plošča Database v Debug baru, ki prikazuje vse izvedene poizvedbe, njihove parametre, čas izvedbe in mesto v kodi, kjer so bile poklicane. - -[* db-panel.webp *] diff --git a/database/sl/mapping.texy b/database/sl/mapping.texy deleted file mode 100644 index 132f47367e..0000000000 --- a/database/sl/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Pretvorba tipov -*************** - -.[perex] -Nette Database samodejno pretvarja vrednosti, vrnjene iz baze podatkov, v ustrezne PHP tipe. - - -Datum in čas ------------- - -Časovni podatki se pretvorijo v objekte `Nette\Utils\DateTime`. Če želite, da se časovni podatki pretvorijo v nespremenljive objekte `Nette\Database\DateTime`, nastavite v [konfiguraciji|configuration] možnost `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 primeru MySQL pretvarja podatkovni tip `TIME` v objekte `DateInterval`. - - -Booleove vrednosti ------------------- - -Booleove vrednosti se samodejno pretvorijo v `true` ali `false`. Pri MySQL se pretvarja `TINYINT(1)`, če nastavimo v [konfiguraciji|configuration] `convertBoolean`. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Številske vrednosti -------------------- - -Številske vrednosti se pretvorijo v `int` ali `float` glede na tip stolpca v bazi podatkov: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Lastna normalizacija --------------------- - -Z metodo `setRowNormalizer(?callable $normalizer)` lahko nastavite lastno funkcijo za transformacijo vrstic iz baze podatkov. To je koristno na primer za samodejno pretvorbo podatkovnih tipov. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // tukaj poteka pretvorba tipov - return $row; -}); -``` diff --git a/database/sl/reflection.texy b/database/sl/reflection.texy deleted file mode 100644 index a5d4cb846a..0000000000 --- a/database/sl/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Refleksija strukture -******************** - -.{data-version:3.2.1} -Nette Database ponuja orodja za introspekcijo strukture baze podatkov z uporabo razreda [api:Nette\Database\Reflection]. Ta omogoča pridobivanje informacij o tabelah, stolpcih, indeksih in tujih ključih. Refleksijo lahko uporabite za generiranje shem, ustvarjanje fleksibilnih aplikacij, ki delajo z bazo podatkov, ali splošnih orodij za baze podatkov. - -Objekt refleksije pridobimo iz instance povezave z bazo podatkov: - -```php -$reflection = $database->getReflection(); -``` - - -Pridobivanje tabel ------------------- - -Readonly lastnost `$reflection->tables` vsebuje asociativno polje vseh tabel v bazi podatkov: - -```php -// Izpis imen vseh tabel -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Na voljo sta še dve metodi: - -```php -// Preverjanje obstoja tabele -if ($reflection->hasTable('users')) { - echo "Tabela users obstaja"; -} - -// Vrne objekt tabele; če ne obstaja, vrže izjemo -$table = $reflection->getTable('users'); -``` - - -Informacije o tabeli --------------------- - -Tabela je predstavljena z objektom [Table|api:Nette\Database\Reflection\Table], ki ponuja naslednje readonly lastnosti: - -- `$name: string` – ime tabele -- `$view: bool` – ali gre za pogled (view) -- `$fullName: ?string` – polno ime tabele, vključno s shemo (če obstaja) -- `$columns: array<string, Column>` – asociativno polje stolpcev tabele -- `$indexes: Index[]` – polje indeksov tabele -- `$primaryKey: ?Index` – primarni ključ tabele ali null -- `$foreignKeys: ForeignKey[]` – polje tujih ključev tabele - - -Stolpci -------- - -Lastnost `columns` tabele ponuja asociativno polje stolpcev, kjer je ključ ime stolpca in vrednost instanca [Column|api:Nette\Database\Reflection\Column] s temi lastnostmi: - -- `$name: string` – ime stolpca -- `$table: ?Table` – referenca na tabelo stolpca -- `$nativeType: string` – nativni podatkovni tip baze podatkov -- `$size: ?int` – velikost/dolžina tipa -- `$nullable: bool` – ali lahko stolpec vsebuje NULL -- `$default: mixed` – privzeta vrednost stolpca -- `$autoIncrement: bool` – ali je stolpec auto-increment -- `$primary: bool` – ali je del primarnega ključa -- `$vendor: array` – dodatni metapodatki, specifični za dani sistem baze podatkov - -```php -foreach ($table->columns as $name => $column) { - echo "Stolpec: $name\n"; - echo "Tip: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Da' : 'Ne') . "\n"; -} -``` - - -Indeksi -------- - -Lastnost `indexes` tabele ponuja polje indeksov, kjer je vsak indeks instanca [Index|api:Nette\Database\Reflection\Index] s temi lastnostmi: - -- `$columns: Column[]` – polje stolpcev, ki tvorijo indeks -- `$unique: bool` – ali je indeks unikaten -- `$primary: bool` – ali gre za primarni ključ -- `$name: ?string` – ime indeksa - -Primarni ključ tabele lahko pridobimo z lastnostjo `primaryKey`, ki vrne bodisi objekt `Index` ali `null` v primeru, da tabela nima primarnega ključa. - -```php -// Izpis indeksov -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Indeks" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Stolpci: $columns\n"; - echo " Unique: " . ($index->unique ? 'Da' : 'Ne') . "\n"; -} - -// Izpis primarnega ključa -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Primarni ključ: $columns\n"; -} -``` - - -Tuji ključi ------------ - -Lastnost `foreignKeys` tabele ponuja polje tujih ključev, kjer je vsak tuji ključ instanca [ForeignKey|api:Nette\Database\Reflection\ForeignKey] s temi lastnostmi: - -- `$foreignTable: Table` – referencirana tabela -- `$localColumns: Column[]` – polje lokalnih stolpcev -- `$foreignColumns: Column[]` – polje referenciranih stolpcev -- `$name: ?string` – ime tujega ključa - -```php -// Izpis tujih ključev -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/sl/security.texy b/database/sl/security.texy deleted file mode 100644 index 9dda20fdc4..0000000000 --- a/database/sl/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Varnostna tveganja -****************** - -<div class=perex> - -Baza podatkov pogosto vsebuje občutljive podatke in omogoča izvajanje nevarnih operacij. Za varno delo z Nette Database je ključno: - -- Razumeti razliko med varnim in nevarnim API-jem -- Uporabljati parametrizirane poizvedbe -- Pravilno validirati vhodne podatke - -</div> - - -Kaj je SQL Injection? -===================== - -SQL injection je najresnejše varnostno tveganje pri delu z bazo podatkov. Nastane, ko neobdelan vnos uporabnika postane del SQL poizvedbe. Napadalec lahko vstavi lastne SQL ukaze in s tem: -- Pridobi nepooblaščen dostop do podatkov -- Spremeni ali izbriše podatke v bazi podatkov -- Obide avtentikacijo - -```php -// ❌ NEVARNA KODA - ranljiva za SQL injection -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Napadalec lahko vnese na primer vrednost: ' OR '1'='1 -// Rezultatna poizvedba bo potem: SELECT * FROM users WHERE name = '' OR '1'='1' -// Kar vrne vse uporabnike -``` - -Enako velja tudi za Database Explorer: - -```php -// ❌ NEVARNA KODA - ranljiva za SQL injection -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Parametrizirane poizvedbe -========================= - -Osnovna obramba pred SQL injection so parametrizirane poizvedbe. Nette Database ponuja več načinov njihove uporabe. - -Najenostavnejši način je uporaba **nadomestnih vprašajev**: - -```php -// ✅ Varna parametrizirana poizvedba -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Varen pogoj v Explorerju -$table->where('name = ?', $name); -``` - -To velja za vse druge metode v [Database Explorer|explorer], ki omogočajo vstavljanje izrazov z nadomestnimi vprašaji in parametri. - -Za ukaze INSERT, UPDATE ali klavzulo WHERE lahko vrednosti posredujemo v polju: - -```php -// ✅ Varen INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Varen INSERT v Explorerju -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Validacija vrednosti parametrov -=============================== - -Parametrizirane poizvedbe so osnovni gradnik varnega dela z bazo podatkov. Vendar pa morajo vrednosti, ki jih vstavljamo vanje, preiti več ravni preverjanj: - - -Tipska kontrola ---------------- - -**Najpomembnejše je zagotoviti pravilen podatkovni tip parametrov** - to je nujen pogoj za varno uporabo Nette Database. Baza podatkov predpostavlja, da imajo vsi vhodni podatki pravilen podatkovni tip, ki ustreza danemu stolpcu. - -Na primer, če bi bil `$name` v prejšnjih primerih nepričakovano polje namesto niza, bi Nette Database poskusila vstaviti vse njegove elemente v SQL poizvedbo, kar bi povzročilo napako. Zato **nikoli ne uporabljajte** nevalidiranih podatkov iz `$_GET`, `$_POST` ali `$_COOKIE` neposredno v poizvedbah baze podatkov. - - -Formatna kontrola ------------------ - -Na drugi ravni preverjamo format podatkov - na primer, ali so nizi v UTF-8 kodiranju in njihova dolžina ustreza definiciji stolpca, ali pa so številske vrednosti v dovoljenem obsegu za dani podatkovni tip stolpca. - -Pri tej ravni validacije se lahko delno zanesemo tudi na samo bazo podatkov - mnoge baze podatkov zavrnejo nevalidne podatke. Vendar pa se obnašanje lahko razlikuje, nekatere lahko dolge nize tiho skrajšajo ali števila izven obsega obrežejo. - - -Domenska kontrola ------------------ - -Tretjo raven predstavljajo logične kontrole, specifične za vašo aplikacijo. Na primer preverjanje, da vrednosti iz izbirnih polj ustrezajo ponujenim možnostim, da so števila v pričakovanem obsegu (npr. starost 0-150 let) ali da medsebojne odvisnosti med vrednostmi imajo smisel. - - -Priporočeni načini validacije ------------------------------ - -- Uporabljajte [Nette Obrazce|forms:], ki samodejno zagotovijo pravilno validacijo vseh vnosov -- Uporabljajte [Presenterje|application:] in navedite pri parametrih v `action*()` in `render*()` metodah podatkovne tipe -- Ali implementirajte lastno validacijsko plast z uporabo standardnih PHP orodij, kot je `filter_var()` - - -Varno delo s stolpci -==================== - -V prejšnjem odseku smo si ogledali, kako pravilno validirati vrednosti parametrov. Pri uporabi polj v SQL poizvedbah pa moramo enako pozornost posvetiti tudi njihovim ključem. - -```php -// ❌ NEVARNA KODA - niso obdelani ključi v polju -$database->query('INSERT INTO users', $_POST); -``` - -Pri ukazih INSERT in UPDATE je to temeljna varnostna napaka - napadalec lahko v bazo podatkov vstavi ali spremeni kateri koli stolpec. Lahko bi si na primer nastavil `is_admin = 1` ali vstavil poljubne podatke v občutljive stolpce (t.i. Mass Assignment Vulnerability). - -V pogojih WHERE je to še nevarnejše, saj lahko vsebujejo operatorje: - -```php -// ❌ NEVARNA KODA - niso obdelani ključi v polju -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// izvede poizvedbo WHERE (`salary` > 100000) -``` - -Napadalec lahko ta pristop izkoristi za sistematično ugotavljanje plač zaposlenih. Začne na primer s poizvedbo o plačah nad 100.000, nato pod 50.000 in s postopnim zoževanjem obsega lahko odkrije približne plače vseh zaposlenih. Ta tip napada se imenuje SQL enumeration. - -Metodi `where()` in `whereOr()` sta še [veliko bolj fleksibilni |explorer#where] in podpirata v ključih in vrednostih SQL izraze, vključno z operatorji in funkcijami. To daje napadalcu možnost izvedbe SQL injection: - -```php -// ❌ NEVARNA KODA - napadalec lahko vstavi lasten SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// izvede poizvedbo WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Ta napad zaključi prvotni pogoj z `0)`, priključi lasten `SELECT` z `UNION`, da pridobi občutljive podatke iz tabele `users`, in zaključi sintaktično pravilno poizvedbo z `WHERE (1)`. - - -Beli seznam stolpcev --------------------- - -Za varno delo z imeni stolpcev potrebujemo mehanizem, ki zagotavlja, da lahko uporabnik dela samo z dovoljenimi stolpci in ne more dodati lastnih. Lahko bi poskusili zaznati in blokirati nevarna imena stolpcev (črni seznam), vendar je ta pristop nezanesljiv - napadalec lahko vedno najde nov način, kako zapisati nevarno ime stolpca, ki ga nismo predvideli. - -Zato je veliko varneje obrniti logiko in definirati ekspliciten seznam dovoljenih stolpcev (beli seznam): - -```php -// Stolpci, ki jih lahko uporabnik ureja -$allowedColumns = ['name', 'email', 'active']; - -// Odstranimo vse nedovoljene stolpce iz vnosa -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); // array_flip for performance - -// ✅ Zdaj lahko varno uporabimo v poizvedbah, kot na primer: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Dinamični identifikatorji -========================= - -Za dinamična imena tabel in stolpcev uporabite nadomestni znak `?name`. Ta zagotavlja pravilno ubežanje identifikatorjev glede na sintakso dane baze podatkov (npr. z uporabo povratnih narekovajev v MySQL): - -```php -// ✅ Varna uporaba zaupanja vrednih identifikatorjev -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Rezultat v MySQL: SELECT `name` FROM `users` -``` - -Pomembno: simbol `?name` uporabljajte samo za zaupanja vredne vrednosti, definirane v kodi aplikacije. Za vrednosti od uporabnika ponovno uporabite [beli seznam |#Beli seznam stolpcev]. Sicer se izpostavljate varnostnim tveganjem: - -```php -// ❌ NEVARNO - nikoli ne uporabljajte vnosa od uporabnika -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/sl/sql-way.texy b/database/sl/sql-way.texy deleted file mode 100644 index e8857452ba..0000000000 --- a/database/sl/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL pristop -*********** - -.[perex] -Nette Database ponuja dve poti: lahko pišete SQL poizvedbe sami (SQL pristop) ali pa jih pustite samodejno generirati (glej [Explorer |explorer]). SQL pristop vam daje popoln nadzor nad poizvedbami in hkrati zagotavlja njihovo varno sestavljanje. - -.[note] -Podrobnosti o povezavi in konfiguraciji podatkovne baze najdete v poglavju [Povezava in konfiguracija |guide#Povezava in konfiguracija]. - - -Osnovno poizvedovanje -===================== - -Za poizvedovanje v podatkovni bazi služi metoda `query()`. Ta vrne objekt [ResultSet |api:Nette\Database\ResultSet], ki predstavlja rezultat poizvedbe. V primeru napake metoda [vrže izjemo|exceptions]. Rezultat poizvedbe lahko prehajamo z zanko `foreach` ali uporabimo katero od [pomožnih funkcij |#Pridobivanje podatkov]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Za varno vstavljanje vrednosti v SQL poizvedbe uporabljamo parametrizirane poizvedbe. Nette Database jih naredi maksimalno preproste - zadostuje, da za SQL poizvedbo dodamo vejico in vrednost: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Pri več parametrih imate dve možnosti zapisa. Lahko SQL poizvedbo "prepletate" s parametri: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Ali pa najprej napišete celotno SQL poizvedbo in nato priključite vse parametre: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Zaščita pred SQL injection -========================== - -Zakaj je pomembno uporabljati parametrizirane poizvedbe? Ker vas ščitijo pred napadom, imenovanim SQL injection, pri katerem bi napadalec lahko podtaknil lastne SQL ukaze in s tem pridobil ali poškodoval podatke v podatkovni bazi. - -.[warning] -**Nikoli ne vstavljajte spremenljivk neposredno v SQL poizvedbo!** Vedno uporabljajte parametrizirane poizvedbe, ki vas ščitijo pred SQL injection. - -```php -// ❌ NEVARNA KODA - ranljiva za SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Varna parametrizirana poizvedba -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Seznanite se z [možnimi varnostnimi tveganji |security]. - - -Tehnike poizvedovanja -===================== - - -Pogoji WHERE ------------- - -Pogoje WHERE lahko zapišete kot asociativno polje, kjer so ključi imena stolpcev in vrednosti podatki za primerjavo. Nette Database samodejno izbere najprimernejši SQL operator glede na tip vrednosti. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -V ključu lahko tudi eksplicitno določite operator za primerjavo: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // uporabi operator > - 'name LIKE' => '%John%', // uporabi operator LIKE - 'email NOT LIKE' => '%example.com%', // uporabi operator NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette samodejno obravnava posebne primere, kot so `null` vrednosti ali polja. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // uporabi operator = - 'category_id' => [1, 2, 3], // uporabi IN - 'description' => null, // uporabi IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Za negativne pogoje uporabite operator `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // uporabi operator <> - 'category_id NOT' => [1, 2, 3], // uporabi NOT IN - 'description NOT' => null, // uporabi IS NOT NULL - 'id' => [], // izpusti se -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Za združevanje pogojev se uporablja operator `AND`. To lahko spremenite z uporabo [nadomestnega znaka ?or |#Namigi za sestavljanje SQL]. - - -Pravila ORDER BY ----------------- - -Razvrščanje `ORDER BY` lahko zapišemo z uporabo polja. V ključih navedemo stolpce, vrednost pa bo boolean, ki določa, ali razvrščati naraščajoče: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // naraščajoče - 'name' => false, // padajoče -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Vstavljanje podatkov (INSERT) ------------------------------ - -Za vstavljanje zapisov se uporablja SQL ukaz `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Metoda `getInsertId()` vrne ID zadnje vstavljene vrstice. Pri nekaterih podatkovnih bazah (npr. PostgreSQL) je treba kot parameter določiti ime sekvence, iz katere naj se ID generira z uporabo `$database->getInsertId($sequenceId)`. - -Kot parametre lahko posredujemo tudi [#Posebne vrednosti] kot so datoteke, objekti DateTime ali naštevni tipi. - -Vstavljanje več zapisov hkrati: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Večkratni INSERT je veliko hitrejši, ker se izvede ena sama poizvedba podatkovne baze namesto mnogih posameznih. - -**Varnostno opozorilo:** Nikoli ne uporabljajte kot `$values` nevalidiranih podatkov. Seznanite se z [možnimi tveganji |security#Varno delo s stolpci]. - - -Posodabljanje podatkov (UPDATE) -------------------------------- - -Za posodabljanje zapisov se uporablja SQL ukaz `UPDATE`. - -```php -// Posodobitev enega zapisa -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Število prizadetih vrstic vrne `$result->getRowCount()`. - -Za UPDATE lahko uporabimo operatorja `+=` in `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // inkrementacija login_count -], 1); -``` - -Primer vstavljanja ali urejanja zapisa, če že obstaja. Uporabimo tehniko `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 -``` - -Opazite, da Nette Database prepozna, v kakšnem kontekstu SQL ukaza vstavljamo parameter s poljem in glede na to iz njega sestavi SQL kodo. Tako je iz prvega polja sestavil `(id, name, year) VALUES (123, 'Jim', 1978)`, medtem ko je drugega pretvoril v obliko `name = 'Jim', year = 1978`. Podrobneje se temu posvečamo v delu [#Namigi za sestavljanje SQL]. - - -Brisanje podatkov (DELETE) --------------------------- - -Za brisanje zapisov se uporablja SQL ukaz `DELETE`. Primer s pridobivanjem števila izbrisanih vrstic: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Namigi za sestavljanje SQL --------------------------- - -Namig je poseben nadomestni znak v SQL poizvedbi, ki pove, kako naj se vrednost parametra prepiše v SQL izraz: - -| Namig | Opis | Samodejno se uporabi -|-----------|-------------------------------------------------|----------------------------- -| `?name` | uporabi za vstavljanje imena tabele ali stolpca | - -| `?values` | generira `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | generira prirejanje `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | združi pogoje v polju z operatorjem `AND` | `WHERE ?`, `HAVING ?` -| `?or` | združi pogoje v polju z operatorjem `OR` | - -| `?order` | generira klavzulo `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Za dinamično vstavljanje imen tabel in stolpcev v poizvedbo služi nadomestni znak `?name`. Nette Database poskrbi za pravilno obdelavo identifikatorjev glede na konvencije dane podatkovne baze (npr. zapiranje v povratne narekovaje 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) -``` - -**Opozorilo:** simbol `?name` uporabljajte samo za imena tabel in stolpcev iz validiranih vnosov, sicer se izpostavljate [varnostnemu tveganju |security#Dinamični identifikatorji]. - -Drugih namigov običajno ni treba navajati, saj Nette pri sestavljanju SQL poizvedbe uporablja pametno samodejno zaznavanje (glej tretji stolpec tabele). Lahko pa ga uporabite na primer v situaciji, ko želite združiti pogoje z `OR` namesto `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' -``` - - -Posebne vrednosti ------------------ - -Poleg običajnih skalarnih tipov (string, int, bool) lahko kot parametre posredujete tudi posebne vrednosti: - -- datoteke: `fopen('image.gif', 'r')` vstavi binarno vsebino datoteke -- datum in čas: objekti `DateTime` se pretvorijo v format podatkovne baze -- naštevni tipi: instance `enum` se pretvorijo v njihovo vrednost -- SQL literali: ustvarjeni z `Connection::literal('NOW()')` se vstavijo neposredno v poizvedbo - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -Pri podatkovnih bazah, ki nimajo nativne podpore za podatkovni tip `datetime` (kot SQLite in Oracle), se `DateTime` pretvori v vrednost, določeno v [konfiguraciji podatkovne baze|configuration] z vnosom `formatDateTime` (privzeta vrednost je `U` - unix timestamp). - - -SQL literali ------------- - -V nekaterih primerih morate kot vrednost navesti neposredno SQL kodo, ki pa se ne sme razumeti kot niz in ubežati. Za to služijo objekti razreda `Nette\Database\SqlLiteral`. Ustvarja jih 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()) -``` - -Ali alternativno: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL literali lahko vsebujejo parametre: - -```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) -``` - -Zaradi česar lahko ustvarjamo zanimive kombinacije: - -```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') -``` - - -Pridobivanje podatkov -===================== - - -Bližnjice za SELECT poizvedbe ------------------------------ - -Za poenostavitev nalaganja podatkov `Connection` ponuja več bližnjic, ki kombinirajo klic `query()` z naslednjim `fetch*()`. Te metode sprejemajo enake parametre kot `query()`, torej SQL poizvedbo in neobvezne parametre. Popoln opis metod `fetch*()` najdete [spodaj |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Izvede poizvedbo in vrne prvo vrstico kot objekt `Row` -| `fetchAll($sql, ...$params): array` | Izvede poizvedbo in vrne vse vrstice kot polje objektov `Row` -| `fetchPairs($sql, ...$params): array` | Izvede poizvedbo in vrne asociativno polje, kjer prvi stolpec predstavlja ključ in drugi vrednost -| `fetchField($sql, ...$params): mixed` | Izvede poizvedbo in vrne vrednost prvega polja iz prve vrstice -| `fetchList($sql, ...$params): ?array` | Izvede poizvedbo in vrne prvo vrstico kot indeksirano polje - -Primer: - -```php -// fetchField() - vrne vrednost prve celice -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - iteracija čez vrstice ---------------------------------- - -Po izvedbi poizvedbe se vrne objekt [ResultSet|api:Nette\Database\ResultSet], ki omogoča prehajanje rezultatov na več načinov. Najlažji način za izvedbo poizvedbe in pridobitev vrstic je iteracija v zanki `foreach`. Ta način je pomnilniško najbolj varčen, saj vrača podatke postopoma in jih ne shranjuje vseh hkrati v pomnilnik. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` je mogoče iterirati samo enkrat. Če potrebujete iterirati večkrat, morate najprej naložiti podatke v polje, na primer z metodo `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Vrne vrstico kot objekt `Row`. Če ni več vrstic, vrne `null`. Premakne notranji kazalec na naslednjo vrstico. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // naloži prvo vrstico -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Vrne vse preostale vrstice iz `ResultSet` kot polje objektov `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // naloži vse vrstice -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Vrne rezultate kot asociativno polje. Prvi argument določa ime stolpca, ki se uporabi kot ključ v polju, drugi argument določa ime stolpca, ki se uporabi kot vrednost: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Če navedemo samo prvi parameter, bo vrednost celotna vrstica, torej objekt `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -V primeru podvojenih ključev se uporabi vrednost iz zadnje vrstice. Pri uporabi `null` kot ključa bo polje indeksirano numerično od nič (potem do kolizij ne pride): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Alternativno lahko kot parameter navedete povratni klic (callback), ki bo za vsako vrstico vrnil bodisi samo vrednost ali par ključ-vrednost. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback lahko vrne tudi polje s parom ključ & vrednost: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Vrne vrednost prvega polja iz trenutne vrstice. Če ni več vrstic, vrne `null`. Premakne notranji kazalec na naslednjo vrstico. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // naloži ime iz prve vrstice -``` - - -fetchList(): ?array .[method] ------------------------------ - -Vrne vrstico kot indeksirano polje. Če ni več vrstic, vrne `null`. Premakne notranji kazalec na naslednjo vrstico. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Vrne število prizadetih vrstic zadnje poizvedbe `UPDATE` ali `DELETE`. Za `SELECT` je to število vrnjenih vrstic, vendar to morda ni znano - v takem primeru metoda vrne `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Vrne število stolpcev v `ResultSet`. - - -Informacije o poizvedbah -======================== - -Za namene razhroščevanja lahko pridobimo informacije o zadnji izvedeni poizvedbi: - -```php -echo $database->getLastQueryString(); // izpiše SQL poizvedbo - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // izpiše SQL poizvedbo -echo $result->getTime(); // izpiše čas izvedbe v sekundah -``` - -Za prikaz rezultata kot HTML tabele lahko uporabimo: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -ResultSet ponuja informacije o tipih stolpcev: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column je tipa $type->type"; // npr. 'id je tipa int' -} -``` - - -Dnevniško beleženje poizvedb ----------------------------- - -Lahko implementiramo lastno dnevniško beleženje poizvedb. Dogodek `onQuery` je polje povratnih klicev (callback), ki se pokličejo po vsaki izvedeni poizvedbi: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Poizvedba: ' . $result->getQueryString()); - $logger->info('Čas: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Velik nabor rezultatov: ' . $result->getRowCount() . ' vrstic'); - } -}; -``` diff --git a/database/sl/transactions.texy b/database/sl/transactions.texy deleted file mode 100644 index 4183ae8e2c..0000000000 --- a/database/sl/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Transakcije -*********** - -.[perex] -Transakcije zagotavljajo, da se bodisi izvedejo vse operacije znotraj transakcije ali pa se ne izvede nobena. Uporabne so za zagotavljanje skladnosti podatkov pri bolj zapletenih operacijah. - -Najenostavnejši način uporabe transakcij je videti takole: - -```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; -} -``` - -Veliko bolj elegantno lahko isto zapišete z metodo `transaction()`. Kot parameter sprejme povratni klic, ki ga izvede v transakciji. Če povratni klic poteka brez izjeme, se transakcija samodejno potrdi. Če pride do izjeme, se transakcija prekliče (rollback) in izjema se širi naprej. - -```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()` lahko tudi vrača vrednosti: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // vrne število posodobljenih vrstic -}); -``` diff --git a/database/uk/@home.texy b/database/uk/@home.texy deleted file mode 100644 index 9cd3b93223..0000000000 --- a/database/uk/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ - - -Підтримувані бази даних -======================= - -Nette підтримує наступні бази даних: - -|* Сервер бази даних |* Ім'я DSN |* Підтримка в Core |* Підтримка в Explorer -| MySQL (>= 5.1) | mysql | ТАК | ТАК -| PostgreSQL (>= 9.0) | pgsql | ТАК | ТАК -| Sqlite 3 (>= 3.8) | sqlite | ТАК | ТАК -| Oracle | oci | ТАК | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ТАК | ТАК -| MS SQL (PDO_DBLIB) | mssql | ТАК | - -| ODBC | odbc | ТАК | - - - - - -{{maintitle: Nette Database - awesome database layer for PHP}} -{{description: Nette Database суттєво спрощує отримання даних з бази даних без необхідності писати SQL-запити. Вона виконує ефективні запити та не передає зайвих даних.}} diff --git a/database/uk/@left-menu.texy b/database/uk/@left-menu.texy deleted file mode 100644 index f8a09afcd7..0000000000 --- a/database/uk/@left-menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -Nette Database -************** -- [Вступ |guide] -- [SQL-доступ |sql way] -- [Explorer |Explorer] -- [Транзакції |transactions] -- [Винятки |exceptions] -- [Рефлексія |reflection] -- [Мапування |mapping] -- [Конфігурація |configuration] -- [Ризики безпеки |security] -- [Оновлення |en:upgrading] diff --git a/database/uk/@meta.texy b/database/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/database/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/database/uk/configuration.texy b/database/uk/configuration.texy deleted file mode 100644 index 60db622a55..0000000000 --- a/database/uk/configuration.texy +++ /dev/null @@ -1,110 +0,0 @@ -Конфігурація бази даних -*********************** - -.[perex] -Огляд параметрів конфігурації для Nette Database. - -Якщо ви не використовуєте весь фреймворк, а лише цю бібліотеку, прочитайте, [як завантажити конфігурацію|bootstrap:]. - - -Одне з'єднання --------------- - -Конфігурація одного з'єднання з базою даних: - -```neon -database: - # DSN, єдиний обов'язковий ключ - dsn: "sqlite:%appDir%/Model/demo.db" - user: ... - password: ... -``` - -Створює сервіси `Nette\Database\Connection` та `Nette\Database\Explorer`, які зазвичай передаються за допомогою [autowiring |dependency-injection:autowiring], або посиланням на [їхню назву |#Сервіси DI]. - -Інші налаштування: - -```neon -database: - # відображати панель бази даних у Tracy Bar? - debugger: ... # (bool) за замовчуванням true - - # відображати EXPLAIN запитів у Tracy Bar? - explain: ... # (bool) за замовчуванням true - - # дозволити autowiring для цього з'єднання? - autowired: ... # (bool) за замовчуванням true для першого з'єднання - - # конвенції таблиць: discovered, static або ім'я класу - conventions: discovered # (string) за замовчуванням 'discovered' - - options: - # підключатися до бази даних лише коли це необхідно? - lazy: ... # (bool) за замовчуванням false - - # PHP клас драйвера бази даних - driverClass: # (string) - - # лише MySQL: встановлює sql_mode - sqlmode: # (string) - - # лише MySQL: встановлює SET NAMES - charset: # (string) за замовчуванням 'utf8mb4' - - # лише MySQL: перетворює TINYINT(1) на bool - convertBoolean: # (bool) за замовчуванням false - - # повертає стовпці з датою як immutable об'єкти (з версії 3.2.1) - newDateTime: # (bool) за замовчуванням false - - # лише Oracle та SQLite: формат для зберігання дати - formatDateTime: # (string) за замовчуванням 'U' -``` - -У ключі `options` можна вказувати інші параметри, які ви знайдете в [документації драйверів PDO |https://www.php.net/manual/en/pdo.drivers.php], наприклад: - -```neon -database: - options: - PDO::MYSQL_ATTR_COMPRESS: true -``` - - -Кілька з'єднань ---------------- - -У конфігурації ми можемо визначити і кілька з'єднань з базою даних, розділивши їх на іменовані секції: - -```neon -database: - main: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password - - another: - dsn: 'sqlite::memory:' -``` - -Autowiring увімкнено лише для сервісів з першої секції. Це можна змінити за допомогою `autowired: false` або `autowired: true`. - - -Сервіси DI ----------- - -Ці сервіси додаються до DI-контейнера, де `###` представляє назву з'єднання: - -| Назва | Тип | Опис -|---------------------------------------------------------- -| `database.###.connection` | [api:Nette\Database\Connection] | з'єднання з базою даних -| `database.###.explorer` | [api:Nette\Database\Explorer] | [Database Explorer |explorer] - - -Якщо ми визначаємо лише одне з'єднання, назви сервісів будуть `database.default.connection` та `database.default.explorer`. Якщо ми визначаємо кілька з'єднань, як у прикладі вище, назви будуть відповідати секціям, тобто `database.main.connection`, `database.main.explorer`, а також `database.another.connection` та `database.another.explorer`. - -Сервіси без autowiring передаються явно за посиланням на їхню назву: - -```neon -services: - - UserFacade(@database.another.connection) -``` diff --git a/database/uk/exceptions.texy b/database/uk/exceptions.texy deleted file mode 100644 index 3d1642573e..0000000000 --- a/database/uk/exceptions.texy +++ /dev/null @@ -1,34 +0,0 @@ -Винятки -******* - -Nette Database використовує ієрархію винятків. Базовим класом є `Nette\Database\DriverException`, який успадковує від `PDOException` і надає розширені можливості для роботи з помилками бази даних: - -- Метод `getDriverCode()` повертає код помилки від драйвера бази даних -- Метод `getSqlState()` повертає код SQLSTATE -- Методи `getQueryString()` та `getParameters()` дозволяють отримати початковий запит та його параметри - -Від `DriverException` успадковуються наступні спеціалізовані винятки: - -- `ConnectionException` - сигналізує про збій підключення до сервера бази даних -- `ConstraintViolationException` - базовий клас для порушення обмежень бази даних, від якого успадковуються: - - `ForeignKeyConstraintViolationException` - порушення зовнішнього ключа - - `NotNullConstraintViolationException` - порушення обмеження NOT NULL - - `UniqueConstraintViolationException` - порушення унікальності значення - - -Приклад перехоплення винятку `UniqueConstraintViolationException`, який виникає, коли ми намагаємося вставити користувача з email, який вже існує в базі даних (за умови, що стовпець email має унікальний індекс). - -```php -try { - $database->query('INSERT INTO users', [ - 'email' => 'john@example.com', - 'name' => 'John Doe', - 'password' => $hashedPassword, - ]); -} catch (Nette\Database\UniqueConstraintViolationException $e) { - echo 'Користувач з цим email вже існує.'; - -} catch (Nette\Database\DriverException $e) { - echo 'Сталася помилка під час реєстрації: ' . $e->getMessage(); -} -``` diff --git a/database/uk/explorer.texy b/database/uk/explorer.texy deleted file mode 100644 index 164032ccda..0000000000 --- a/database/uk/explorer.texy +++ /dev/null @@ -1,912 +0,0 @@ -Database Explorer -***************** - -<div class=perex> - -Explorer пропонує інтуїтивно зрозумілий та ефективний спосіб роботи з базою даних. Він автоматично дбає про зв'язки між таблицями та оптимізацію запитів, тож ви можете зосередитися на своєму додатку. Працює одразу без налаштувань. Якщо вам потрібен повний контроль над SQL-запитами, ви можете скористатися [SQL-підходом |SQL way]. - -- Робота з даними є природною та легкою для розуміння -- Генерує оптимізовані SQL-запити, які завантажують лише необхідні дані -- Дозволяє легко отримати доступ до пов'язаних даних без необхідності писати JOIN-запити -- Працює одразу без будь-якої конфігурації чи генерації сутностей - -</div> - - -З Explorer ви починаєте, викликаючи метод `table()` об'єкта [api:Nette\Database\Explorer] (деталі підключення див. у розділі [Підключення та конфігурація |guide#Підключення та конфігурація]): - -```php -$books = $explorer->table('book'); // 'book' - назва таблиці -``` - -Метод повертає об'єкт [Selection |api:Nette\Database\Table\Selection], який представляє SQL-запит. До цього об'єкта можна додавати інші методи для фільтрації та сортування результатів. Запит складається та виконується лише тоді, коли ми починаємо запитувати дані. Наприклад, проходячи циклом `foreach`. Кожен рядок представлений об'єктом [ActiveRow |api:Nette\Database\Table\ActiveRow]: - -```php -foreach ($books as $book) { - echo $book->title; // виведення стовпця 'title' - echo $book->author_id; // виведення стовпця 'author_id' -} -``` - -Explorer суттєво спрощує роботу зі [зв'язками між таблицями |#Зв язки між таблицями]. Наступний приклад показує, як легко ми можемо вивести дані з пов'язаних таблиць (книги та їхні автори). Зверніть увагу, що нам не потрібно писати жодних JOIN-запитів, Nette створить їх за нас: - -```php -$books = $explorer->table('book'); - -foreach ($books as $book) { - echo 'Книга: ' . $book->title; - echo 'Автор: ' . $book->author->name; // створить JOIN на таблицю 'author' -} -``` - -Nette Database Explorer оптимізує запити, щоб вони були максимально ефективними. Вищезгаданий приклад виконає лише два SELECT-запити, незалежно від того, чи обробляємо ми 10 чи 10 000 книг. - -Крім того, Explorer відстежує, які стовпці використовуються в коді, і завантажує з бази даних лише їх, тим самим заощаджуючи додаткову продуктивність. Ця поведінка повністю автоматична та адаптивна. Якщо ви пізніше зміните код і почнете використовувати інші стовпці, Explorer автоматично змінить запити. Вам не потрібно нічого налаштовувати або думати про те, які стовпці вам знадобляться - залиште це Nette. - - -Фільтрація та сортування -======================== - -Клас `Selection` надає методи для фільтрації та сортування вибірки даних. - -.[language-php] -| `where($condition, ...$params)` | Додає умову WHERE. Кілька умов об'єднуються оператором AND -| `whereOr(array $conditions)` | Додає групу умов WHERE, об'єднаних оператором OR -| `wherePrimary($value)` | Додає умову WHERE за первинним ключем -| `order($columns, ...$params)` | Встановлює сортування ORDER BY -| `select($columns, ...$params)` | Вказує стовпці, які потрібно завантажити -| `limit($limit, $offset = null)` | Обмежує кількість рядків (LIMIT) та опціонально встановлює OFFSET -| `page($page, $itemsPerPage, &$total = null)` | Встановлює пагінацію -| `group($columns, ...$params)` | Групує рядки (GROUP BY) -| `having($condition, ...$params)` | Додає умову HAVING для фільтрації згрупованих рядків - -Методи можна ланцюжком (так званий [fluent interface |nette:introduction-to-object-oriented-programming#Fluent Interfaces]): `$table->where(...)->order(...)->limit(...)`. - -У цих методах ви також можете використовувати спеціальну нотацію для доступу до [даних з пов'язаних таблиць |#Запити через пов язані таблиці]. - - -Екранування та ідентифікатори ------------------------------ - -Методи автоматично екранують параметри та беруть у лапки ідентифікатори (назви таблиць та стовпців), тим самим запобігаючи SQL injection. Для правильної роботи необхідно дотримуватися кількох правил: - -- Ключові слова, назви функцій, процедур тощо пишіть **великими літерами**. -- Назви стовпців та таблиць пишіть **малими літерами**. -- Рядки завжди підставляйте через **параметри**. - -```php -where('name = ' . $name); // КРИТИЧНА ВРАЗЛИВІСТЬ: SQL injection -where('name LIKE "%search%"'); // ПОГАНО: ускладнює автоматичне взяття в лапки -where('name LIKE ?', '%search%'); // ПРАВИЛЬНО: значення підставлене через параметр - -where('name like ?', $name); // ПОГАНО: згенерує: `name` `like` ? -where('name LIKE ?', $name); // ПРАВИЛЬНО: згенерує: `name` LIKE ? -where('LOWER(name) = ?', $value);// ПРАВИЛЬНО: LOWER(`name`) = ? -``` - - -where(string|array $condition, ...$parameters): static .[method] ----------------------------------------------------------------- - -Фільтрує результати за допомогою умов WHERE. Її сильною стороною є інтелектуальна робота з різними типами значень та автоматичний вибір SQL-операторів. - -Базове використання: - -```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' -``` - -Завдяки автоматичному визначенню відповідних операторів нам не потрібно розбиратися з різними спеціальними випадками. Nette вирішить їх за нас: - -```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) -// можна використовувати і знак питання без оператора: -$table->where('id ?', 1); // WHERE `id` = 1 -``` - -Метод правильно обробляє також заперечні умови та порожні масиви: - -```php -$table->where('id', []); // WHERE `id` IS NULL AND FALSE -- нічого не знайде -$table->where('id NOT', []); // WHERE `id` IS NULL OR TRUE -- знайде все -$table->where('NOT (id ?)', []); // WHERE NOT (`id` IS NULL AND FALSE) -- знайде все -// $table->where('NOT id ?', $ids); Увага - ця синтаксична конструкція не підтримується -``` - -Як параметр ми можемо передати також результат з іншої таблиці - створиться підзапит: - -```php -// 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')); -``` - -Умови ми можемо передати також як масив, елементи якого об'єднаються за допомогою AND: - -```php -// WHERE (`price_final` < `price_original`) AND (`stock_count` > `min_stock`) -$table->where([ - 'price_final < price_original', - 'stock_count > min_stock', -]); -``` - -У масиві ми можемо використовувати пари ключ => значення, і Nette знову автоматично вибере правильні оператори: - -```php -// WHERE (`status` = 'active') AND (`id` IN (1, 2, 3)) -$table->where([ - 'status' => 'active', - 'id' => [1, 2, 3], -]); -``` - -У масиві ми можемо комбінувати SQL-вирази зі знаками питання та кількома параметрами. Це зручно для складних умов з точно визначеними операторами: - -```php -// WHERE (`age` > 18) AND (ROUND(`score`, 2) > 75.5) -$table->where([ - 'age > ?' => 18, - 'ROUND(score, ?) > ?' => [2, 75.5], // два параметри передаємо як масив -]); -``` - -Багаторазовий виклик `where()` автоматично об'єднує умови за допомогою AND. - - -whereOr(array $parameters): static .[method] --------------------------------------------- - -Подібно до `where()`, додає умови, але з тією різницею, що об'єднує їх за допомогою OR: - -```php -// WHERE (`status` = 'active') OR (`deleted` = 1) -$table->whereOr([ - 'status' => 'active', - 'deleted' => true, -]); -``` - -Тут також можна використовувати складніші вирази: - -```php -// WHERE (`price` > 1000) OR (`price_with_tax` > 1500) -$table->whereOr([ - 'price > ?' => 1000, - 'price_with_tax > ?' => 1500, -]); -``` - - -wherePrimary(mixed $key): static .[method] ------------------------------------------- - -Додає умову для первинного ключа таблиці: - -```php -// WHERE `id` = 123 -$table->wherePrimary(123); - -// WHERE `id` IN (1, 2, 3) -$table->wherePrimary([1, 2, 3]); -``` - -Якщо таблиця має складений первинний ключ (наприклад, `foo_id`, `bar_id`), передаємо його як масив: - -```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(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Визначає порядок, у якому будуть повернені рядки. Можна сортувати за одним або кількома стовпцями, у спадному чи зростаючому порядку, або за власним виразом: - -```php -$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(string $columns, ...$parameters): static .[method] ---------------------------------------------------------- - -Вказує стовпці, які потрібно повернути з бази даних. За замовчуванням Nette Database Explorer повертає лише ті стовпці, які реально використовуються в коді. Метод `select()` ми використовуємо у випадках, коли потрібно повернути специфічні вирази: - -```php -// SELECT *, DATE_FORMAT(`created_at`, "%d.%m.%Y") AS `formatted_date` -$table->select('*, DATE_FORMAT(created_at, ?) AS formatted_date', '%d.%m.%Y'); -``` - -Аліаси, визначені за допомогою `AS`, потім доступні як властивості об'єкта ActiveRow: - -```php -foreach ($table as $row) { - echo $row->formatted_date; // доступ до аліасу -} -``` - - -limit(?int $limit, ?int $offset = null): static .[method] ---------------------------------------------------------- - -Обмежує кількість повернутих рядків (LIMIT) та опціонально дозволяє встановити зсув (offset): - -```php -$table->limit(10); // LIMIT 10 (поверне перші 10 рядків) -$table->limit(10, 20); // LIMIT 10 OFFSET 20 -``` - -Для пагінації краще використовувати метод `page()`. - - -page(int $page, int $itemsPerPage, &$numOfPages = null): static .[method] -------------------------------------------------------------------------- - -Спрощує пагінацію результатів. Приймає номер сторінки (рахується з 1) та кількість елементів на сторінку. Опціонально можна передати посилання на змінну, в яку буде збережено загальну кількість сторінок: - -```php -$numOfPages = null; -$table->page(page: 3, itemsPerPage: 10, $numOfPages); -echo "Всього сторінок: $numOfPages"; -``` - - -group(string $columns, ...$parameters): static .[method] --------------------------------------------------------- - -Групує рядки за вказаними стовпцями (GROUP BY). Зазвичай використовується у поєднанні з агрегатними функціями: - -```php -// Рахує кількість продуктів у кожній категорії -$table->select('category_id, COUNT(*) AS count') - ->group('category_id'); -``` - - -having(string $having, ...$parameters): static .[method] --------------------------------------------------------- - -Встановлює умову для фільтрації згрупованих рядків (HAVING). Можна використовувати у поєднанні з методом `group()` та агрегатними функціями: - -```php -// Знаходить категорії, які мають більше 100 продуктів -$table->select('category_id, COUNT(*) AS count') - ->group('category_id') - ->having('count > ?', 100); -``` - - -Читання даних -============= - -Для читання даних з бази даних у нас є кілька корисних методів: - -.[language-php] -| `foreach ($table as $key => $row)` | Ітерує по всіх рядках, `$key` - значення первинного ключа, `$row` - об'єкт ActiveRow -| `$row = $table->get($key)` | Повертає один рядок за первинним ключем -| `$row = $table->fetch()` | Повертає поточний рядок і переміщує вказівник на наступний -| `$array = $table->fetchPairs()` | Створює асоціативний масив з результатів -| `$array = $table->fetchAll()` | Повертає всі рядки як масив -| `count($table)` | Повертає кількість рядків в об'єкті Selection - -Об'єкт [ActiveRow |api:Nette\Database\Table\ActiveRow] призначений лише для читання. Це означає, що не можна змінювати значення його властивостей. Це обмеження забезпечує консистенцію даних та запобігає неочікуваним побічним ефектам. Дані завантажуються з бази даних, і будь-яка зміна повинна бути виконана явно та контрольовано. - - -`foreach` - ітерація по всіх рядках ------------------------------------ - -Найпростіший спосіб виконати запит і отримати рядки – це ітерація в циклі `foreach`. Автоматично запускає SQL-запит. - -```php -$books = $explorer->table('book'); -foreach ($books as $key => $book) { - // $key - значення первинного ключа, $book - ActiveRow - echo "$book->title ({$book->author->name})"; -} -``` - - -get($key): ?ActiveRow .[method] -------------------------------- - -Виконує SQL-запит і повертає рядок за первинним ключем, або `null`, якщо він не існує. - -```php -$book = $explorer->table('book')->get(123); // поверне ActiveRow з ID 123 або null -if ($book) { - echo $book->title; -} -``` - - -fetch(): ?ActiveRow .[method] ------------------------------ - -Повертає рядок і переміщує внутрішній вказівник на наступний. Якщо більше немає рядків, повертає `null`. - -```php -$books = $explorer->table('book'); -while ($book = $books->fetch()) { - $this->processBook($book); -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Повертає результати як асоціативний масив. Перший аргумент визначає назву стовпця, який буде використовуватися як ключ у масиві, другий аргумент визначає назву стовпця, який буде використовуватися як значення: - -```php -$authors = $explorer->table('author')->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Якщо вказати лише перший параметр, значенням буде весь рядок, тобто об'єкт `ActiveRow`: - -```php -$authors = $explorer->table('author')->fetchPairs('id'); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - -У випадку дублювання ключів використовується значення з останнього рядка. При використанні `null` як ключа, масив буде індексований чисельно з нуля (тоді колізій не виникає): - -```php -$authors = $explorer->table('author')->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Альтернативно, ви можете вказати як параметр callback, який для кожного рядка повертатиме або саме значення, або пару ключ-значення. - -```php -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => "$row->title ({$row->author->name})"); -// ['Перша книга (Ян Новак)', ...] - -// Callback може також повертати масив з парою ключ & значення: -$titles = $explorer->table('book') - ->fetchPairs(fn($row) => [$row->title, $row->author->name]); -// ['Перша книга' => 'Ян Новак', ...] -``` - - -fetchAll(): array .[method] ---------------------------- - -Повертає всі рядки як асоціативний масив об'єктів `ActiveRow`, де ключами є значення первинних ключів. - -```php -$allBooks = $explorer->table('book')->fetchAll(); -// [1 => ActiveRow(id: 1, ...), 2 => ActiveRow(id: 2, ...), ...] -``` - - -count(): int .[method] ----------------------- - -Метод `count()` без параметра повертає кількість рядків в об'єкті `Selection`: - -```php -$table->where('category', 1); -$count = $table->count(); -$count = count($table); // альтернатива -``` - -Увага, `count()` з параметром виконує агрегатну функцію COUNT у базі даних, див. нижче. - - -ActiveRow::toArray(): array .[method] -------------------------------------- - -Перетворює об'єкт `ActiveRow` на асоціативний масив, де ключами є назви стовпців, а значеннями – відповідні дані. - -```php -$book = $explorer->table('book')->get(1); -$bookArray = $book->toArray(); -// $bookArray буде ['id' => 1, 'title' => '...', 'author_id' => ..., ...] -``` - - -Агрегація -========= - -Клас `Selection` надає методи для легкого виконання агрегатних функцій (COUNT, SUM, MIN, MAX, AVG тощо). - -.[language-php] -| `count($expr)` | Рахує кількість рядків -| `min($expr)` | Повертає мінімальне значення у стовпці -| `max($expr)` | Повертає максимальне значення у стовпці -| `sum($expr)` | Повертає суму значень у стовпці -| `aggregation($function)` | Дозволяє виконати будь-яку агрегатну функцію. Напр. `AVG()`, `GROUP_CONCAT()` - - -count(string $expr): int .[method] ----------------------------------- - -Виконує SQL-запит з функцією COUNT і повертає результат. Метод використовується для визначення, скільки рядків відповідає певній умові: - -```php -$count = $table->count('*'); // SELECT COUNT(*) FROM `table` -$count = $table->count('DISTINCT column'); // SELECT COUNT(DISTINCT `column`) FROM `table` -``` - -Увага, [#count()] без параметра лише повертає кількість рядків в об'єкті `Selection`. - - -min(string $expr) a max(string $expr) .[method] ------------------------------------------------ - -Методи `min()` та `max()` повертають мінімальне та максимальне значення у вказаному стовпці або виразі: - -```php -// SELECT MAX(`price`) FROM `products` WHERE `active` = 1 -$maxPrice = $products->where('active', true) - ->max('price'); -``` - - -sum(string $expr) .[method] ---------------------------- - -Повертає суму значень у вказаному стовпці або виразі: - -```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] ----------------------------------------------------------------------- - -Дозволяє виконати будь-яку агрегатну функцію. - -```php -// середня ціна продуктів у категорії -$avgPrice = $products->where('category_id', 1) - ->aggregation('AVG(price)'); - -// об'єднує теги продукту в один рядок -$tags = $products->where('id', 1) - ->aggregation('GROUP_CONCAT(tag.name) AS tags') - ->fetch() - ->tags; -``` - -Якщо нам потрібно агрегувати результати, які вже самі по собі виникли з якоїсь агрегатної функції та групування (наприклад, `SUM(значення)` за згрупованими рядками), як другий аргумент вкажемо агрегатну функцію, яка має бути застосована до цих проміжних результатів: - -```php -// Розраховує загальну вартість продуктів на складі для окремих категорій, а потім підсумовує ці ціни разом. -$totalPrice = $products->select('category_id, SUM(price * stock) AS category_total') - ->group('category_id') - ->aggregation('SUM(category_total)', 'SUM'); -``` - -У цьому прикладі ми спочатку розраховуємо загальну вартість продуктів у кожній категорії (`SUM(price * stock) AS category_total`) та групуємо результати за `category_id`. Потім використовуємо `aggregation('SUM(category_total)', 'SUM')` для підсумовування цих проміжних сум `category_total`. Другий аргумент `'SUM'` вказує, що до проміжних результатів має бути застосована функція SUM. - - -Insert, Update & Delete -======================= - -Nette Database Explorer спрощує вставку, оновлення та видалення даних. Усі наведені методи у випадку помилки викидають виняток `Nette\Database\DriverException`. - - -Selection::insert(iterable $data) .[method] -------------------------------------------- - -Вставляє нові записи до таблиці. - -**Вставка одного запису:** - -Новий запис передаємо як асоціативний масив або iterable об'єкт (наприклад, ArrayHash, що використовується у [формах |forms:]), де ключі відповідають назвам стовпців у таблиці. - -Якщо таблиця має визначений первинний ключ, метод повертає об'єкт `ActiveRow`, який перезавантажується з бази даних, щоб врахувати можливі зміни, внесені на рівні бази даних (тригери, значення за замовчуванням стовпців, обчислення auto-increment стовпців). Це забезпечує консистенцію даних, і об'єкт завжди містить актуальні дані з бази даних. Якщо однозначного первинного ключа немає, повертає передані дані у вигляді масиву. - -```php -$row = $explorer->table('users')->insert([ - 'name' => 'John Doe', - 'email' => 'john.doe@example.com', -]); -// $row є екземпляром ActiveRow і містить повні дані вставленого рядка, -// включно з автоматично згенерованим ID та можливими змінами, внесеними тригерами -echo $row->id; // Виведе ID новоствореного користувача -echo $row->created_at; // Виведе час створення, якщо встановлено тригером -``` - -**Вставка кількох записів одночасно:** - -Метод `insert()` дозволяє вставити кілька записів за допомогою одного SQL-запиту. У цьому випадку повертає кількість вставлених рядків. - -```php -$insertedRows = $explorer->table('users')->insert([ - [ - 'name' => 'John', - 'year' => 1994, - ], - [ - 'name' => 'Jack', - 'year' => 1995, - ], -]); -// INSERT INTO `users` (`name`, `year`) VALUES ('John', 1994), ('Jack', 1995) -// $insertedRows буде 2 -``` - -Як параметр можна також передати об'єкт `Selection` з вибіркою даних. - -```php -$newUsers = $explorer->table('potential_users') - ->where('approved', 1) - ->select('name, email'); - -$insertedRows = $explorer->table('users')->insert($newUsers); -``` - -**Вставка спеціальних значень:** - -Як значення ми можемо передавати також файли, об'єкти DateTime або SQL-літерали: - -```php -$explorer->table('users')->insert([ - 'name' => 'John', - 'created_at' => new DateTime, // перетворює на формат бази даних - 'avatar' => fopen('image.jpg', 'rb'), // вставляє бінарний вміст файлу - 'uuid' => $explorer::literal('UUID()'), // викликає функцію UUID() -]); -``` - - -Selection::update(iterable $data): int .[method] ------------------------------------------------- - -Оновлює рядки в таблиці відповідно до вказаного фільтра. Повертає кількість фактично змінених рядків. - -Змінювані стовпці передаємо як асоціативний масив або iterable об'єкт (наприклад, ArrayHash, що використовується у [формах |forms:]), де ключі відповідають назвам стовпців у таблиці: - -```php -$affected = $explorer->table('users') - ->where('id', 10) - ->update([ - 'name' => 'John Smith', - 'year' => 1994, - ]); -// UPDATE `users` SET `name` = 'John Smith', `year` = 1994 WHERE `id` = 10 -``` - -Для зміни числових значень можна використовувати оператори `+=` та `-=`: - -```php -$explorer->table('users') - ->where('id', 10) - ->update([ - 'points+=' => 1, // збільшить значення стовпця 'points' на 1 - 'coins-=' => 1, // зменшить значення стовпця 'coins' на 1 - ]); -// UPDATE `users` SET `points` = `points` + 1, `coins` = `coins` - 1 WHERE `id` = 10 -``` - - -Selection::delete(): int .[method] ----------------------------------- - -Видаляє рядки з таблиці відповідно до вказаного фільтра. Повертає кількість видалених рядків. - -```php -$count = $explorer->table('users') - ->where('id', 10) - ->delete(); -// DELETE FROM `users` WHERE `id` = 10 -``` - -.[caution] -При виклику `update()` та `delete()` не забудьте за допомогою `where()` вказати рядки, які потрібно змінити/видалити. Якщо `where()` не використовувати, операція буде виконана над усією таблицею! - - -ActiveRow::update(iterable $data): bool .[method] -------------------------------------------------- - -Оновлює дані в рядку бази даних, представленому об'єктом `ActiveRow`. Як параметр приймає iterable з даними, які потрібно оновити (ключі - назви стовпців). Для зміни числових значень можна використовувати оператори `+=` та `-=`: - -Після виконання оновлення `ActiveRow` автоматично перезавантажується з бази даних, щоб врахувати можливі зміни, внесені на рівні бази даних (наприклад, тригери). Метод повертає true лише якщо відбулася фактична зміна даних. - -```php -$article = $explorer->table('article')->get(1); -$article->update([ - 'views += 1', // збільшимо кількість переглядів -]); -echo $article->views; // Виведе поточну кількість переглядів -``` - -Цей метод оновлює лише один конкретний рядок у базі даних. Для масового оновлення кількох рядків використовуйте метод [#Selection::update()]. - - -ActiveRow::delete() .[method] ------------------------------ - -Видаляє рядок з бази даних, який представлений об'єктом `ActiveRow`. - -```php -$book = $explorer->table('book')->get(1); -$book->delete(); // Видалить книгу з ID 1 -``` - -Цей метод видаляє лише один конкретний рядок у базі даних. Для масового видалення кількох рядків використовуйте метод [#Selection::delete()]. - - -Зв'язки між таблицями -===================== - -У реляційних базах даних дані розділені на кілька таблиць і взаємопов'язані за допомогою зовнішніх ключів. Nette Database Explorer пропонує революційний спосіб роботи з цими зв'язками - без написання JOIN-запитів та необхідності щось конфігурувати чи генерувати. - -Для ілюстрації роботи зі зв'язками використаємо приклад бази даних книг ([знайдете його на GitHub |https://github.com/nette-examples/books]). У базі даних маємо таблиці: - -- `author` - письменники та перекладачі (стовпці `id`, `name`, `web`, `born`) -- `book` - книги (стовпці `id`, `author_id`, `translator_id`, `title`, `sequel_id`) -- `tag` - теги (стовпці `id`, `name`) -- `book_tag` - таблиця зв'язку між книгами та тегами (стовпці `book_id`, `tag_id`) - -[* db-schema-1-.webp *] *** Структура бази даних, що використовується в прикладах *** - -У нашому прикладі бази даних книг знайдемо кілька типів зв'язків (хоча модель спрощена порівняно з реальністю): - -- One-to-many 1:N – кожна книга **має одного** автора, автор може написати **кілька** книг -- Zero-to-many 0:N – книга **може мати** перекладача, перекладач може перекласти **кілька** книг -- Zero-to-one 0:1 – книга **може мати** наступну частину -- Many-to-many M:N – книга **може мати кілька** тегів, а тег може бути присвоєний **кільком** книгам - -У цих зв'язках завжди існує батьківська та дочірня таблиця. Наприклад, у зв'язку між автором та книгою таблиця `author` є батьківською, а `book` - дочірньою. Ми можемо уявити це так, що книга завжди "належить" якомусь автору. Це проявляється і в структурі бази даних: дочірня таблиця `book` містить зовнішній ключ `author_id`, який посилається на батьківську таблицю `author`. - -Якщо нам потрібно вивести книги разом з іменами їхніх авторів, у нас є два варіанти. Або отримати дані одним SQL-запитом за допомогою JOIN: - -```sql -SELECT book.*, author.name FROM book LEFT JOIN author ON book.author_id = author.id -``` - -Або завантажити дані у два кроки - спочатку книги, а потім їхніх авторів - і потім зібрати їх у PHP: - -```sql -SELECT * FROM book; -SELECT * FROM author WHERE id IN (1, 2, 3); -- id авторів отриманих книг -``` - -Другий підхід насправді ефективніший, хоча це може здатися дивним. Дані завантажуються лише один раз і можуть бути краще використані в кеші. Саме таким чином працює Nette Database Explorer - все вирішує під капотом і пропонує вам елегантний API: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo 'title: ' . $book->title; - echo 'written by: ' . $book->author->name; // $book->author - це запис з таблиці 'author' - echo 'translated by: ' . $book->translator?->name; -} -``` - - -Доступ до батьківської таблиці ------------------------------- - -Доступ до батьківської таблиці є прямолінійним. Йдеться про зв'язки типу *книга має автора* або *книга може мати перекладача*. Пов'язаний запис отримуємо через властивість об'єкта ActiveRow - її назва відповідає назві стовпця із зовнішнім ключем без `id`: - -```php -$book = $explorer->table('book')->get(1); -echo $book->author->name; // знайде автора за стовпцем author_id -echo $book->translator?->name; // знайде перекладача за translator_id -``` - -Коли ми звертаємося до властивості `$book->author`, Explorer у таблиці `book` шукає стовпець, назва якого містить рядок `author` (тобто `author_id`). За значенням у цьому стовпці він завантажує відповідний запис з таблиці `author` і повертає його як `ActiveRow`. Подібно працює і `$book->translator`, який використовує стовпець `translator_id`. Оскільки стовпець `translator_id` може містити `null`, ми використовуємо в коді оператор `?->`. - -Альтернативний шлях пропонує метод `ref()`, який приймає два аргументи: назву цільової таблиці та назву сполучного стовпця, і повертає екземпляр `ActiveRow` або `null`: - -```php -echo $book->ref('author', 'author_id')->name; // зв'язок з автором -echo $book->ref('author', 'translator_id')->name; // зв'язок з перекладачем -``` - -Метод `ref()` корисний, якщо не можна використати доступ через властивість, оскільки таблиця містить стовпець з такою ж назвою (тобто `author`). В інших випадках рекомендується використовувати доступ через властивість, який є більш читабельним. - -Explorer автоматично оптимізує запити до бази даних. Коли ми проходимо книги в циклі та звертаємося до їхніх пов'язаних записів (авторів, перекладачів), Explorer не генерує запит для кожної книги окремо. Замість цього він виконує лише один SELECT для кожного типу зв'язку, тим самим значно знижуючи навантаження на базу даних. Наприклад: - -```php -$books = $explorer->table('book'); -foreach ($books as $book) { - echo $book->title . ': '; - echo $book->author->name; - echo $book->translator?->name; -} -``` - -Цей код викличе лише ці три блискавичні запити до бази даних: - -```sql -SELECT * FROM `book`; -SELECT * FROM `author` WHERE (`id` IN (1, 2, 3)); -- id зі стовпця author_id вибраних книг -SELECT * FROM `author` WHERE (`id` IN (2, 3)); -- id зі стовпця translator_id вибраних книг -``` - -.[note] -Логіка пошуку сполучного стовпця визначається реалізацією [Conventions |api:Nette\Database\Conventions]. Рекомендуємо використовувати [DiscoveredConventions |api:Nette\Database\Conventions\DiscoveredConventions], які аналізують зовнішні ключі та дозволяють легко працювати з існуючими зв'язками між таблицями. - - -Доступ до дочірньої таблиці ---------------------------- - -Доступ до дочірньої таблиці працює у зворотному напрямку. Тепер ми запитуємо *які книги написав цей автор* або *переклав цей перекладач*. Для цього типу запиту ми використовуємо метод `related()`, який повертає `Selection` з пов'язаними записами. Розглянемо приклад: - -```php -$author = $explorer->table('author')->get(1); - -// Виведе всі книги автора -foreach ($author->related('book.author_id') as $book) { - echo "Написав: $book->title"; -} - -// Виведе всі книги, які автор переклав -foreach ($author->related('book.translator_id') as $book) { - echo "Переклав: $book->title"; -} -``` - -Метод `related()` приймає опис з'єднання як один аргумент з точковою нотацією або як два окремі аргументи: - -```php -$author->related('book.translator_id'); // один аргумент -$author->related('book', 'translator_id'); // два аргументи -``` - -Explorer може автоматично визначити правильний сполучний стовпець на основі назви батьківської таблиці. У цьому випадку з'єднання відбувається через стовпець `book.author_id`, оскільки назва вихідної таблиці - `author`: - -```php -$author->related('book'); // використовує book.author_id -``` - -Якщо існує кілька можливих з'єднань, Explorer викине виняток [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Метод `related()` можна, звичайно, використовувати і при проходженні кількох записів у циклі, і Explorer і в цьому випадку автоматично оптимізує запити: - -```php -$authors = $explorer->table('author'); -foreach ($authors as $author) { - echo $author->name . ' написав:'; - foreach ($author->related('book') as $book) { - echo $book->title; - } -} -``` - -Цей код згенерує лише два блискавичні SQL-запити: - -```sql -SELECT * FROM `author`; -SELECT * FROM `book` WHERE (`author_id` IN (1, 2, 3)); -- id вибраних авторів -``` - - -Зв'язок Many-to-many --------------------- - -Для зв'язку many-to-many (M:N) необхідна наявність таблиці зв'язку (у нашому випадку `book_tag`), яка містить два стовпці із зовнішніми ключами (`book_id`, `tag_id`). Кожен з цих стовпців посилається на первинний ключ однієї з пов'язуваних таблиць. Для отримання пов'язаних даних спочатку отримуємо записи з таблиці зв'язку за допомогою `related('book_tag')`, а далі переходимо до цільових даних: - -```php -$book = $explorer->table('book')->get(1); -// виведе назви тегів, присвоєних книзі -foreach ($book->related('book_tag') as $bookTag) { - echo $bookTag->tag->name; // виведе назву тегу через таблицю зв'язку -} - -$tag = $explorer->table('tag')->get(1); -// або навпаки: виведе назви книг, позначених цим тегом -foreach ($tag->related('book_tag') as $bookTag) { - echo $bookTag->book->title; // виведе назву книги -} -``` - -Explorer знову оптимізує SQL-запити до ефективної форми: - -```sql -SELECT * FROM `book`; -SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 2, ...)); -- id вибраних книг -SELECT * FROM `tag` WHERE (`tag`.`id` IN (1, 2, ...)); -- id тегів, знайдених у book_tag -``` - - -Запити через пов'язані таблиці ------------------------------- - -У методах `where()`, `select()`, `order()` та `group()` ми можемо використовувати спеціальні нотації для доступу до стовпців з інших таблиць. Explorer автоматично створить необхідні JOIN-и. - -**Точкова нотація** (`батьківська_таблиця.стовпець`) використовується для зв'язку 1:N з точки зору дочірньої таблиці: - -```php -$books = $explorer->table('book'); - -// Знаходить книги, автор яких має ім'я, що починається на 'Jon' -$books->where('author.name LIKE ?', 'Jon%'); - -// Сортує книги за іменем автора за спаданням -$books->order('author.name DESC'); - -// Виводить назву книги та ім'я автора -$books->select('book.title, author.name'); -``` - -**Двокрапкова нотація** (`:дочірня_таблиця.стовпець`) використовується для зв'язку 1:N з точки зору батьківської таблиці: - -```php -$authors = $explorer->table('author'); - -// Знаходить авторів, які написали книгу з 'PHP' у назві -$authors->where(':book.title LIKE ?', '%PHP%'); - -// Рахує кількість книг для кожного автора -$authors->select('*, COUNT(:book.id) AS book_count') - ->group('author.id'); -``` - -У вищезгаданому прикладі з двокрапковою нотацією (`:book.title`) не вказано стовпець із зовнішнім ключем. Explorer автоматично визначає правильний стовпець на основі назви батьківської таблиці. У цьому випадку з'єднання відбувається через стовпець `book.author_id`, оскільки назва вихідної таблиці - `author`. Якщо існує кілька можливих з'єднань, Explorer викине виняток [AmbiguousReferenceKeyException |api:Nette\Database\Conventions\AmbiguousReferenceKeyException]. - -Сполучний стовпець можна явно вказати в дужках: - -```php -// Знаходить авторів, які переклали книгу з 'PHP' у назві -$authors->where(':book(translator_id).title LIKE ?', '%PHP%'); -``` - -Нотації можна ланцюжком для доступу через кілька таблиць: - -```php -// Знаходить авторів книг, позначених тегом 'PHP' -$authors->where(':book:book_tag.tag.name', 'PHP') - ->group('author.id'); -``` - - -Розширення умов для JOIN ------------------------- - -Метод `joinWhere()` розширює умови, які вказуються при з'єднанні таблиць у SQL за ключовим словом `ON`. - -Припустимо, ми хочемо знайти книги, перекладені конкретним перекладачем: - -```php -// Знаходить книги, перекладені перекладачем на ім'я 'David' -$books = $explorer->table('book') - ->joinWhere('translator', 'translator.name', 'David'); -// LEFT JOIN author translator ON book.translator_id = translator.id AND (translator.name = 'David') -``` - -В умові `joinWhere()` ми можемо використовувати ті ж конструкції, що й у методі `where()` - оператори, знаки питання, масиви значень або SQL-вирази. - -Для складніших запитів з кількома JOIN-ами ми можемо визначити аліаси таблиць: - -```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) -``` - -Зверніть увагу, що тоді як метод `where()` додає умови до клаузули `WHERE`, метод `joinWhere()` розширює умови в клаузулі `ON` при з'єднанні таблиць. diff --git a/database/uk/guide.texy b/database/uk/guide.texy deleted file mode 100644 index e192adf548..0000000000 --- a/database/uk/guide.texy +++ /dev/null @@ -1,216 +0,0 @@ -Nette Database -************** - -.[perex] -Nette Database — це потужний та елегантний шар бази даних для PHP з акцентом на простоту та розумні функції. Він пропонує два способи роботи з базою даних — [Explorer |Explorer] для швидкої розробки додатків або [SQL підхід |SQL way] для прямої роботи з запитами. - -<div class="grid gap-3"> -<div> - - -[SQL підхід |SQL way] -===================== -- Безпечні параметризовані запити -- Точний контроль над формою SQL-запитів -- Коли ви пишете складні запити з розширеними функціями -- Оптимізуєте продуктивність за допомогою специфічних функцій SQL - -</div> - -<div> - - -[Explorer |Explorer] -==================== -- Розробляєте швидко, не пишучи SQL -- Інтуїтивна робота з відношеннями між таблицями -- Оціните автоматичну оптимізацію запитів -- Підходить для швидкої та зручної роботи з базою даних - -</div> - -</div> - - -Встановлення -============ - -Завантажте та встановіть бібліотеку за допомогою інструмента [Composer|best-practices:composer]: - -```shell -composer require nette/database -``` - - -Підтримувані бази даних -======================= - -Nette Database підтримує наступні бази даних: - -|* Сервер бази даних |* Ім'я DSN |* Підтримка в Explorer -|---------------------|-------------|----------------------- -| MySQL (>= 5.1) | mysql | ТАК -| PostgreSQL (>= 9.0) | pgsql | ТАК -| Sqlite 3 (>= 3.8) | sqlite | ТАК -| Oracle | oci | - -| MS SQL (PDO_SQLSRV) | sqlsrv | ТАК -| MS SQL (PDO_DBLIB) | mssql | - -| ODBC | odbc | - - - -Два підходи до бази даних -========================= - -Nette Database надає вам вибір: ви можете або писати SQL-запити безпосередньо (SQL підхід), або дозволити генерувати їх автоматично (Explorer). Давайте подивимося, як обидва підходи вирішують однакові завдання: - -[SQL підхід|sql way] - SQL-запити - -```php -// вставка запису -$database->query('INSERT INTO books', [ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// отримання записів: автори книг -$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 -'); - -// виведення (не оптимально, генерує N додаткових запитів) -foreach ($result as $author) { - $books = $database->query(' - SELECT * FROM books - WHERE author_id = ? - ORDER BY published_at DESC - ', $author->id); - - echo "Автор $author->name написав $author->books_count книг:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -[Explorer підхід|explorer] - автоматичне генерування SQL - -```php -// вставка запису -$database->table('books')->insert([ - 'author_id' => $authorId, - 'title' => $bookData->title, - 'published_at' => new DateTime, -]); - -// отримання записів: автори книг -$authors = $database->table('authors') - ->where('active', 1); - -// виведення (автоматично генерує лише 2 оптимізовані запити) -foreach ($authors as $author) { - $books = $author->related('books') - ->order('published_at DESC'); - - echo "Автор $author->name написав {$books->count()} книг:\n"; - - foreach ($books as $book) { - echo "- $book->title\n"; - } -} -``` - -Підхід Explorer генерує та оптимізує SQL-запити автоматично. У наведеному прикладі SQL підхід генерує N+1 запитів (один для авторів, а потім один для книг кожного автора), тоді як Explorer автоматично оптимізує запити та виконує лише два - один для авторів та один для всіх їхніх книг. - -Обидва підходи можна вільно комбінувати в додатку за потреби. - - -Підключення та конфігурація -=========================== - -Для підключення до бази даних достатньо створити екземпляр класу [api:Nette\Database\Connection]: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password); -``` - -Параметр `$dsn` (data source name) такий самий, [який використовує PDO |https://www.php.net/manual/en/pdo.construct.php#refsect1-pdo.construct-parameters], наприклад `mysql:host=127.0.0.1;dbname=test`. У разі збою викидається виняток `Nette\Database\ConnectionException`. - -Однак, зручніший спосіб пропонує [конфігурація програми |configuration], куди достатньо додати секцію `database`, і будуть створені необхідні об'єкти, а також панель бази даних у [Tracy |tracy:] барі. - -```neon -database: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: password -``` - -Потім об'єкт з'єднання [отримаємо як сервіс з DI-контейнера |dependency-injection:passing-dependencies], наприклад: - -```php -class Model -{ - public function __construct( - // або Nette\Database\Explorer - private Nette\Database\Connection $database, - ) { - } -} -``` - -Більше інформації про [конфігурацію бази даних|configuration]. - - -Ручне створення Explorer ------------------------- - -Якщо ви не використовуєте Nette DI-контейнер, ви можете створити екземпляр `Nette\Database\Explorer` вручну: - -```php -// підключення до бази даних -$connection = new Nette\Database\Connection('mysql:host=127.0.0.1;dbname=mydatabase', 'user', 'password'); -// сховище для кешу, реалізує Nette\Caching\Storage, наприклад: -$storage = new Nette\Caching\Storages\FileStorage('/path/to/temp/dir'); -// відповідає за рефлексію структури бази даних -$structure = new Nette\Database\Structure($connection, $storage); -// визначає правила для відображення назв таблиць, стовпців та зовнішніх ключів -$conventions = new Nette\Database\Conventions\DiscoveredConventions($structure); -$explorer = new Nette\Database\Explorer($connection, $structure, $conventions, $storage); -``` - - -Управління підключенням -======================= - -При створенні об'єкта `Connection` підключення відбувається автоматично. Якщо ви хочете відкласти підключення, використовуйте режим lazy - його можна увімкнути в [конфігурації|configuration], встановивши `lazy: true`, або так: - -```php -$database = new Nette\Database\Connection($dsn, $user, $password, ['lazy' => true]); -``` - -Для управління підключенням використовуйте методи `connect()`, `disconnect()` та `reconnect()`. -- `connect()` створює підключення, якщо його ще немає, при цьому може викликати виняток `Nette\Database\ConnectionException`. -- `disconnect()` відключає поточне підключення до бази даних. -- `reconnect()` виконує відключення та подальше повторне підключення до бази даних. Цей метод також може викликати виняток `Nette\Database\ConnectionException`. - -Крім того, ви можете відстежувати події, пов'язані з підключенням, за допомогою події `onConnect`, яка є масивом колбеків, що викликаються після встановлення з'єднання з базою даних. - -```php -// виконується після підключення до бази даних -$database->onConnect[] = function($database) { - echo "Підключено до бази даних"; -}; -``` - - -Tracy Debug Bar -=============== - -Якщо ви використовуєте [Tracy |tracy:], автоматично активується панель Database в Debug барі, яка відображає всі виконані запити, їхні параметри, час виконання та місце в коді, де вони були викликані. - -[* db-panel.webp *] diff --git a/database/uk/mapping.texy b/database/uk/mapping.texy deleted file mode 100644 index c8bcebb200..0000000000 --- a/database/uk/mapping.texy +++ /dev/null @@ -1,55 +0,0 @@ -Перетворення типів -****************** - -.[perex] -Nette Database автоматично перетворює значення, повернуті з бази даних, на відповідні типи PHP. - - -Дата та час ------------ - -Часові дані перетворюються на об'єкти `Nette\Utils\DateTime`. Якщо ви хочете, щоб часові дані перетворювалися на незмінні об'єкти `Nette\Database\DateTime`, встановіть у [конфігурації|configuration] опцію `newDateTime: 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'); -``` - -У випадку MySQL перетворює тип даних `TIME` на об'єкти `DateInterval`. - - -Булеві значення ---------------- - -Булеві значення автоматично перетворюються на `true` або `false`. У MySQL перетворюється `TINYINT(1)`, якщо ми встановимо в [конфігурації|configuration] `convertBoolean: true`. - -```php -$row = $database->fetch('SELECT is_published FROM articles'); -echo gettype($row->is_published); // 'boolean' -``` - - -Числові значення ----------------- - -Числові значення перетворюються на `int` або `float` відповідно до типу стовпця в базі даних: - -```php -$row = $database->fetch('SELECT id, price FROM products'); -echo gettype($row->id); // integer -echo gettype($row->price); // float -``` - - -Власна нормалізація -------------------- - -За допомогою методу `setRowNormalizer(?callable $normalizer)` ви можете встановити власну функцію для трансформації рядків з бази даних. Це корисно, наприклад, для автоматичного перетворення типів даних. - -```php -$database->setRowNormalizer(function(array $row, ResultSet $resultSet): array { - // тут відбувається перетворення типів - return $row; -}); -``` diff --git a/database/uk/reflection.texy b/database/uk/reflection.texy deleted file mode 100644 index 703664abb8..0000000000 --- a/database/uk/reflection.texy +++ /dev/null @@ -1,125 +0,0 @@ -Рефлексія структури -******************* - -.{data-version:3.2.1} -Nette Database надає інструменти для інтроспекції структури бази даних за допомогою класу [api:Nette\Database\Reflection]. Вона дозволяє отримувати інформацію про таблиці, стовпці, індекси та зовнішні ключі. Рефлексію можна використовувати для генерації схем, створення гнучких додатків, що працюють з базою даних, або загальних інструментів для роботи з базами даних. - -Об'єкт рефлексії отримуємо з екземпляра підключення до бази даних: - -```php -$reflection = $database->getReflection(); -``` - - -Отримання таблиць ------------------ - -Readonly властивість `$reflection->tables` містить асоціативний масив усіх таблиць у базі даних: - -```php -// Виведення назв усіх таблиць -foreach ($reflection->tables as $name => $table) { - echo $name . "\n"; -} -``` - -Доступні ще два методи: - -```php -// Перевірка існування таблиці -if ($reflection->hasTable('users')) { - echo "Таблиця users існує"; -} - -// Повертає об'єкт таблиці; якщо не існує, викидає виняток -$table = $reflection->getTable('users'); -``` - - -Інформація про таблицю ----------------------- - -Таблиця представлена об'єктом [Table|api:Nette\Database\Reflection\Table], який надає наступні readonly властивості: - -- `$name: string` – назва таблиці -- `$view: bool` – чи є це представленням (view) -- `$fullName: ?string` – повна назва таблиці, включаючи схему (якщо існує) -- `$columns: array<string, Column>` – асоціативний масив стовпців таблиці -- `$indexes: Index[]` – масив індексів таблиці -- `$primaryKey: ?Index` – первинний ключ таблиці або null -- `$foreignKeys: ForeignKey[]` – масив зовнішніх ключів таблиці - - -Стовпці -------- - -Властивість `columns` таблиці надає асоціативний масив стовпців, де ключем є назва стовпця, а значенням - екземпляр [Column|api:Nette\Database\Reflection\Column] з такими властивостями: - -- `$name: string` – назва стовпця -- `$table: ?Table` – посилання на таблицю стовпця -- `$nativeType: string` – нативний тип даних бази даних -- `$size: ?int` – розмір/довжина типу -- `$nullable: bool` – чи може стовпець містити NULL -- `$default: mixed` – значення за замовчуванням стовпця -- `$autoIncrement: bool` – чи є стовпець автоінкрементним -- `$primary: bool` – чи є частиною первинного ключа -- `$vendor: array` – додаткові метадані, специфічні для даної системи бази даних - -```php -foreach ($table->columns as $name => $column) { - echo "Стовпець: $name\n"; - echo "Тип: {$column->nativeType}\n"; - echo "Nullable: " . ($column->nullable ? 'Так' : 'Ні') . "\n"; -} -``` - - -Індекси -------- - -Властивість `indexes` таблиці надає масив індексів, де кожен індекс є екземпляром [Index|api:Nette\Database\Reflection\Index] з такими властивостями: - -- `$columns: Column[]` – масив стовпців, що утворюють індекс -- `$unique: bool` – чи є індекс унікальним -- `$primary: bool` – чи є це первинним ключем -- `$name: ?string` – назва індексу - -Первинний ключ таблиці можна отримати за допомогою властивості `primaryKey`, яка повертає або об'єкт `Index`, або `null` у випадку, якщо таблиця не має первинного ключа. - -```php -// Виведення індексів -foreach ($table->indexes as $index) { - $columns = implode(', ', array_map(fn($col) => $col->name, $index->columns)); - echo "Індекс" . ($index->name ? " {$index->name}" : '') . ":\n"; - echo " Стовпці: $columns\n"; - echo " Unique: " . ($index->unique ? 'Так' : 'Ні') . "\n"; -} - -// Виведення первинного ключа -if ($primaryKey = $table->primaryKey) { - $columns = implode(', ', array_map(fn($col) => $col->name, $primaryKey->columns)); - echo "Первинний ключ: $columns\n"; -} -``` - - -Зовнішні ключі --------------- - -Властивість `foreignKeys` таблиці надає масив зовнішніх ключів, де кожен зовнішній ключ є екземпляром [ForeignKey|api:Nette\Database\Reflection\ForeignKey] з такими властивостями: - -- `$foreignTable: Table` – таблиця, на яку посилається ключ -- `$localColumns: Column[]` – масив локальних стовпців -- `$foreignColumns: Column[]` – масив стовпців, на які посилається ключ -- `$name: ?string` – назва зовнішнього ключа - -```php -// Виведення зовнішніх ключів -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/uk/security.texy b/database/uk/security.texy deleted file mode 100644 index cc04ec9783..0000000000 --- a/database/uk/security.texy +++ /dev/null @@ -1,185 +0,0 @@ -Ризики безпеки -************** - -<div class=perex> - -База даних часто містить конфіденційні дані та дозволяє виконувати небезпечні операції. Для безпечної роботи з Nette Database ключовим є: - -- Розуміти різницю між безпечним та небезпечним API -- Використовувати параметризовані запити -- Правильно валідувати вхідні дані - -</div> - - -Що таке SQL Injection? -====================== - -SQL injection є найсерйознішим ризиком безпеки при роботі з базою даних. Він виникає, коли необроблені вхідні дані від користувача стають частиною SQL-запиту. Зловмисник може вставити власні SQL-команди і таким чином: -- Отримати несанкціонований доступ до даних -- Змінити або видалити дані в базі даних -- Обійти автентифікацію - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL-ін'єкції -$database->query("SELECT * FROM users WHERE name = '$_GET[name]'"); - -// Зловмисник може ввести, наприклад, значення: ' OR '1'='1 -// Кінцевий запит буде: SELECT * FROM users WHERE name = '' OR '1'='1' -// Що поверне всіх користувачів -``` - -Те саме стосується і Database Explorer: - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL-ін'єкції -$table->where('name = ' . $_GET['name']); -$table->where("name = '$_GET[name]'"); -``` - - -Параметризовані запити -====================== - -Основний захист від SQL injection - це параметризовані запити. Nette Database пропонує кілька способів їх використання. - -Найпростіший спосіб - використання **заповнювачів-знаків питання**: - -```php -// ✅ Безпечний параметризований запит -$database->query('SELECT * FROM users WHERE name = ?', $name); - -// ✅ Безпечна умова в Explorer -$table->where('name = ?', $name); -``` - -Це стосується всіх інших методів у [Database Explorer|explorer], які дозволяють вставляти вирази з заповнювачами-знаками питання та параметрами. - -Для команд INSERT, UPDATE або умови WHERE ми можемо передати значення в масиві: - -```php -// ✅ Безпечний INSERT -$database->query('INSERT INTO users', [ - 'name' => $name, - 'email' => $email, -]); - -// ✅ Безпечний INSERT в Explorer -$table->insert([ - 'name' => $name, - 'email' => $email, -]); -``` - - -Валідація значень параметрів -============================ - -Параметризовані запити є основним будівельним блоком безпечної роботи з базою даних. Однак значення, які ми в них вставляємо, повинні пройти кілька рівнів перевірок: - - -Перевірка типу --------------- - -**Найважливіше - забезпечити правильний тип даних параметрів** - це необхідна умова для безпечного використання Nette Database. База даних передбачає, що всі вхідні дані мають правильний тип даних, що відповідає даному стовпцю. - -Наприклад, якби `$name` у попередніх прикладах було несподівано масивом замість рядка, Nette Database спробувала б вставити всі його елементи в SQL-запит, що призвело б до помилки. Тому **ніколи не використовуйте** невалідовані дані з `$_GET`, `$_POST` або `$_COOKIE` безпосередньо в запитах до бази даних. - - -Перевірка формату ------------------ - -На другому рівні ми перевіряємо формат даних - наприклад, чи є рядки в кодуванні UTF-8 та чи їхня довжина відповідає визначенню стовпця, або чи є числові значення в дозволеному діапазоні для даного типу даних стовпця. - -На цьому рівні валідації ми можемо частково покладатися і на саму базу даних - багато баз даних відхилять невалідовані дані. Однак поведінка може відрізнятися, деякі можуть тихо скоротити довгі рядки або обрізати числа поза діапазоном. - - -Доменна перевірка ------------------ - -Третій рівень представляють логічні перевірки, специфічні для вашого додатка. Наприклад, перевірка, що значення з select box відповідають запропонованим варіантам, що числа знаходяться в очікуваному діапазоні (наприклад, вік 0-150 років) або що взаємні залежності між значеннями мають сенс. - - -Рекомендовані способи валідації -------------------------------- - -- Використовуйте [Nette Forms|forms:], які автоматично забезпечать правильну валідацію всіх вхідних даних -- Використовуйте [Presenters|application:] та вказуйте у параметрів в `action*()` та `render*()` методах типи даних -- Або реалізуйте власний шар валідації за допомогою стандартних інструментів PHP, таких як `filter_var()` - - -Безпечна робота зі стовпцями -============================ - -У попередньому розділі ми показали, як правильно валідувати значення параметрів. Однак при використанні масивів у SQL-запитах ми повинні приділяти таку ж увагу і їхнім ключам. - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - не оброблені ключі в масиві -$database->query('INSERT INTO users', $_POST); -``` - -У командах INSERT та UPDATE це є критичною помилкою безпеки - зловмисник може вставити або змінити будь-який стовпець у базі даних. Він міг би, наприклад, встановити `is_admin = 1` або вставити будь-які дані в конфіденційні стовпці (так звана Mass Assignment Vulnerability). - -В умовах WHERE це ще небезпечніше, оскільки вони можуть містити оператори: - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - не оброблені ключі в масиві -$_POST['salary >'] = 100000; -$database->query('SELECT * FROM users WHERE', $_POST); -// виконає запит WHERE (`salary` > 100000) -``` - -Зловмисник може використати цей підхід для систематичного з'ясування зарплат співробітників. Наприклад, почне із запиту на зарплати понад 100 000, потім менше 50 000 і поступовим звуженням діапазону може виявити приблизні зарплати всіх співробітників. Цей тип атаки називається SQL enumeration. - -Методи `where()` та `whereOr()` є ще [набагато гнучкішими |explorer#where] і підтримують у ключах та значеннях SQL-вирази, включаючи оператори та функції. Це дає зловмиснику можливість здійснити SQL-ін'єкцію: - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - зловмисник може вставити власний SQL -$_POST = ['0) UNION SELECT name, salary FROM users WHERE (1']; -$table->where($_POST); -// виконає запит WHERE (0) UNION SELECT name, salary FROM users WHERE (1) -``` - -Ця атака завершує початкову умову за допомогою `0)`, приєднує власний `SELECT` за допомогою `UNION` для отримання конфіденційних даних з таблиці `users` та закриває синтаксично правильний запит за допомогою `WHERE (1)`. - - -Білий список стовпців ---------------------- - -Для безпечної роботи з назвами стовпців нам потрібен механізм, який забезпечить, що користувач може працювати лише з дозволеними стовпцями і не може додати власні. Ми могли б спробувати виявляти та блокувати небезпечні назви стовпців (чорний список), але цей підхід ненадійний - зловмисник завжди може придумати новий спосіб записати небезпечну назву стовпця, який ми не передбачили. - -Тому набагато безпечніше змінити логіку і визначити явний список дозволених стовпців (білий список): - -```php -// Стовпці, які користувач може редагувати -$allowedColumns = ['name', 'email', 'active']; - -// Видалимо всі недозволені стовпці з вхідних даних -$filteredData = array_intersect_key($userData, array_flip($allowedColumns)); - -// ✅ Тепер можна безпечно використовувати в запитах, наприклад: -$database->query('INSERT INTO users', $filteredData); -$table->update($filteredData); -$table->where($filteredData); -``` - - -Динамічні ідентифікатори -======================== - -Для динамічних назв таблиць та стовпців використовуйте заповнювач `?name`. Він забезпечить правильне екранування ідентифікаторів відповідно до синтаксису даної бази даних (наприклад, за допомогою зворотних апострофів у MySQL): - -```php -// ✅ Безпечне використання довірених ідентифікаторів -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name', $column, $table); -// Результат у MySQL: SELECT `name` FROM `users` -``` - -Важливо: символ `?name` використовуйте лише для довірених значень, визначених у коді програми. Для значень від користувача використовуйте знову [білий список |#Білий список стовпців]. Інакше ви наражаєтеся на ризики безпеки: - -```php -// ❌ НЕБЕЗПЕЧНО - ніколи не використовуйте вхідні дані від користувача -$database->query('SELECT ?name FROM users', $_GET['column']); -``` diff --git a/database/uk/sql-way.texy b/database/uk/sql-way.texy deleted file mode 100644 index 3565ac16c4..0000000000 --- a/database/uk/sql-way.texy +++ /dev/null @@ -1,513 +0,0 @@ -SQL підхід -********** - -.[perex] -Nette Database пропонує два шляхи: ви можете писати SQL-запити самостійно (SQL підхід), або дозволити генерувати їх автоматично (див. [Explorer |explorer]). SQL підхід дає вам повний контроль над запитами і при цьому забезпечує їх безпечне формування. - -.[note] -Деталі щодо підключення та конфігурації бази даних знайдете в розділі [Підключення та конфігурація |guide#Підключення та конфігурація]. - - -Базові запити -============= - -Для запитів до бази даних служить метод `query()`. Він повертає об'єкт [ResultSet |api:Nette\Database\ResultSet], який представляє результат запиту. У разі невдачі метод [викине виняток|exceptions]. Результат запиту можна перебирати за допомогою циклу `foreach`, або використати одну з [допоміжних функцій |#Отримання даних]. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; -} -``` - -Для безпечного вставлення значень у SQL-запити використовуємо параметризовані запити. Nette Database робить їх максимально простими - достатньо після SQL-запиту додати кому та значення: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -При використанні кількох параметрів у вас є два варіанти запису. Ви можете "розбавляти" SQL-запит параметрами: - -```php -$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age); -``` - -Або написати спочатку весь SQL-запит, а потім додати всі параметри: - -```php -$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age); -``` - - -Захист від SQL injection -======================== - -Чому важливо використовувати параметризовані запити? Тому що вони захищають вас від атаки під назвою SQL injection, під час якої зловмисник міг би підсунути власні SQL-команди і таким чином отримати або пошкодити дані в базі даних. - -.[warning] -**Ніколи не вставляйте змінні безпосередньо в SQL-запит!** Завжди використовуйте параметризовані запити, які захистять вас від SQL injection. - -```php -// ❌ НЕБЕЗПЕЧНИЙ КОД - вразливий до SQL injection -$database->query("SELECT * FROM users WHERE name = '$name'"); - -// ✅ Безпечний параметризований запит -$database->query('SELECT * FROM users WHERE name = ?', $name); -``` - -Ознайомтеся з [можливими ризиками безпеки |security]. - - -Техніки запитів -=============== - - -Умови WHERE ------------ - -Умови WHERE можна записати як асоціативний масив, де ключі - це назви стовпців, а значення - дані для порівняння. Nette Database автоматично вибере найбільш відповідний SQL-оператор залежно від типу значення. - -```php -$database->query('SELECT * FROM users WHERE', [ - 'name' => 'John', - 'active' => true, -]); -// WHERE `name` = 'John' AND `active` = 1 -``` - -У ключі можна також явно вказати оператор для порівняння: - -```php -$database->query('SELECT * FROM users WHERE', [ - 'age >' => 25, // використовує оператор > - 'name LIKE' => '%John%', // використовує оператор LIKE - 'email NOT LIKE' => '%example.com%', // використовує оператор NOT LIKE -]); -// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%' -``` - -Nette автоматично обробляє спеціальні випадки, такі як значення `null` або масиви. - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name' => 'Laptop', // використовує оператор = - 'category_id' => [1, 2, 3], // використовує IN - 'description' => null, // використовує IS NULL -]); -// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL -``` - -Для негативних умов використовуйте оператор `NOT`: - -```php -$database->query('SELECT * FROM products WHERE', [ - 'name NOT' => 'Laptop', // використовує оператор <> - 'category_id NOT' => [1, 2, 3], // використовує NOT IN - 'description NOT' => null, // використовує IS NOT NULL - 'id' => [], // пропускається -]); -// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL -``` - -Для об'єднання умов використовується оператор `AND`. Це можна змінити за допомогою [заповнювача ?or |#Підказки для побудови SQL]. - - -Правила ORDER BY ----------------- - -Сортування `ORDER BY` можна записати за допомогою масиву. У ключах вказуємо стовпці, а значенням буде boolean, що визначає, чи сортувати за зростанням: - -```php -$database->query('SELECT id FROM author ORDER BY', [ - 'id' => true, // за зростанням - 'name' => false, // за спаданням -]); -// SELECT id FROM author ORDER BY `id`, `name` DESC -``` - - -Вставка даних (INSERT) ----------------------- - -Для вставки записів використовується SQL-команда `INSERT`. - -```php -$values = [ - 'name' => 'John Doe', - 'email' => 'john@example.com', -]; -$database->query('INSERT INTO users ?', $values); -$userId = $database->getInsertId(); -``` - -Метод `getInsertId()` повертає ID останнього вставленого рядка. У деяких базах даних (наприклад, PostgreSQL) необхідно як параметр вказати назву послідовності, з якої має генеруватися ID, за допомогою `$database->getInsertId($sequenceId)`. - -Як параметри можна передавати і [#Спеціальні значення], такі як файли, об'єкти DateTime або перелічувані типи. - -Вставка кількох записів одночасно: - -```php -$database->query('INSERT INTO users ?', [ - ['name' => 'User 1', 'email' => 'user1@mail.com'], - ['name' => 'User 2', 'email' => 'user2@mail.com'], -]); -``` - -Багаторазовий INSERT набагато швидший, оскільки виконується єдиний запит до бази даних замість багатьох окремих. - -**Попередження щодо безпеки:** Ніколи не використовуйте як `$values` невалідовані дані. Ознайомтеся з [можливими ризиками |security#Безпечна робота зі стовпцями]. - - -Оновлення даних (UPDATE) ------------------------- - -Для оновлення записів використовується SQL-команда `UPDATE`. - -```php -// Оновлення одного запису -$values = [ - 'name' => 'John Smith', -]; -$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1); -``` - -Кількість зачеплених рядків поверне `$result->getRowCount()`. - -Для UPDATE можна використовувати оператори `+=` та `-=`: - -```php -$database->query('UPDATE users SET ? WHERE id = ?', [ - 'login_count+=' => 1, // інкрементація login_count -], 1); -``` - -Приклад вставки або оновлення запису, якщо він вже існує. Використаємо техніку `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 -``` - -Зверніть увагу, що Nette Database розпізнає, в якому контексті SQL-команди вставляється параметр з масивом, і відповідно до цього складає з нього SQL-код. Так, з першого масиву він склав `(id, name, year) VALUES (123, 'Jim', 1978)`, тоді як другий перетворив на вигляд `name = 'Jim', year = 1978`. Детальніше про це йдеться в розділі [#Підказки для побудови SQL]. - - -Видалення даних (DELETE) ------------------------- - -Для видалення записів використовується SQL-команда `DELETE`. Приклад з отриманням кількості видалених рядків: - -```php -$count = $database->query('DELETE FROM users WHERE id = ?', 1) - ->getRowCount(); -``` - - -Підказки для побудови SQL -------------------------- - -Підказка - це спеціальний заповнювач у SQL-запиті, який вказує, як значення параметра має бути перетворено на SQL-вираз: - -| Підказка | Опис | Автоматично використовується -|-----------|-------------------------------------------------|----------------------------- -| `?name` | використовується для вставки назви таблиці або стовпця | - -| `?values` | генерує `(key, ...) VALUES (value, ...)` | `INSERT ... ?`, `REPLACE ... ?` -| `?set` | генерує присвоєння `key = value, ...` | `SET ?`, `KEY UPDATE ?` -| `?and` | об'єднує умови в масиві оператором `AND` | `WHERE ?`, `HAVING ?` -| `?or` | об'єднує умови в масиві оператором `OR` | - -| `?order` | генерує умову `ORDER BY` | `ORDER BY ?`, `GROUP BY ?` - -Для динамічного вставлення назв таблиць та стовпців у запит служить заповнювач `?name`. Nette Database подбає про правильну обробку ідентифікаторів відповідно до конвенцій даної бази даних (наприклад, взяття у зворотні лапки в MySQL). - -```php -$table = 'users'; -$column = 'name'; -$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table); -// SELECT `name` FROM `users` WHERE id = 1 (у MySQL) -``` - -**Попередження:** символ `?name` використовуйте лише для назв таблиць та стовпців з валідованих вхідних даних, інакше ви наражаєтеся на [ризик безпеки |security#Динамічні ідентифікатори]. - -Інші підказки зазвичай не потрібно вказувати, оскільки Nette використовує розумну автодетекцію при складанні SQL-запиту (див. третій стовпець таблиці). Але ви можете її використати, наприклад, у ситуації, коли хочете об'єднати умови за допомогою `OR` замість `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' -``` - - -Спеціальні значення -------------------- - -Крім звичайних скалярних типів (string, int, bool), ви можете передавати як параметри і спеціальні значення: - -- файли: `fopen('image.gif', 'r')` вставить бінарний вміст файлу -- дата та час: об'єкти `DateTime` перетворяться на формат бази даних -- перелічувані типи: екземпляри `enum` перетворяться на їхнє значення -- SQL літерали: створені за допомогою `Connection::literal('NOW()')` вставляться безпосередньо в запит - -```php -$database->query('INSERT INTO articles ?', [ - 'title' => 'My Article', - 'published_at' => new DateTime, - 'content' => fopen('image.png', 'r'), - 'state' => Status::Draft, -]); -``` - -У базах даних, які не мають нативної підтримки для типу даних `datetime` (як SQLite та Oracle), `DateTime` перетворюється на значення, визначене в [конфігурації бази даних|configuration] елементом `formatDateTime` (значення за замовчуванням - `U` - unix timestamp). - - -SQL літерали ------------- - -У деяких випадках потрібно вказати як значення безпосередньо SQL-код, який, однак, не повинен розглядатися як рядок і екрануватися. Для цього служать об'єкти класу `Nette\Database\SqlLiteral`. Їх створює метод `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()) -``` - -Або альтернативно: - -```php -$result = $database->query('SELECT * FROM users WHERE', [ - 'name' => $name, - $database::literal('year > YEAR()'), -]); -// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR()) -``` - -SQL літерали можуть містити параметри: - -```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) -``` - -Завдяки чому можна створювати цікаві комбінації: - -```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') -``` - - -Отримання даних -=============== - - -Скорочення для SELECT-запитів ------------------------------ - -Для спрощення завантаження даних `Connection` пропонує кілька скорочень, які комбінують виклик `query()` з наступним `fetch*()`. Ці методи приймають ті самі параметри, що й `query()`, тобто SQL-запит та необов'язкові параметри. Повний опис методів `fetch*()` знайдете [нижче |#fetch]. - -| `fetch($sql, ...$params): ?Row` | Виконує запит і повертає перший рядок як об'єкт `Row` -| `fetchAll($sql, ...$params): array` | Виконує запит і повертає всі рядки як масив об'єктів `Row` -| `fetchPairs($sql, ...$params): array` | Виконує запит і повертає асоціативний масив, де перший стовпець представляє ключ, а другий - значення -| `fetchField($sql, ...$params): mixed` | Виконує запит і повертає значення першого поля з першого рядка -| `fetchList($sql, ...$params): ?array` | Виконує запит і повертає перший рядок як індексований масив - -Приклад: - -```php -// fetchField() - повертає значення першої комірки -$count = $database->query('SELECT COUNT(*) FROM articles') - ->fetchField(); -``` - - -`foreach` - ітерація по рядках ------------------------------- - -Після виконання запиту повертається об'єкт [ResultSet|api:Nette\Database\ResultSet], який дозволяє перебирати результати кількома способами. Найпростіший спосіб виконати запит і отримати рядки - це ітерація в циклі `foreach`. Цей спосіб є найбільш економним з точки зору пам'яті, оскільки повертає дані поступово і не зберігає їх усі в пам'яті одночасно. - -```php -$result = $database->query('SELECT * FROM users'); - -foreach ($result as $row) { - echo $row->id; - echo $row->name; - // ... -} -``` - -.[note] -`ResultSet` можна ітерувати лише один раз. Якщо вам потрібно ітерувати повторно, ви повинні спочатку завантажити дані в масив, наприклад, за допомогою методу `fetchAll()`. - - -fetch(): ?Row .[method] ------------------------ - -Повертає рядок як об'єкт `Row`. Якщо більше немає рядків, повертає `null`. Пересуває внутрішній вказівник на наступний рядок. - -```php -$result = $database->query('SELECT * FROM users'); -$row = $result->fetch(); // читає перший рядок -if ($row) { - echo $row->name; -} -``` - - -fetchAll(): array .[method] ---------------------------- - -Повертає всі рядки, що залишилися, з `ResultSet` як масив об'єктів `Row`. - -```php -$result = $database->query('SELECT * FROM users'); -$rows = $result->fetchAll(); // читає всі рядки -foreach ($rows as $row) { - echo $row->name; -} -``` - - -fetchPairs(string|int|null $key = null, string|int|null $value = null): array .[method] ---------------------------------------------------------------------------------------- - -Повертає результати як асоціативний масив. Перший аргумент визначає назву стовпця, який буде використаний як ключ у масиві, другий аргумент визначає назву стовпця, який буде використаний як значення: - -```php -$result = $database->query('SELECT id, name FROM users'); -$names = $result->fetchPairs('id', 'name'); -// [1 => 'John Doe', 2 => 'Jane Doe', ...] -``` - -Якщо вказати лише перший параметр, значенням буде весь рядок, тобто об'єкт `Row`: - -```php -$rows = $result->fetchPairs('id'); -// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...] -``` - -У разі дублювання ключів використовується значення з останнього рядка. При використанні `null` як ключа масив буде індексовано нумерично з нуля (тоді колізій не виникає): - -```php -$names = $result->fetchPairs(null, 'name'); -// [0 => 'John Doe', 1 => 'Jane Doe', ...] -``` - - -fetchPairs(Closure $callback): array .[method] ----------------------------------------------- - -Альтернативно, ви можете вказати як параметр callback, який для кожного рядка повертатиме або саме значення, або пару ключ-значення. - -```php -$result = $database->query('SELECT * FROM users'); -$items = $result->fetchPairs(fn($row) => "$row->id - $row->name"); -// ['1 - John', '2 - Jane', ...] - -// Callback також може повертати масив із парою ключ & значення: -$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]); -// ['John' => 46, 'Jane' => 21, ...] -``` - - -fetchField(): mixed .[method] ------------------------------ - -Повертає значення першого поля з поточного рядка. Якщо більше немає рядків, повертає `null`. Пересуває внутрішній вказівник на наступний рядок. - -```php -$result = $database->query('SELECT name FROM users'); -$name = $result->fetchField(); // читає ім'я з першого рядка -``` - - -fetchList(): ?array .[method] ------------------------------ - -Повертає рядок як індексований масив. Якщо більше немає рядків, повертає `null`. Пересуває внутрішній вказівник на наступний рядок. - -```php -$result = $database->query('SELECT name, email FROM users'); -$row = $result->fetchList(); // ['John', 'john@example.com'] -``` - - -getRowCount(): ?int .[method] ------------------------------ - -Повертає кількість зачеплених рядків останнім запитом `UPDATE` або `DELETE`. Для `SELECT` це кількість повернутих рядків, але вона може бути невідомою - у такому випадку метод поверне `null`. - - -getColumnCount(): ?int .[method] --------------------------------- - -Повертає кількість стовпців у `ResultSet`. - - -Інформація про запити -===================== - -Для цілей налагодження ми можемо отримати інформацію про останній виконаний запит: - -```php -echo $database->getLastQueryString(); // виводить SQL-запит - -$result = $database->query('SELECT * FROM articles'); -echo $result->getQueryString(); // виводить SQL-запит -echo $result->getTime(); // виводить час виконання в секундах -``` - -Для відображення результату у вигляді HTML-таблиці можна використати: - -```php -$result = $database->query('SELECT * FROM articles'); -$result->dump(); -``` - -ResultSet пропонує інформацію про типи стовпців: - -```php -$result = $database->query('SELECT * FROM articles'); -$types = $result->getColumnTypes(); - -foreach ($types as $column => $type) { - echo "$column має тип $type->type"; // напр. 'id має тип int' -} -``` - - -Логування запитів ------------------ - -Ми можемо реалізувати власне логування запитів. Подія `onQuery` - це масив callback'ів, які викликаються після кожного виконаного запиту: - -```php -$database->onQuery[] = function ($database, $result) use ($logger) { - $logger->info('Запит: ' . $result->getQueryString()); - $logger->info('Час: ' . $result->getTime()); - - if ($result->getRowCount() > 1000) { - $logger->warning('Великий набір результатів: ' . $result->getRowCount() . ' рядків'); - } -}; -``` diff --git a/database/uk/transactions.texy b/database/uk/transactions.texy deleted file mode 100644 index 57053362e7..0000000000 --- a/database/uk/transactions.texy +++ /dev/null @@ -1,43 +0,0 @@ -Транзакції -********** - -.[perex] -Транзакції гарантують, що або всі операції в рамках транзакції будуть виконані, або жодна з них. Вони корисні для забезпечення узгодженості даних під час складних операцій. - -Найпростіший спосіб використання транзакцій виглядає так: - -```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; -} -``` - -Набагато елегантніше те саме можна записати за допомогою методу `transaction()`. Він приймає як параметр callback, який виконується в транзакції. Якщо callback завершується без винятку, транзакція автоматично підтверджується. Якщо виникає виняток, транзакція скасовується (rollback), а виняток поширюється далі. - -```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' - ]); -}); -``` - -Метод `transaction()` також може повертати значення: - -```php -$count = $database->transaction(function ($database) { - $result = $database->query('UPDATE users SET active = ?', true); - return $result->getRowCount(); // повертає кількість оновлених рядків -}); -``` diff --git a/dependency-injection/bg/@home.texy b/dependency-injection/bg/@home.texy deleted file mode 100644 index 6e1130be6c..0000000000 --- a/dependency-injection/bg/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection е дизайн патърн, който коренно ще промени вашия поглед върху кода и разработката. Ще ви отвори пътя към света на чисто проектирани и устойчиви приложения. - -- [Какво е Dependency Injection? |introduction] -- [Глобално състояние и сингълтони |global-state] -- [Предаване на зависимости |passing-dependencies] -- [Какво е DI контейнер? |container] -- [Често задавани въпроси|faq] - - -Пакетът `nette/di` предоставя изключително усъвършенстван компилиран DI контейнер за PHP. - -- [Nette DI Container |nette-container] -- [Конфигурация |configuration] -- [Дефиниране на сървиси |services] -- [Autowiring |autowiring] -- [Генерирани фабрики |factory] -- [Създаване на разширения за Nette DI|extensions] diff --git a/dependency-injection/bg/@left-menu.texy b/dependency-injection/bg/@left-menu.texy deleted file mode 100644 index 77e92a85f8..0000000000 --- a/dependency-injection/bg/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Какво е DI? |introduction] -- [Глобално състояние и сингълтони |global-state] -- [Предаване на зависимости |passing-dependencies] -- [Какво е DI контейнер? |container] -- [Често задавани въпроси|faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Конфигурация |configuration] -- [Дефиниране на сървиси |services] -- [Autowiring |autowiring] -- [Генерирани фабрики |factory] -- [Създаване на разширения за Nette DI|extensions] diff --git a/dependency-injection/bg/@meta.texy b/dependency-injection/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/dependency-injection/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/dependency-injection/bg/autowiring.texy b/dependency-injection/bg/autowiring.texy deleted file mode 100644 index c5ce14c62f..0000000000 --- a/dependency-injection/bg/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring е страхотна функция, която може автоматично да предава необходимите сървиси към конструктора и други методи, така че изобщо не е необходимо да ги пишем. Ще ви спести много време. - -Благодарение на това можем да пропуснем по-голямата част от аргументите при писане на дефиниции на сървиси. Вместо: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Достатъчно е да напишете: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring се ръководи от типовете, така че за да работи, класът `ArticleRepository` трябва да бъде дефиниран приблизително така: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -За да може да се използва autowiring, за всеки тип трябва да има **точно един сървис** в контейнера. Ако има повече, autowiring няма да знае кой от тях да предаде и ще хвърли изключение: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # ХВЪРЛЯ ИЗКЛЮЧЕНИЕ, отговарят и mainDb, и tempDb -``` - -Решението би било или да се заобиколи autowiring и изрично да се посочи името на сървиса (т.е. `articles: Model\ArticleRepository(@mainDb)`). По-удобно обаче е autowiring-ът на един от сървисите да се [изключи |#Изключване на autowiring] или първият сървис да се [предпочете |#Предпочитание за autowiring]. - - -Изключване на autowiring ------------------------- - -Можем да изключим autowiring-а на сървис с помощта на опцията `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # сървисът tempDb е изключен от autowiring - - articles: Model\ArticleRepository # следователно предава mainDb на конструктора -``` - -Сървисът `articles` няма да хвърли изключение, че съществуват два подходящи сървиса от тип `PDO` (т.е. `mainDb` и `tempDb`), които могат да бъдат предадени на конструктора, защото вижда само сървиса `mainDb`. - -.[note] -Конфигурацията на autowiring в Nette работи различно от тази в Symfony, където опцията `autowire: false` указва, че autowiring не трябва да се използва за аргументите на конструктора на дадения сървис. В Nette autowiring се използва винаги, независимо дали за аргументите на конструктора, или за които и да било други методи. Опцията `autowired: false` указва, че инстанцията на дадения сървис не трябва да бъде предавана никъде чрез autowiring. - - -Предпочитание за autowiring ---------------------------- - -Ако имаме няколко сървиса от един и същи тип и за един от тях посочим опцията `autowired`, този сървис става предпочитан: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # става предпочитан - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Сървисът `articles` няма да хвърли изключение, че съществуват два подходящи сървиса от тип `PDO` (т.е. `mainDb` и `tempDb`), а ще използва предпочитания сървис, т.е. `mainDb`. - - -Масив от сървиси ----------------- - -Autowiring може да предава и масиви от сървиси от определен тип. Тъй като в PHP не може нативно да се запише типът на елементите на масива, е необходимо освен типа `array` да се добави и phpDoc коментар с типа на елемента във формата `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -След това DI контейнерът автоматично предава масив от сървиси, съответстващи на дадения тип. Пропуска сървисите, които имат изключен autowiring. - -Типът в коментара може да бъде също във формата `array<int, Class>` или `list<Class>`. Ако не можете да повлияете на формата на phpDoc коментара, можете да предадете масива от сървиси директно в конфигурацията с помощта на [`typed()` |services#Специални функции]. - - -Скаларни аргументи ------------------- - -Autowiring може да инжектира само обекти и масиви от обекти. Скаларните аргументи (напр. низове, числа, булеви стойности) [се записват в конфигурацията |services#Аргументи]. Алтернатива е да се създаде [settings-обект |best-practices:passing-settings-to-presenters], който капсулира скаларната стойност (или няколко стойности) под формата на обект, и той след това може отново да се предава чрез autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly може да се използва от PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Създавате сървис от него, като го добавите към конфигурацията: - -```neon -services: - - MySettings('any value') -``` - -След това всички класове го изискват чрез autowiring. - - -Стесняване на autowiring ------------------------- - -За отделни сървиси autowiring може да бъде стеснен само до определени класове или интерфейси. - -Обикновено autowiring предава сървиса на всеки параметър на метод, чийто тип съответства на сървиса. Стесняването означава, че задаваме условия, на които трябва да отговарят типовете, посочени в параметрите на методите, за да им бъде предаден сървисът. - -Ще го покажем с пример: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Ако ги регистрираме всички като сървиси, autowiring ще се провали: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # ХВЪРЛЯ ИЗКЛЮЧЕНИЕ, отговарят сървисите parent и child - childDep: ChildDependent # autowiring предава сървиса child на конструктора -``` - -Сървисът `parentDep` ще хвърли изключение `Multiple services of type ParentClass found: parent, child`, тъй като и двата сървиса `parent` и `child` отговарят на конструктора му, и autowiring не може да реши кой от тях да избере. - -Затова можем да стесним autowiring-а на сървиса `child` до тип `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # може да се напише и 'autowired: self' - - parentDep: ParentDependent # autowiring предава сървиса parent на конструктора - childDep: ChildDependent # autowiring предава сървиса child на конструктора -``` - -Сега на конструктора на сървиса `parentDep` се предава сървисът `parent`, защото сега той е единственият подходящ обект. Autowiring вече не предава сървиса `child` там. Да, сървисът `child` все още е от тип `ParentClass`, но стесняващото условие, зададено за типа на параметъра, вече не е валидно, т.е. не е вярно, че `ParentClass` *е надтип на* `ChildClass`. - -При сървиса `child` би било възможно `autowired: ChildClass` да се запише и като `autowired: self`, тъй като `self` е заместващо означение за класа на текущия сървис. - -В ключа `autowired` е възможно да се посочат и няколко класа или интерфейса като масив: - -```neon -autowired: [BarClass, FooInterface] -``` - -Нека допълним примера и с интерфейси: - -```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) - {} -} -``` - -Ако не ограничим сървиса `child` по никакъв начин, той ще пасне на конструкторите на всички класове `FooDependent`, `BarDependent`, `ParentDependent` и `ChildDependent` и autowiring ще го предаде там. - -Но ако стесним неговия autowiring до `ChildClass` с помощта на `autowired: ChildClass` (или `self`), autowiring ще го предаде само на конструктора на `ChildDependent`, тъй като той изисква аргумент от тип `ChildClass` и е вярно, че `ChildClass` *е от тип* `ChildClass`. Никой друг тип, посочен в другите параметри, не е надтип на `ChildClass`, така че сървисът не се предава. - -Ако го ограничим до `ParentClass` с помощта на `autowired: ParentClass`, autowiring отново ще го предаде на конструктора на `ChildDependent` (тъй като изискваният `ChildClass` е надтип на `ParentClass`), а също и на конструктора на `ParentDependent`, тъй като изискваният тип `ParentClass` също е подходящ. - -Ако го ограничим до `FooInterface`, той все още ще бъде автоматично инжектиран в `ParentDependent` (изискваният `ParentClass` е надтип на `FooInterface`) и `ChildDependent`, но освен това и в конструктора на `FooDependent`, но не и в `BarDependent`, тъй като `BarInterface` не е надтип на `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring предава child на конструктора - barDep: BarDependent # ХВЪРЛЯ ИЗКЛЮЧЕНИЕ, нито един сървис не отговаря - parentDep: ParentDependent # autowiring предава child на конструктора - childDep: ChildDependent # autowiring предава child на конструктора -``` diff --git a/dependency-injection/bg/configuration.texy b/dependency-injection/bg/configuration.texy deleted file mode 100644 index 3f6bf580ca..0000000000 --- a/dependency-injection/bg/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Конфигурация на DI контейнера -***************************** - -.[perex] -Преглед на опциите за конфигурация на Nette DI контейнера. - - -Конфигурационен файл -==================== - -Nette DI контейнерът се управлява лесно с помощта на конфигурационни файлове. Те обикновено се записват във [формат NEON|neon:format]. За редактиране препоръчваме [редактори с поддръжка |best-practices:editors-and-tools#IDE редактор] на този формат. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Декоратор .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI контейнер .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Разширения]: "Инсталиране на други DI разширения .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Включване на файлове]: "Включване на файлове .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Параметри]: "Параметри .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Автоматично регистриране на сървиси .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Сървиси .[prism-token prism-comment]" -</pre> - -.[note] -Ако искате да напишете низ, съдържащ знака `%`, трябва да го екранирате, като го удвоите на `%%`. - - -Параметри -========= - -В конфигурацията можете да дефинирате параметри, които след това могат да се използват като част от дефинициите на сървисите. Това може да направи конфигурацията по-ясна или да обедини и изолира стойности, които ще се променят. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Към параметъра `dsn` се обръщаме навсякъде в конфигурацията, като напишем `%dsn%`. Параметрите могат да се използват и в низове като `'%wwwDir%/images'`. - -Параметрите не трябва да бъдат само низове или числа, те могат да съдържат и масиви: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Към конкретен ключ се обръщаме като `%mailer.user%`. - -Ако трябва да разберете стойността на който и да е параметър във вашия код, например в клас, предайте го на този клас. Например в конструктора. Няма глобален обект, представляващ конфигурацията, към който класовете да се обръщат за стойности на параметри. Това би било нарушение на принципа на dependency injection. - - -Сървиси -======= - -Вижте [отделна глава|services]. - - -Decorator -========= - -Как да модифицирате масово всички сървиси от определен тип? Например, да извикате определен метод за всички презентери, които наследяват от конкретен общ предшественик? За това служи декораторът. - -```neon -decorator: - # за всички сървиси, които са инстанции на този клас или интерфейс - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # извикайте този метод - - $absoluteUrls = true # и задайте променливата -``` - -Decorator може да се използва и за задаване на [тагове |services#Тагове] или за активиране на режим [inject |services#Режим Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Технически настройки на DI контейнера. - -```neon -di: - # показва ли се DIC в Tracy Bar? - debugger: ... # (bool) по подразбиране е true - - # типове параметри, които никога да не се autowire-ват - excluded: ... # (string[]) - - # разрешава ли се lazy създаване на сървиси? - lazy: ... # (bool) по подразбиране е false - - # клас, от който наследява DI контейнерът - parentClass: ... # (string) по подразбиране е Nette\DI\Container -``` - - -Lazy сървиси .{data-version:3.2.4} ----------------------------------- - -Настройката `lazy: true` активира lazy (отложено) създаване на сървиси. Това означава, че сървисите не се създават реално в момента, в който ги поискаме от DI контейнера, а едва в момента на първото им използване. Това може да ускори стартирането на приложението и да намали изискванията за памет, тъй като се създават само тези сървиси, които са действително необходими в дадена заявка. - -За конкретен сървис lazy създаването може да бъде [променено |services#Lazy сървиси]. - -.[note] -Lazy обектите могат да се използват само за потребителски класове, а не за вътрешни PHP класове. Изисква PHP 8.4 или по-нова версия. - - -Експортиране на метаданни -------------------------- - -Класът на DI контейнера съдържа и много метаданни. Можете да го намалите, като редуцирате експорта на метаданни. - -```neon -di: - export: - # експортиране на параметри? - parameters: false # (bool) по подразбиране е true - - # експортиране на тагове и кои? - tags: # (string[]|bool) по подразбиране са всички - - event.subscriber - - # експортиране на данни за autowiring и кои? - types: # (string[]|bool) по подразбиране са всички - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Ако не използвате масива `$container->getParameters()`, можете да изключите експорта на параметри. Освен това можете да експортирате само тези тагове, чрез които получавате сървиси с метода `$container->findByTag(...)`. Ако изобщо не извиквате метода, можете напълно да изключите експорта на тагове с `false`. - -Можете значително да намалите метаданните за [autowiring |autowiring] , като посочите класовете, които използвате като параметър на метода `$container->getByType()`. И отново, ако изобщо не извиквате метода (или само в [bootstrap|application:bootstrapping], за да получите `Nette\Application\Application`), можете напълно да изключите експорта с `false`. - - -Разширения -========== - -Регистриране на други DI разширения. По този начин добавяме например DI разширението `Dibi\Bridges\Nette\DibiExtension22` под името `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -След това го конфигурираме в секцията `dibi`: - -```neon -dibi: - host: localhost -``` - -Като разширение може да се добави и клас, който има параметри: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Включване на файлове -==================== - -Можем да включим други конфигурационни файлове в секцията `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Името `parameters.php` не е печатна грешка, конфигурацията може да бъде записана и в PHP файл, който я връща като масив: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Ако в конфигурационните файлове се появят елементи с еднакви ключове, те ще бъдат презаписани или, в случай на [масиви, слети |#Сливане]. Файлът, включен по-късно, има по-висок приоритет от предишния. Файлът, в който е посочена секцията `includes`, има по-висок приоритет от файловете, включени в него. - - -Search -====== - -Автоматичното добавяне на сървиси към DI контейнера прави работата изключително приятна. Nette автоматично добавя презентери към контейнера, но можете лесно да добавяте и всякакви други класове. - -Достатъчно е да посочите в кои директории (и поддиректории) да търси класове: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Обикновено обаче не искаме да добавяме абсолютно всички класове и интерфейси, така че можем да ги филтрираме: - -```neon -search: - - in: %appDir%/Forms - - # филтриране по име на файл (string|string[]) - files: - - *Factory.php - - # филтриране по име на клас (string|string[]) - classes: - - *Factory -``` - -Или можем да изберем класове, които наследяват или имплементират поне един от изброените класове: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Могат да се дефинират и изключващи правила, т.е. маски на имена на класове или наследствени предци, които, ако съвпадат, сървисът няма да бъде добавен към DI контейнера: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -На всички сървиси могат да се зададат тагове: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Сливане -======= - -Ако в няколко конфигурационни файла се появят елементи с еднакви ключове, те ще бъдат презаписани или, в случай на масиви, слети. Файлът, включен по-късно, има по-висок приоритет от предишния. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>резултат</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -При масивите сливането може да бъде предотвратено чрез добавяне на удивителен знак след името на ключа: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>резултат</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Конфигурация на Dependency Injection}} diff --git a/dependency-injection/bg/container.texy b/dependency-injection/bg/container.texy deleted file mode 100644 index dc3ae58a36..0000000000 --- a/dependency-injection/bg/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Какво е DI контейнер? -********************* - -.[perex] -Dependency injection контейнерът (DIC) е клас, който може да инстанцира и конфигурира обекти. - -Може да ви изненада, но в много случаи не се нуждаете от dependency injection контейнер, за да се възползвате от предимствата на dependency injection (накратко DI). В края на краищата, дори в [уводната глава|introduction] показахме DI с конкретни примери и не беше необходим контейнер. - -Въпреки това, ако трябва да управлявате голям брой различни обекти с много зависимости, dependency injection контейнерът ще бъде наистина полезен. Такъв е случаят например с уеб приложения, изградени върху framework. - -В предишната глава представихме класовете `Article` и `UserController`. И двата имат някои зависимости, а именно база данни и фабриката `ArticleFactory`. И сега ще създадем контейнер за тези класове. Разбира се, за толкова прост пример няма смисъл да имаме контейнер. Но ще го създадем, за да покажем как изглежда и работи. - -Ето един прост hardcoded контейнер за дадения пример: - -```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()); - } -} -``` - -Използването би изглеждало така: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Просто питаме контейнера за обект и вече не е нужно да знаем нищо за това как да го създадем или какви са неговите зависимости; контейнерът знае всичко това. Зависимостите се инжектират автоматично от контейнера. В това е неговата сила. - -Засега контейнерът има всички данни, записани hardcoded. Така че ще направим следващата стъпка и ще добавим параметри, за да направим контейнера наистина полезен: - -```php -class Container -{ - public function __construct( - private array $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' => '***', -]); -``` - -Наблюдателните читатели може би са забелязали определен проблем. Всеки път, когато получа обект `UserController`, се създава и нова инстанция на `ArticleFactory` и базата данни. Определено не искаме това. - -Затова ще добавим метод `getService()`, който винаги ще връща едни и същи инстанции: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') ще извика createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -При първото извикване, например `$container->getService('Database')`, той ще накара `createDatabase()` да създаде обект на базата данни, ще го съхрани в масива `$services` и ще го върне директно при следващото извикване. - -Ще модифицираме и останалата част от контейнера, за да използва `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Между другото, терминът сървис се отнася до всеки обект, управляван от контейнера. Оттук и името на метода `getService()`. - -Готово. Имаме напълно функционален DI контейнер! И можем да го използваме: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Както виждате, написването на DIC не е сложно. Струва си да се отбележи, че самите обекти не знаят, че се създават от някакъв контейнер. Следователно е възможно да се създаде по този начин всеки PHP обект, без да се променя неговият изходен код. - -Ръчното създаване и поддръжка на клас контейнер може бързо да се превърне в кошмар. Затова в следващата глава ще говорим за [Nette DI Container|nette-container], който може да се генерира и актуализира почти сам. - - -{{maintitle: Какво е dependency injection контейнер?}} diff --git a/dependency-injection/bg/extensions.texy b/dependency-injection/bg/extensions.texy deleted file mode 100644 index d38bb6e373..0000000000 --- a/dependency-injection/bg/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Създаване на разширения за Nette DI -*********************************** - -.[perex] -Генерирането на DI контейнера, освен от конфигурационните файлове, се влияе и от така наречените *разширения*. Активираме ги в конфигурационния файл в секцията `extensions`. - -По този начин добавяме разширение, представено от класа `BlogExtension`, под името `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Всяко разширение на компилатора наследява от [api:Nette\DI\CompilerExtension] и може да имплементира следните методи, които се извикват последователно по време на изграждането на DI контейнера: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Този метод се извиква пръв. Той дефинира схема за валидиране на конфигурационните параметри. - -Конфигурираме разширението в секция, чието име е същото като това, под което е добавено разширението, т.е. `blog`: - -```neon -# същото име като разширението -blog: - postsPerPage: 10 - allowComments: false -``` - -Създаваме схема, описваща всички опции за конфигурация, включително техните типове, разрешени стойности и евентуално стойности по подразбиране: - -```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), - ]); - } -} -``` - -Документацията можете да намерите на страницата [Schema |schema:]. Освен това можете да посочите кои опции могат да бъдат [динамични |application:bootstrapping#Динамични параметри] с помощта на `dynamic()`, напр. `Expect::int()->dynamic()`. - -Достъпваме конфигурацията чрез променливата `$this->config`, която е обект `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Използва се за добавяне на сървиси към контейнера. За това служи [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']) // или setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -Конвенцията е сървисите, добавени от разширение, да се префиксират с неговото име, за да се избегнат конфликти на имена. Това прави методът `prefix()`, така че ако разширението се нарича `blog`, сървисът ще носи името `blog.articles`. - -Ако трябва да преименуваме сървис, можем да създадем псевдоним с оригиналното име, за да запазим обратната съвместимост. Nette прави нещо подобно, например със сървиса `routing.router`, който е достъпен и под предишното име `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Зареждане на сървиси от файл ----------------------------- - -Не е необходимо да създаваме сървиси само с помощта на API на класа ContainerBuilder, но и с познатия синтаксис, използван в конфигурационния файл NEON в секцията services. Префиксът `@extension` представлява текущото разширение. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Зареждаме сървисите: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // зареждане на конфигурационния файл за разширението - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Методът се извиква, когато контейнерът съдържа всички сървиси, добавени от отделните разширения в методите `loadConfiguration`, както и от потребителските конфигурационни файлове. Следователно на този етап от изграждането можем да модифицираме дефинициите на сървисите или да добавим връзки между тях. За търсене на сървиси в контейнера по тагове може да се използва методът `findByTag()`, а по клас или интерфейс - методът `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] -======================== - -На този етап класът на контейнера вече е генериран под формата на обект [ClassType |php-generator:#Класове], съдържа всички методи, които създават сървиси, и е готов за запис в кеша. Все още можем да модифицираме получения код на класа на този етап. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -Класът Configurator, след [създаване на контейнера |application:bootstrapping#index.php], извиква инициализационен код, който се създава чрез запис в обекта `$this->initialization` с помощта на [метода addBody() |php-generator:#Тела на методи и функции]. - -Ще покажем пример как да стартирате сесия или да стартирате сървиси, които имат таг `run`, с помощта на инициализационен код: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // автоматично стартиране на сесията - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // сървисите с таг run трябва да бъдат създадени след инстанциране на контейнера - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/bg/factory.texy b/dependency-injection/bg/factory.texy deleted file mode 100644 index eec7b3abf4..0000000000 --- a/dependency-injection/bg/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Генерирани фабрики -****************** - -.[perex] -Nette DI може автоматично да генерира код на фабрики въз основа на интерфейси, което ви спестява писане на код. - -Фабриката е клас, който произвежда и конфигурира обекти. Следователно тя им предава и техните зависимости. Моля, не бъркайте с дизайн патърна *factory method*, който описва специфичен начин за използване на фабрики и не е свързан с тази тема. - -Как изглежда такава фабрика, показахме в [уводната глава |introduction#Фабрика]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI може автоматично да генерира код на фабрики. Всичко, което трябва да направите, е да създадете интерфейс и Nette DI ще генерира имплементацията. Интерфейсът трябва да има точно един метод с име `create` и да декларира тип на връщане: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Така фабриката `ArticleFactory` има метод `create`, който създава обекти `Article`. Класът `Article` може да изглежда например така: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Добавяме фабриката към конфигурационния файл: - -```neon -services: - - ArticleFactory -``` - -Nette DI ще генерира съответната имплементация на фабриката. - -В кода, който използва фабриката, изискваме обект по интерфейс и Nette DI ще използва генерираната имплементация: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // оставяме фабриката да създаде обект - $article = $this->articleFactory->create(); - } -} -``` - - -Параметризирана фабрика -======================= - -Фабричният метод `create` може да приема параметри, които след това предава на конструктора. Нека добавим например ID на автора на статията към класа `Article`: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Добавяме параметъра и към фабриката: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Тъй като параметърът в конструктора и параметърът във фабриката имат едно и също име, Nette DI ги предава напълно автоматично. - - -Разширена дефиниция -=================== - -Дефиницията може да бъде записана и в многоредов вид, като се използва ключът `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -При писане по този по-дълъг начин е възможно да се посочат допълнителни аргументи за конструктора в ключа `arguments` и допълнителна конфигурация с помощта на `setup`, точно както при обикновените сървиси. - -Пример: ако методът `create()` не приемаше параметъра `$authorId`, бихме могли да посочим фиксирана стойност в конфигурацията, която да бъде предадена на конструктора на `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Или обратно, ако `create()` приемаше параметъра `$authorId`, но той не беше част от конструктора и се предаваше чрез метода `Article::setAuthorId()`, щяхме да се обърнем към него в секцията `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Освен фабрики, Nette може да генерира и т.нар. аксесори. Това са обекти с метод `get()`, който връща определен сървис от DI контейнера. Повторното извикване на `get()` винаги връща същата инстанция. - -Аксесорите осигуряват lazy-loading за зависимостите. Да приемем, че имаме клас, който записва грешки в специална база данни. Ако този клас получаваше връзката с базата данни като зависимост чрез конструктора, връзката винаги трябваше да се създава, въпреки че на практика грешка се появява само рядко и следователно връзката в повечето случаи би останала неизползвана. Вместо това класът получава аксесор и едва когато се извика неговият `get()`, се създава обектът на базата данни: - -Как да създадем аксесор? Просто напишете интерфейс и Nette DI ще генерира имплементацията. Интерфейсът трябва да има точно един метод с име `get` и да декларира тип на връщане: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Добавяме аксесора към конфигурационния файл, където е и дефиницията на сървиса, който той ще връща: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Тъй като аксесорът връща сървис от тип `PDO` и в конфигурацията има само един такъв сървис, той ще върне точно него. Ако имаше повече сървиси от този тип, щяхме да посочим връщания сървис по име, напр. `- PDOAccessor(@db1)`. - - -Множествена фабрика/аксесор -=========================== -Досега нашите фабрики и аксесори винаги са можели да произвеждат или връщат само един обект. Въпреки това е много лесно да се създадат и множествени фабрики, комбинирани с аксесори. Интерфейсът на такъв клас ще съдържа произволен брой методи с имена `create<name>()` и `get<name>()`, напр.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Така че, вместо да предаваме няколко генерирани фабрики и аксесори, предаваме една по-сложна фабрика, която може да прави повече неща. - -Алтернативно, вместо няколко метода, може да се използва `get()` с параметър: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Тогава `MultiFactory::getArticle()` прави същото като `MultiFactoryAlt::get('article')`. Въпреки това, алтернативният запис има недостатъка, че не е ясно кои стойности на `$name` се поддържат и логично не е възможно да се разграничат различни върнати стойности за различни `$name` в интерфейса. - - -Дефиниция чрез списък ---------------------- -По този начин може да се дефинира множествена фабрика в конфигурацията: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # дефинира createArticle() - db: PDO(%dsn%, %user%, %password%) # дефинира getDb() - ) -``` - -Или можем да се обърнем към съществуващи сървиси в дефиницията на фабриката чрез референция: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # дефинира createArticle() - db: @\PDO # дефинира getDb() - ) -``` - - -Дефиниция с помощта на тагове ------------------------------ - -Втората възможност е да се използват [тагове |services#Тагове] за дефиницията: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/bg/faq.texy b/dependency-injection/bg/faq.texy deleted file mode 100644 index 9c77b0168d..0000000000 --- a/dependency-injection/bg/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Често задавани въпроси за DI (FAQ) -********************************** - - -DI ли е друго име за IoC? -------------------------- - -*Inversion of Control* (IoC) е принцип, фокусиран върху начина, по който се изпълнява кодът - дали вашият код изпълнява чужд код, или вашият код е интегриран в чужд код, който след това го извиква. IoC е широк термин, обхващащ [събития |nette:glossary#Събития events], така наречения [Холивудски принцип |application:components#Hollywood style] и други аспекти. Част от тази концепция са и фабриките, за които се говори в [Правило № 3: оставете го на фабриката |introduction#Правило 3: Остави го на фабриката], и които представляват инверсия за оператора `new`. - -*Dependency Injection* (DI) се фокусира върху начина, по който един обект научава за друг обект, т.е. за неговите зависимости. Това е дизайн патърн, който изисква изрично предаване на зависимости между обектите. - -Следователно може да се каже, че DI е специфична форма на IoC. Въпреки това, не всички форми на IoC са подходящи от гледна точка на чистотата на кода. Например, анти-патърните включват техники, които работят с [глобално състояние |global-state] или така наречения [Service Locator |#Какво е Service Locator]. - - -Какво е Service Locator? ------------------------- - -Това е алтернатива на Dependency Injection. Работи, като създава централно хранилище, където са регистрирани всички налични сървиси или зависимости. Когато обект се нуждае от зависимост, той я иска от Service Locator. - -В сравнение с Dependency Injection обаче, той губи прозрачност: зависимостите не се предават директно на обектите и не са толкова лесно идентифицируеми, което изисква преглед на кода, за да се разкрият и разберат всички връзки. Тестването също е по-сложно, тъй като не можем просто да предаваме mock обекти на тестваните обекти, а трябва да го правим чрез Service Locator. Освен това, Service Locator нарушава дизайна на кода, тъй като отделните обекти трябва да знаят за неговото съществуване, което е различно от Dependency Injection, където обектите нямат представа за DI контейнера. - - -Кога е по-добре да не се използва DI? -------------------------------------- - -Не са известни трудности, свързани с използването на дизайн патърна Dependency Injection. Напротив, получаването на зависимости от глобално достъпни места води до [цяла поредица от усложнения |global-state], както и използването на Service Locator. Затова е препоръчително винаги да се използва DI. Това не е догматичен подход, а просто не е намерена по-добра алтернатива. - -Въпреки това съществуват определени ситуации, в които не предаваме обекти, а ги получаваме от глобалното пространство. Например, при дебъгване на код, когато трябва да изведете стойността на променлива в определена точка от програмата, да измерите продължителността на определена част от програмата или да запишете съобщение. В такива случаи, когато става въпрос за временни действия, които по-късно ще бъдат премахнати от кода, е легитимно да се използва глобално достъпен dumper, хронометър или logger. Тези инструменти всъщност не принадлежат към дизайна на кода. - - -Има ли използването на DI своите недостатъци? ---------------------------------------------- - -Носи ли използването на Dependency Injection някакви недостатъци, като например повишена сложност при писане на код или влошена производителност? Какво губим, когато започнем да пишем код в съответствие с DI? - -DI не влияе на производителността или изискванията за памет на приложението. Производителността на DI Container-а може да играе известна роля, но в случая на [Nette DI |nette-container], контейнерът се компилира в чист PHP, така че неговата режия по време на изпълнение на приложението е практически нулева. - -При писане на код често е необходимо да се създават конструктори, приемащи зависимости. Преди това можеше да бъде досадно, но благодарение на модерните IDE и [constructor property promotion |https://blog.nette.org/bg/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], сега това е въпрос на няколко секунди. Фабриките могат лесно да се генерират с помощта на Nette DI и плъгин за PhpStorm с едно кликване на мишката. От друга страна, отпада необходимостта от писане на сингълтъни и статични точки за достъп. - -Може да се каже, че правилно проектирано приложение, използващо DI, не е нито по-кратко, нито по-дълго в сравнение с приложение, използващо сингълтъни. Частите от кода, работещи със зависимости, са просто извадени от отделните класове и преместени на нови места, т.е. в DI контейнера и фабриките. - - -Как да пренапишем legacy приложение към DI? -------------------------------------------- - -Преходът от legacy приложение към Dependency Injection може да бъде предизвикателен процес, особено при големи и сложни приложения. Важно е да се подходи към този процес систематично. - -- При преминаване към Dependency Injection е важно всички членове на екипа да разбират принципите и процедурите, които се използват. -- Първо, направете анализ на съществуващото приложение и идентифицирайте ключовите компоненти и техните зависимости. Създайте план кои части ще бъдат рефакторирани и в какъв ред. -- Имплементирайте DI контейнер или още по-добре, използвайте съществуваща библиотека, например Nette DI. -- Постепенно рефакторирайте отделните части на приложението, за да използват Dependency Injection. Това може да включва промени в конструкторите или методите, така че да приемат зависимости като параметри. -- Модифицирайте местата в кода, където се създават обекти със зависимости, така че вместо това зависимостите да се инжектират от контейнера. Това може да включва използването на фабрики. - -Помнете, че преходът към Dependency Injection е инвестиция в качеството на кода и дългосрочната поддръжка на приложението. Въпреки че може да е предизвикателство да се направят тези промени, резултатът трябва да бъде по-чист, по-модулен и лесно тестваем код, който е готов за бъдещи разширения и поддръжка. - - -Защо се предпочита композиция пред наследяването? -------------------------------------------------- -По-подходящо е да се използва [композиция |nette:introduction-to-object-oriented-programming#Композиция] вместо [наследяване |nette:introduction-to-object-oriented-programming#Наследяване], тъй като тя служи за повторно използване на код, без да се налага да се притесняваме за последствията от промените. Следователно тя осигурява по-слаба връзка, при която не трябва да се притесняваме, че промяната на някой код ще доведе до необходимост от промяна на друг зависим код. Типичен пример е ситуацията, наречена [constructor hell |passing-dependencies#Адът на конструктора]. - - -Може ли да се използва Nette DI Container извън Nette? ------------------------------------------------------- - -Определено. Nette DI Container е част от Nette, но е проектиран като самостоятелна библиотека, която може да се използва независимо от другите части на framework-а. Просто го инсталирайте с помощта на Composer, създайте конфигурационен файл с дефиницията на вашите сървиси и след това използвайте няколко реда PHP код, за да създадете DI контейнера. И веднага можете да започнете да се възползвате от предимствата на Dependency Injection във вашите проекти. - -Как изглежда конкретното използване, включително кодове, е описано в главата [Nette DI Container |nette-container]. - - -Защо е конфигурацията в NEON файлове? -------------------------------------- - -NEON е прост и лесен за четене конфигурационен език, разработен в рамките на Nette за настройка на приложения, сървиси и техните зависимости. В сравнение с JSON или YAML, той предлага много по-интуитивни и гъвкави опции за тази цел. В NEON могат естествено да се опишат връзки, които в Symfony & YAMLu би било невъзможно да се запишат изобщо или само чрез сложно описание. - - -Не забавя ли приложението парсването на NEON файлове? ------------------------------------------------------ - -Въпреки че NEON файловете се парсват много бързо, на този аспект изобщо няма значение. Причината е, че парсването на файловете се извършва само веднъж при първото стартиране на приложението. След това се генерира кодът на DI контейнера, записва се на диска и се изпълнява при всяка следваща заявка, без да е необходимо допълнително парсване. - -Така работи в продукционна среда. По време на разработка NEON файловете се парсват всеки път, когато съдържанието им се промени, така че разработчикът винаги да има актуален DI контейнер. Самото парсване е, както беше споменато, въпрос на момент. - - -Как да получа достъп до параметрите в конфигурационния файл от моя клас? ------------------------------------------------------------------------- - -Нека си припомним [Правило № 1: нека ти го предадат |introduction#Правило 1: Нека ви го предадат]. Ако класът изисква информация от конфигурационния файл, не е нужно да мислим как да стигнем до тази информация, вместо това просто я искаме - например чрез конструктора на класа. И осъществяваме предаването в конфигурационния файл. - -В този пример `%myParameter%` е placeholder за стойността на параметъра `myParameter`, която се предава на конструктора на класа `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Ако искате да предавате повече параметри или да използвате autowiring, е препоръчително [да опаковате параметрите в обект |best-practices:passing-settings-to-presenters]. - - -Поддържа ли Nette PSR-11: Container interface? ----------------------------------------------- - -Nette DI Container не поддържа директно PSR-11. Въпреки това, ако се нуждаете от оперативна съвместимост между Nette DI Container-а и библиотеки или framework-ове, които очакват PSR-11 Container Interface, можете да създадете [прост адаптер |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], който ще служи като мост между Nette DI Container-а и PSR-11. diff --git a/dependency-injection/bg/global-state.texy b/dependency-injection/bg/global-state.texy deleted file mode 100644 index b4bf3be4e8..0000000000 --- a/dependency-injection/bg/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Глобално състояние и сингълтъни -******************************* - -.[perex] -Предупреждение: Следните конструкции са признак на лошо проектиран код: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` или `static::$var` - -Срещат ли се някои от тези конструкции във вашия код? Тогава имате възможност да го подобрите. Може би си мислите, че това са обичайни конструкции, които виждате дори в примерни решения на различни библиотеки и framework-ове. Ако е така, тогава дизайнът на техния код не е добър. - -Сега определено не говорим за някаква академична чистота. Всички тези конструкции имат едно общо нещо: те използват глобално състояние. А то има разрушителен ефект върху качеството на кода. Класовете лъжат за своите зависимости. Кодът става непредсказуем. Обърква програмистите и намалява тяхната ефективност. - -В тази глава ще обясним защо е така и как да избегнем глобалното състояние. - - -Глобална свързаност -------------------- - -В идеалния свят обектът трябва да може да комуникира само с обекти, които са му били [директно предадени |passing-dependencies]. Ако създам два обекта `A` и `B` и никога не предам референция между тях, тогава нито `A`, нито `B` могат да достигнат до другия обект или да променят неговото състояние. Това е много желана характеристика на кода. Подобно е на това да имате батерия и крушка; крушката няма да свети, докато не я свържете с батерията с проводник. - -Но това не важи за глобални (статични) променливи или сингълтъни. Обект `A` може *безжично* да достигне до обект `C` и да го модифицира без никакво предаване на референция, като извика `C::changeSomething()`. Ако обект `B` също се възползва от глобалния `C`, тогава `A` и `B` могат да си влияят взаимно чрез `C`. - -Използването на глобални променливи въвежда нова форма на *безжична* свързаност в системата, която не се вижда отвън. Създава димна завеса, усложняваща разбирането и използването на кода. За да разберат наистина зависимостите, разработчиците трябва да прочетат всеки ред от изходния код. Вместо просто да се запознаят с интерфейсите на класовете. Освен това става дума за напълно ненужна свързаност. Глобалното състояние се използва, защото е лесно достъпно отвсякъде и позволява например запис в базата данни чрез глобален (статичен) метод `DB::insert()`. Но както ще покажем, предимството, което носи, е незначително, докато усложненията, които причинява, са фатални. - -.[note] -От гледна точка на поведението няма разлика между глобална и статична променлива. Те са еднакво вредни. - - -Призрачно действие от разстояние --------------------------------- - -"Призрачно действие от разстояние" - така Алберт Айнщайн нарича през 1935 г. явление в квантовата физика, което го кара да настръхне. -Става дума за квантово заплитане, чиято особеност е, че когато измерите информация за една частица, веднага повлиявате на другата частица, дори ако те са на милиони светлинни години една от друга. Което привидно нарушава основния закон на Вселената, че нищо не може да се разпространява по-бързо от светлината. - -В света на софтуера можем да наречем "призрачно действие от разстояние" ситуация, при която стартираме някакъв процес, за който смятаме, че е изолиран (защото не сме му предали никакви референции), но на отдалечени места в системата възникват неочаквани взаимодействия и промени в състоянието, за които не сме подозирали. Това може да се случи само чрез глобално състояние. - -Представете си, че се присъединявате към екип от разработчици на проект, който има голяма, зряла кодова база. Новият ви ръководител ви моли да имплементирате нова функция и вие, като добър разработчик, започвате с писане на тест. Но тъй като сте нов в проекта, правите много проучвателни тестове от типа "какво ще се случи, ако извикам този метод". И опитвате да напишете следния тест: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // номер на вашата карта - $cc->charge(100); -} -``` - -Изпълнявате кода, може би няколко пъти, и след известно време забелязвате известия от банката на мобилния си телефон, че при всяко стартиране са били изтеглени 100 долара от вашата кредитна карта 🤦‍♂️ - -Как, за бога, тестът може да е причинил реално теглене на пари? Работата с кредитна карта не е лесна. Трябва да комуникирате с уеб услуга на трета страна, трябва да знаете URL адреса на тази уеб услуга, трябва да влезете и т.н. Нито една от тази информация не се съдържа в теста. Още по-лошо, дори не знаете къде се намира тази информация и следователно как да mock-нете външните зависимости, така че всяко стартиране да не води до повторно теглене на 100 долара. И как вие, като нов разработчик, трябваше да знаете, че това, което се каните да направите, ще доведе до това да сте с 100 долара по-беден? - -Това е призрачно действие от разстояние! - -Не ви остава нищо друго, освен дълго да ровите в много изходни кодове, да питате по-стари и по-опитни колеги, докато разберете как работят връзките в проекта. Това се дължи на факта, че при разглеждане на интерфейса на класа `CreditCard` не може да се установи глобалното състояние, което трябва да се инициализира. Дори поглед към изходния код на класа няма да ви каже кой инициализационен метод трябва да извикате. В най-добрия случай можете да намерите глобална променлива, до която се осъществява достъп, и от нея да се опитате да отгатнете как да я инициализирате. - -Класовете в такъв проект са патологични лъжци. Кредитната карта се преструва, че е достатъчно да я инстанцирате и да извикате метода `charge()`. Но тайно тя си сътрудничи с друг клас `PaymentGateway`, който представлява платежен портал. Неговият интерфейс също казва, че може да се инициализира самостоятелно, но всъщност извлича идентификационни данни от някакъв конфигурационен файл и т.н. За разработчиците, които са написали този код, е ясно, че `CreditCard` се нуждае от `PaymentGateway`. Те са написали кода по този начин. Но за всеки нов в проекта това е пълна загадка и пречи на ученето. - -Как да поправим ситуацията? Лесно. **Нека API декларира зависимостите.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Забележете как изведнъж взаимовръзките в кода стават очевидни. Тъй като методът `charge()` декларира, че се нуждае от `PaymentGateway`, не е нужно да питате никого как е свързан кодът. Знаете, че трябва да създадете негова инстанция и когато се опитате да го направите, ще откриете, че трябва да предоставите параметри за достъп. Без тях кодът дори не би могъл да се изпълни. - -И най-важното, сега можете да mock-нете платежния портал, така че няма да ви бъдат таксувани 100 долара всеки път, когато стартирате теста. - -Глобалното състояние кара вашите обекти да имат таен достъп до неща, които не са декларирани в техните API, и в резултат на това превръща вашите API в патологични лъжци. - -Може би не сте мислили за това по този начин преди, но всеки път, когато използвате глобално състояние, създавате тайни безжични комуникационни канали. Призрачното действие от разстояние принуждава разработчиците да четат всеки ред код, за да разберат потенциалните взаимодействия, намалява производителността на разработчиците и обърква новите членове на екипа. Ако вие сте този, който е създал кода, познавате истинските зависимости, но всеки, който дойде след вас, е безпомощен. - -Не пишете код, който използва глобално състояние, предпочитайте предаването на зависимости. Тоест dependency injection. - - -Крехкост на глобалното състояние --------------------------------- - -В код, който използва глобално състояние и сингълтъни, никога не е сигурно кога и кой е променил това състояние. Този риск се появява още при инициализацията. Следният код трябва да създаде връзка с база данни и да инициализира платежен портал, но постоянно хвърля изключение и намирането на причината е изключително досадно: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Трябва подробно да прегледате кода, за да установите, че обектът `PaymentGateway` осъществява безжичен достъп до други обекти, някои от които изискват връзка с база данни. Следователно е необходимо да се инициализира базата данни преди `PaymentGateway`. Въпреки това, димната завеса на глобалното състояние скрива това от вас. Колко време бихте спестили, ако API-тата на отделните класове не лъжеха и декларираха своите зависимости? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Подобен проблем възниква и при използване на глобален достъп до връзката с базата данни: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -При извикване на метода `save()` не е сигурно дали вече е създадена връзка с базата данни и кой носи отговорност за нейното създаване. Ако искаме например да променяме връзката с базата данни по време на изпълнение, например за тестове, вероятно ще трябва да създадем допълнителни методи като `DB::reconnect(...)` или `DB::reconnectForTest()`. - -Да разгледаме пример: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Къде имаме сигурност, че при извикване на `$article->save()` наистина се използва тестовата база данни? Ами ако методът `Foo::doSomething()` е променил глобалната връзка с базата данни? За да разберем, ще трябва да проучим изходния код на класа `Foo` и вероятно на много други класове. Този подход обаче би донесъл само краткосрочен отговор, тъй като ситуацията може да се промени в бъдеще. - -Ами ако преместим връзката с базата данни в статична променлива вътре в класа `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -С това нищо не се промени. Проблемът е глобалното състояние и няма никакво значение в кой клас се крие. В този случай, както и в предишния, при извикване на метода `$article->save()` нямаме никаква представа в коя база данни ще се запише. Всеки от другия край на приложението може по всяко време да промени базата данни с помощта на `Article::setDb()`. Под носа ни. - -Глобалното състояние прави нашето приложение **изключително крехко**. - -Съществува обаче прост начин за справяне с този проблем. Достатъчно е да оставим API да декларира зависимостите, което ще гарантира правилната функционалност. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Благодарение на този подход отпада притеснението за скрити и неочаквани промени във връзката с базата данни. Сега имаме сигурност къде се съхранява статията и никакви промени в кода в друг несвързан клас вече не могат да променят ситуацията. Кодът вече не е крехък, а стабилен. - -Не пишете код, който използва глобално състояние, предпочитайте предаването на зависимости. Тоест dependency injection. - - -Singleton ---------- - -Singleton е дизайн патърн, който според "дефиницията":https://en.wikipedia.org/wiki/Singleton_pattern от известната публикация на Gang of Four ограничава класа до една единствена инстанция и предлага глобален достъп до нея. Имплементацията на този патърн обикновено прилича на следния код: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // и други методи, изпълняващи функциите на дадения клас -} -``` - -За съжаление, сингълтънът въвежда глобално състояние в приложението. И както показахме по-горе, глобалното състояние е нежелателно. Затова сингълтънът се счита за антипатърн. - -Не използвайте сингълтъни във вашия код и ги заменете с други механизми. Наистина не се нуждаете от сингълтъни. Въпреки това, ако трябва да гарантирате съществуването на една единствена инстанция на клас за цялото приложение, оставете това на [DI контейнера |container]. По този начин създайте апликационен сингълтън, т.е. сървис. Така класът ще спре да се занимава с осигуряването на собствената си уникалност (т.е. няма да има метод `getInstance()` и статична променлива) и ще изпълнява само своите функции. Така ще спре да нарушава принципа на единствената отговорност. - - -Глобално състояние срещу тестове --------------------------------- - -При писане на тестове предполагаме, че всеки тест е изолирана единица и че в него не влиза никакво външно състояние. И никакво състояние не напуска тестовете. След приключване на теста цялото свързано с теста състояние трябва да бъде автоматично премахнато от garbage collector-а. Благодарение на това тестовете са изолирани. Затова можем да изпълняваме тестовете в произволен ред. - -Ако обаче са налице глобални състояния/сингълтъни, всички тези приятни предположения се разпадат. Състоянието може да влиза и излиза от теста. Изведнъж редът на тестовете може да има значение. - -За да можем изобщо да тестваме сингълтъни, разработчиците често трябва да разхлабят техните свойства, например като позволят инстанцията да бъде заменена с друга. Такива решения в най-добрия случай са хак, който създава трудно поддържаем и разбираем код. Всеки тест или метод `tearDown()`, който повлияе на някакво глобално състояние, трябва да върне тези промени обратно. - -Глобалното състояние е най-голямото главоболие при unit тестването! - -Как да поправим ситуацията? Лесно. Не пишете код, който използва сингълтъни, предпочитайте предаването на зависимости. Тоест dependency injection. - - -Глобални константи ------------------- - -Глобалното състояние не се ограничава само до използването на сингълтъни и статични променливи, но може да се отнася и до глобални константи. - -Константи, чиято стойност не ни носи никаква нова (`M_PI`) или полезна (`PREG_BACKTRACK_LIMIT_ERROR`) информация, са недвусмислено в ред. Напротив, константи, които служат като начин за *безжично* предаване на информация вътре в кода, не са нищо друго освен скрита зависимост. Като например `LOG_FILE` в следващия пример. Използването на константата `FILE_APPEND` е напълно коректно. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -В този случай трябва да декларираме параметър в конструктора на класа `Foo`, за да стане част от API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Сега можем да предадем информация за пътя до лог файла и лесно да го променяме при нужда, което улеснява тестването и поддръжката на кода. - - -Глобални функции и статични методи ----------------------------------- - -Искаме да подчертаем, че самото използване на статични методи и глобални функции не е проблематично. Обяснихме защо използването на `DB::insert()` и подобни методи е неподходящо, но винаги ставаше дума само за глобално състояние, което се съхранява в някаква статична променлива. Методът `DB::insert()` изисква съществуването на статична променлива, тъй като в нея се съхранява връзката с базата данни. Без тази променлива би било невъзможно да се имплементира методът. - -Използването на детерминистични статични методи и функции, като например `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` и много други, е в пълно съответствие с dependency injection. Тези функции винаги връщат едни и същи резултати за едни и същи входни параметри и следователно са предвидими. Те не използват никакво глобално състояние. - -Съществуват обаче и функции в PHP, които не са детерминистични. Към тях принадлежи например функцията `htmlspecialchars()`. Нейният трети параметър `$encoding`, ако не е посочен, по подразбиране има стойността на конфигурационната опция `ini_get('default_charset')`. Затова се препоръчва винаги да се посочва този параметър, за да се избегне евентуално непредсказуемо поведение на функцията. Nette го прави последователно. - -Някои функции, като например `strtolower()`, `strtoupper()` и подобни, в близкото минало се държаха недетерминистично и зависеха от настройката `setlocale()`. Това причиняваше много усложнения, най-често при работа с турски език. Той различава малки и големи букви `I` с точка и без точка. Така че `strtolower('I')` връщаше знака `ı`, а `strtoupper('i')` - знака `İ`, което водеше до това, че приложенията започваха да причиняват редица мистериозни грешки. Този проблем обаче беше отстранен в PHP версия 8.2 и функциите вече не зависят от locale. - -Това е хубав пример как глобалното състояние е измъчвало хиляди разработчици по целия свят. Решението беше да се замени с dependency injection. - - -Кога е възможно да се използва глобално състояние? --------------------------------------------------- - -Съществуват определени специфични ситуации, в които е възможно да се използва глобално състояние. Например, при дебъгване на код, когато трябва да изведете стойността на променлива или да измерите продължителността на определена част от програмата. В такива случаи, които се отнасят до временни актове, които по-късно ще бъдат премахнати от кода, е възможно легитимно да се използва глобално достъпен dumper или хронометър. Тези инструменти всъщност не са част от дизайна на кода. - -Друг пример са функциите за работа с регулярни изрази `preg_*`, които вътрешно съхраняват компилирани регулярни изрази в статичен кеш в паметта. Така че, когато извиквате един и същ регулярен израз многократно на различни места в кода, той се компилира само веднъж. Кешът спестява производителност и в същото време е напълно невидим за потребителя, затова такова използване може да се счита за легитимно. - - -Обобщение ---------- - -Обсъдихме защо има смисъл: - -1) Да премахнете всички статични променливи от кода -2) Да декларирате зависимости -3) И да използвате dependency injection - -Когато обмисляте дизайна на кода, имайте предвид, че всяко `static $foo` представлява проблем. За да бъде вашият код среда, уважаваща DI, е необходимо напълно да изкорените глобалното състояние и да го замените с dependency injection. - -По време на този процес може да откриете, че е необходимо да разделите класа, защото той има повече от една отговорност. Не се страхувайте от това; стремете се към принципа на единствената отговорност. - -*Бих искал да благодаря на Miško Hevery, чиито статии, като [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], са в основата на тази глава.* diff --git a/dependency-injection/bg/introduction.texy b/dependency-injection/bg/introduction.texy deleted file mode 100644 index 902247b1dd..0000000000 --- a/dependency-injection/bg/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Какво е Dependency Injection? -***************************** - -.[perex] -Тази глава ще ви запознае с основните програмни практики, които трябва да следвате при писането на всички приложения. Това са основите, необходими за писане на чист, разбираем и поддържаем код. - -Ако усвоите тези правила и ги спазвате, Nette ще ви помага на всяка стъпка. Той ще се справя с рутинните задачи вместо вас и ще ви осигури максимален комфорт, за да можете да се съсредоточите върху самата логика. - -Принципите, които ще покажем тук, са доста прости. Няма нужда да се притеснявате за нищо. - - -Спомняте ли си първата си програма? ------------------------------------ - -Не знаем на какъв език сте я написали, но ако беше PHP, вероятно щеше да изглежда така: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // извежда 24 -``` - -Няколко тривиални реда код, но в тях се крият толкова много ключови концепции. Че съществуват променливи. Че кодът се разделя на по-малки единици, като например функции. Че им предаваме входни аргументи и те връщат резултати. Липсват само условия и цикли. - -Това, че предаваме входни данни на функция и тя връща резултат, е напълно разбираема концепция, която се използва и в други области, като например математиката. - -Функцията има своя сигнатура, която се състои от нейното име, списък с параметри и техните типове, и накрая тип на връщаната стойност. Като потребители ни интересува сигнатурата, обикновено не е необходимо да знаем нищо за вътрешната имплементация. - -Сега си представете, че сигнатурата на функцията изглеждаше така: - -```php -function soucet(float $x): float -``` - -Сума с един параметър? Това е странно… А какво ще кажете за това? - -```php -function soucet(): float -``` - -Това вече е наистина много странно, нали? Как се използва функцията? - -```php -echo soucet(); // какво ли ще изведе? -``` - -При вида на такъв код бихме били объркани. Не само начинаещ не би го разбрал, такъв код не разбира и опитен програмист. - -Чудите ли се как би изглеждала такава функция отвътре? Откъде ще вземе събираемите? Вероятно би си ги набавила *по някакъв начин* сама, например така: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -В тялото на функцията открихме скрити връзки към други глобални функции или статични методи. За да разберем откъде всъщност идват събираемите, трябва да търсим по-нататък. - - -Не така! --------- - -Дизайнът, който току-що показахме, е есенцията на много негативни черти: - -- сигнатурата на функцията се преструваше, че не се нуждае от събираеми, което ни объркваше -- изобщо не знаем как да накараме функцията да събере други две числа -- трябваше да погледнем в кода, за да разберем откъде взема събираемите -- открихме скрити зависимости -- за пълно разбиране е необходимо да се проучат и тези зависимости - -И изобщо задача ли е на функцията за събиране да си набавя входове? Разбира се, че не е. Нейната отговорност е само самото събиране. - - -С такъв код не искаме да се сблъскваме и определено не искаме да го пишем. Поправката е проста: да се върнем към основите и просто да използваме параметри: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Правило № 1: Нека ви го предадат --------------------------------- - -Най-важното правило е: **всички данни, от които функциите или класовете се нуждаят, трябва да им бъдат предадени**. - -Вместо да измисляте скрити начини, чрез които те биха могли да стигнат до тях сами, просто предайте параметрите. Ще спестите време, необходимо за измисляне на скрити пътища, които определено няма да подобрят вашия код. - -Ако спазвате това правило винаги и навсякъде, сте на път към код без скрити зависимости. Към код, който е разбираем не само за автора, но и за всеки, който ще го чете след него. Където всичко е разбираемо от сигнатурите на функциите и класовете и не е необходимо да се търсят скрити тайни в имплементацията. - -Тази техника се нарича професионално **dependency injection**. А тези данни се наричат **зависимости.** Всъщност това е просто предаване на параметри, нищо повече. - -.[note] -Моля, не бъркайте dependency injection, което е дизайнерски патърн, с „dependency injection container“, което пък е инструмент, т.е. нещо диаметрално различно. Ще се занимаваме с контейнерите по-късно. - - -От функции към класове ----------------------- - -А как това е свързано с класовете? Класът е по-сложна единица от проста функция, но правило № 1 важи изцяло и тук. Само че съществуват [повече начини за предаване на аргументи|passing-dependencies]. Например, доста подобно на случая с функция: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Или чрез други методи, или директно чрез конструктора: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -И двата примера са напълно в съответствие с dependency injection. - - -Реални примери --------------- - -В реалния свят няма да пишете класове за събиране на числа. Нека преминем към примери от практиката. - -Нека имаме клас `Article`, представляващ статия в блог: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // запазваме статията в базата данни - } -} -``` - -и употребата ще бъде следната: - -```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(); -``` - -Методът `save()` запазва статията в таблица в базата данни. Имплементирането му с помощта на [Nette Database |database:] ще бъде лесно, ако не беше една спънка: откъде `Article` да вземе връзка към базата данни, т.е. обект от клас `Nette\Database\Connection`? - -Изглежда, че имаме много възможности. Може да я вземе отнякъде от статична променлива. Или да наследи от клас, който осигурява връзка с базата данни. Или да използва т.нар. [singleton |global-state#Singleton]. Или т.нар. фасади, които се използват в 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], - ); - } -} -``` - -Страхотно, решихме проблема. - -Или не? - -Да си припомним [#Правило № 1: Нека ви го предадат |#Правило 1: Нека ви го предадат]: всички зависимости, от които класът се нуждае, трябва да му бъдат предадени. Защото ако нарушим правилото, сме поели по пътя към мръсен код, пълен със скрити зависимости, неразбираемост, и резултатът ще бъде приложение, което ще бъде болезнено за поддръжка и разработка. - -Потребителят на класа `Article` не знае къде методът `save()` запазва статията. В таблица в базата данни? В коя, продукционната или тестовата? И как може да се промени това? - -Потребителят трябва да погледне как е имплементиран методът `save()` и намира използването на метода `DB::insert()`. Така че трябва да търси по-нататък как този метод си набавя връзка към базата данни. А скритите зависимости могат да образуват доста дълга верига. - -В чист и добре проектиран код никога не се срещат скрити зависимости, фасади в стил Laravel или статични променливи. В чист и добре проектиран код се предават аргументи: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Още по-практично, както ще видим по-нататък, ще бъде чрез конструктора: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Ако сте опитен програмист, може би си мислите, че `Article` изобщо не трябва да има метод `save()`, трябва да представлява чисто компонент за данни и за запазването трябва да се грижи отделно репозитори. Това има смисъл. Но така бихме се отклонили твърде много от темата, която е dependency injection, и от стремежа да даваме прости примери. - -Ако пишете клас, който изисква за дейността си например база данни, не измисляйте откъде да я вземете, а поискайте да ви бъде предадена. Например като параметър на конструктора или друг метод. Признайте зависимостите. Признайте ги в API на вашия клас. Ще получите разбираем и предвидим код. - -А какво ще кажете за този клас, който логва съобщения за грешки: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Какво мислите, спазихме ли [#Правило № 1: Нека ви го предадат |#Правило 1: Нека ви го предадат]? - -Не спазихме. - -Ключовата информация, т.е. директорията с файла с лога, класът *си набавя сам* от константа. - -Погледнете примера за употреба: - -```php -$logger = new Logger; -$logger->log('Температурата е 23 °C'); -$logger->log('Температурата е 10 °C'); -``` - -Без да познавате имплементацията, бихте ли могли да отговорите на въпроса къде се записват съобщенията? Би ли ви хрумнало, че за функционирането е необходимо съществуването на константата `LOG_DIR`? И бихте ли могли да създадете втора инстанция, която да записва другаде? Със сигурност не. - -Нека поправим класа: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Класът сега е много по-разбираем, конфигурируем и следователно по-полезен. - -```php -$logger = new Logger('/път/към/лог.txt'); -$logger->log('Температурата е 15 °C'); -``` - - -Но това не ме интересува! -------------------------- - -*„Когато създам обект Article и извикам save(), не искам да се занимавам с базата данни, просто искам да се запази в тази, която съм настроил в конфигурацията.“* - -*„Когато използвам Logger, просто искам съобщението да се запише и не искам да се занимавам къде. Нека се използва глобалната настройка.“* - -Това са правилни забележки. - -Като пример ще покажем клас, който разпраща бюлетини и логва как е минало: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Имейлите бяха изпратени'); - - } catch (Exception $e) { - $logger->log('Възникна грешка при изпращането'); - throw $e; - } - } -} -``` - -Подобреният `Logger`, който вече не използва константата `LOG_DIR`, изисква в конструктора да се посочи пътят към файла. Как да решим това? Класът `NewsletterDistributor` изобщо не се интересува къде се записват съобщенията, иска само да ги запише. - -Решението е отново [#Правило № 1: Нека ви го предадат |#Правило 1: Нека ви го предадат]: всички данни, от които класът се нуждае, му предаваме. - -Значи това означава, че ще си предадем пътя към лога чрез конструктора, който след това ще използваме при създаването на обекта `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ ТАКА НЕ! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Така не! Пътят всъщност **не принадлежи** към данните, от които класът `NewsletterDistributor` се нуждае; от тях се нуждае `Logger`. Усещате ли разликата? Класът `NewsletterDistributor` се нуждае от логъра като такъв. Така че ще си го предадем: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Имейлите бяха изпратени'); - - } catch (Exception $e) { - $this->logger->log('Възникна грешка при изпращането'); - throw $e; - } - } -} -``` - -Сега от сигнатурите на класа `NewsletterDistributor` е ясно, че част от неговата функционалност е и логването. А задачата да се смени логърът с друг, например за тестване, е напълно тривиална. Освен това, ако конструкторът на класа `Logger` се промени, това няма да има никакво влияние върху нашия клас. - - -Правило № 2: Вземи това, което е твое -------------------------------------- - -Не се заблуждавайте и не си предавайте зависимостите на вашите зависимости. Предавайте си само вашите собствени зависимости. - -Благодарение на това кодът, използващ други обекти, ще бъде напълно независим от промените в техните конструктори. Неговото API ще бъде по-вярно. И най-важното, ще бъде тривиално тези зависимости да се заменят с други. - - -Нов член на семейството ------------------------ - -В екипа за разработка беше взето решение да се създаде втори логър, който записва в база данни. Затова създаваме клас `DatabaseLogger`. Така имаме два класа, `Logger` и `DatabaseLogger`, единият записва във файл, другият в база данни … не ви ли се струва нещо странно в това именуване? Не би ли било по-добре да преименуваме `Logger` на `FileLogger`? Със сигурност да. - -Но ще го направим умно. Под оригиналното име ще създадем интерфейс: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… който и двата логъра ще имплементират: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -И благодарение на това няма да е необходимо да се променя нищо в останалата част от кода, където се използва логърът. Например конструкторът на класа `NewsletterDistributor` ще продължи да бъде доволен, че като параметър изисква `Logger`. И ще зависи само от нас коя инстанция ще му предадем. - -**Затова никога не даваме на имената на интерфейсите суфикс `Interface` или префикс `I`.** В противен случай не би било възможно кодът да се развива толкова добре. - - -Хюстън, имаме проблем ---------------------- - -Докато в цялото приложение можем да се справим с една единствена инстанция на логъра, било то файлов или базиран на данни, и просто го предаваме навсякъде, където нещо се логва, съвсем различно е положението с класа `Article`. Неговите инстанции създаваме според нуждите, дори многократно. Как да се справим със зависимостта от базата данни в неговия конструктор? - -Като пример може да послужи контролер, който след изпращане на формуляр трябва да запази статия в базата данни: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Възможното решение се натрапва само: ще си предадем обекта на базата данни чрез конструктора в `EditController` и ще използваме `$article = new Article($this->db)`. - -Точно както в предишния случай с `Logger` и пътя към файла, това не е правилният подход. Базата данни не е зависимост на `EditController`, а на `Article`. Предаването на базата данни следователно противоречи на [Правило № 2: Вземи това, което е твое |#Правило 2: Вземи това което е твое]. Когато конструкторът на класа `Article` се промени (добави се нов параметър), ще бъде необходимо да се коригира и кодът на всички места, където се създават инстанции. Уф. - -Хюстън, какво предлагаш? - - -Правило № 3: Остави го на фабриката ------------------------------------ - -Като премахнахме скритите зависимости и предаваме всички зависимости като аргументи, получихме по-конфигурируеми и гъвкави класове. И следователно се нуждаем от още нещо, което да ни създаде и конфигурира тези по-гъвкави класове. Ще го наречем фабрики. - -Правилото гласи: ако класът има зависимости, оставете създаването на техните инстанции на фабрика. - -Фабриките са по-умната замяна на оператора `new` в света на dependency injection. - -.[note] -Моля, не бъркайте с дизайнерския патърн *factory method*, който описва специфичен начин за използване на фабрики и не е свързан с тази тема. - - -Фабрика -------- - -Фабриката е метод или клас, който произвежда и конфигурира обекти. Класът, произвеждащ `Article`, ще наречем `ArticleFactory` и би могъл да изглежда например така: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Нейното използване в контролера ще бъде следното: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // оставяме фабриката да създаде обекта - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Ако в този момент се промени сигнатурата на конструктора на класа `Article`, единствената част от кода, която трябва да реагира на това, е самата фабрика `ArticleFactory`. Целият останал код, който работи с обекти `Article`, като например `EditController`, няма да бъде засегнат по никакъв начин. - -Може би сега си удряте челото, дали изобщо сме си помогнали. Количеството код нарасна и всичко започва да изглежда подозрително сложно. - -Не се притеснявайте, скоро ще стигнем до Nette DI контейнера. А той има редица асове в ръкава, с които изграждането на приложения, използващи dependency injection, се опростява неимоверно. Така например, вместо клас `ArticleFactory`, ще е достатъчно [напишете само интерфейс |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Но това е изпреварване, изчакайте още малко :-) - - -Резюме ------- - -В началото на тази глава обещахме, че ще покажем процедура за проектиране на чист код. Достатъчно е на класовете - -1) [предавайте зависимостите, от които се нуждаят |#Правило 1: Нека ви го предадат] -2) [и обратно, не предавайте това, от което не се нуждаят пряко |#Правило 2: Вземи това което е твое] -3) [и че обектите със зависимости се създават най-добре във фабрики |#Правило 3: Остави го на фабриката] - -Може да не изглежда така на пръв поглед, но тези три правила имат далечни последици. Водят до радикално различен поглед върху дизайна на кода. Струва ли си? Програмистите, които са изоставили старите навици и са започнали последователно да използват dependency injection, смятат тази стъпка за ключов момент в професионалния си живот. Открил се е пред тях свят на прегледни и поддържаеми приложения. - -Ами ако кодът не използва последователно dependency injection? Ами ако е изграден върху статични методи или сингълтони? Носи ли това някакви проблеми? [Носи и то много съществени |global-state]. diff --git a/dependency-injection/bg/nette-container.texy b/dependency-injection/bg/nette-container.texy deleted file mode 100644 index 801555f8d8..0000000000 --- a/dependency-injection/bg/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI контейнер -****************** - -.[perex] -Nette DI е една от най-интересните библиотеки на Nette. Тя може да генерира и автоматично да актуализира компилирани DI контейнери, които са изключително бързи и невероятно лесни за конфигуриране. - -Формата на сървисите, които DI контейнерът трябва да създава, обикновено дефинираме с помощта на конфигурационни файлове във [формат NEON|neon:format]. Контейнерът, който ръчно създадохме в [предишната глава|container], би се записал така: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Записът е наистина кратък. - -Всички зависимости, декларирани в конструкторите на класовете `ArticleFactory` и `UserController`, Nette DI само открива и предава благодарение на т.нар. [autowiring|autowiring], затова в конфигурационния файл не е необходимо да се посочва нищо. Така че дори ако параметрите се променят, не е необходимо да променяте нищо в конфигурацията. Nette контейнерът автоматично ще се прегенерира. Вие можете да се съсредоточите изцяло върху разработката на приложението. - -Ако искаме да предаваме зависимости чрез сетъри, използваме за това секцията [setup |services#Setup]. - -Nette DI генерира директно PHP код на контейнера. Резултатът е файл `.php`, който можете да отворите и изучавате. Благодарение на това виждате точно как работи контейнерът. Можете също да го дебъгвате в IDE и да го проследявате стъпка по стъпка. И най-важното: генерираният PHP е изключително бърз. - -Nette DI може също да генерира код на [фабрики|factory] въз основа на предоставен интерфейс. Затова вместо клас `ArticleFactory` ще ни е достатъчно да създадем в приложението само интерфейс: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Целият пример можете да намерите [в GitHub|https://github.com/nette-examples/di-example-doc]. - - -Самостоятелна употреба ----------------------- - -Внедряването на библиотеката Nette DI в приложение е много лесно. Първо я инсталираме с Composer (защото изтеглянето на zip файлове е тааака остаряло): - -```shell -composer require nette/di -``` - -Следващият код създава инстанция на DI контейнер според конфигурацията, съхранена във файла `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Контейнерът се генерира само веднъж, неговият код се записва в кеша (директория `__DIR__ . '/temp'`) и при следващи заявки се зарежда само оттам. - -За създаване и получаване на сървиси служат методите `getService()` или `getByType()`. Така създаваме обект `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -По време на разработка е полезно да се активира режимът на автоматично опресняване, при който контейнерът автоматично се прегенерира, ако настъпи промяна в някой клас или конфигурационен файл. Достатъчно е в конструктора на `ContainerLoader` да се посочи като втори аргумент `true`. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Използване с Nette Framework ----------------------------- - -Както показахме, използването на Nette DI не е ограничено до приложения, написани в Nette Framework, можете да го внедрите навсякъде само с 3 реда код. Ако обаче разработвате приложения в Nette Framework, конфигурацията и създаването на контейнера се управляват от [Bootstrap |application:bootstrapping#Конфигурация на DI контейнера]. diff --git a/dependency-injection/bg/passing-dependencies.texy b/dependency-injection/bg/passing-dependencies.texy deleted file mode 100644 index 0652e8cd2f..0000000000 --- a/dependency-injection/bg/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Предаване на зависимости -************************ - -<div class=perex> - -Аргументите, или в терминологията на DI „зависимости“, могат да се предават на класове по следните основни начини: - -* предаване чрез конструктор -* предаване чрез метод (т.нар. сетър) -* задаване на променлива -* чрез метод, анотация или атрибут *inject* - -</div> - -Сега ще покажем отделните варианти с конкретни примери. - - -Предаване чрез конструктор -========================== - -Зависимостите се предават в момента на създаване на обекта като аргументи на конструктора: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Тази форма е подходяща за задължителни зависимости, от които класът непременно се нуждае за своята функция, тъй като без тях инстанцията няма да може да бъде създадена. - -От PHP 8.0 можем да използваме по-кратка форма на запис ([constructor property promotion |https://blog.nette.org/bg/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), която е функционално еквивалентна: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -От PHP 8.1 променливата може да бъде маркирана с флага `readonly`, който декларира, че съдържанието на променливата няма да се променя повече: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -DI контейнерът предава зависимостите на конструктора автоматично чрез [autowiring |autowiring]. Аргументите, които не могат да бъдат предадени по този начин (напр. низове, числа, булеви стойности), [записваме в конфигурацията |services#Аргументи]. - - -Адът на конструктора --------------------- - -Терминът *constructor hell* описва ситуация, когато наследник наследява от родителски клас, чийто конструктор изисква зависимости, и същевременно наследникът изисква зависимости. При това трябва да приеме и предаде и родителските: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Проблемът възниква в момента, когато искаме да променим конструктора на класа `BaseClass`, например когато се добави нова зависимост. Тогава е необходимо да се коригират и всички конструктори на наследниците. Което превръща такава корекция в ад. - -Как да предотвратим това? Решението е **да се дава предимство на [композиция пред наследяване |faq#Защо се предпочита композиция пред наследяването]**. - -Тоест, ще проектираме кода по друг начин. Ще избягваме [абстрактни |nette:introduction-to-object-oriented-programming#Абстрактни класове] `Base*` класове. Вместо `MyClass` да получава определена функционалност чрез наследяване от `BaseClass`, тази функционалност ще му бъде предадена като зависимост: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Предаване чрез сетър -==================== - -Зависимостите се предават чрез извикване на метод, който ги съхранява в частна променлива. Обичайната конвенция за именуване на тези методи е формата `set*()`, затова се наричат сетъри, но разбира се, могат да се наричат и по друг начин. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Този начин е подходящ за незадължителни зависимости, които не са необходими за функцията на класа, тъй като не е гарантирано, че обектът действително ще получи зависимостта (т.е. че потребителят ще извика метода). - -Същевременно този начин позволява сетърът да се извиква многократно и така зависимостта да се променя. Ако това не е желателно, добавяме проверка в метода, или от PHP 8.1 маркираме свойството `$cache` с флага `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('Зависимостта вече е зададена'); - } - $this->cache = $cache; - } -} -``` - -Извикването на сетъра дефинираме в конфигурацията на DI контейнера в [ключа setup |services#Setup]. И тук се използва автоматично предаване на зависимости чрез autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Чрез задаване на променлива -=========================== - -Зависимостите се предават чрез записване директно в член-променлива: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Този начин се счита за неподходящ, тъй като член-променливата трябва да бъде декларирана като `public`. И следователно нямаме контрол над това, че предадената зависимост ще бъде действително от дадения тип (важеше преди PHP 7.4) и губим възможността да реагираме на новоприсвоената зависимост със собствен код, например да предотвратим последваща промяна. Същевременно променливата става част от публичния интерфейс на класа, което може да не е желателно. - -Задаването на променливата дефинираме в конфигурацията на DI контейнера в [секцията setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Докато предходните три начина важат общо за всички обектно-ориентирани езици, инжектирането чрез метод, анотация или атрибут *inject* е специфично само за презентерите в Nette. За тях се разказва в [отделна глава |best-practices:inject-method-attribute]. - - -Кой метод да изберем? -===================== - -- конструкторът е подходящ за задължителни зависимости, от които класът непременно се нуждае за своята функция -- сетърът, напротив, е подходящ за незадължителни зависимости или зависимости, които може да се наложи да се променят по-нататък -- публичните променливи не са подходящи diff --git a/dependency-injection/bg/services.texy b/dependency-injection/bg/services.texy deleted file mode 100644 index 3328859a8f..0000000000 --- a/dependency-injection/bg/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Дефиниране на сървиси -********************* - -.[perex] -Конфигурацията е мястото, където учим DI контейнера как да изгражда отделните сървиси и как да ги свързва с други зависимости. Nette предоставя много прегледен и елегантен начин да се постигне това. - -Секцията `services` в конфигурационния файл във формат NEON е мястото, където дефинираме собствени сървиси и техните конфигурации. Нека разгледаме прост пример за дефиниция на сървис, наречен `database`, който представлява инстанция на класа `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Посочената конфигурация ще доведе до следния фабричен метод в [DI контейнера|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Имената на сървисите ни позволяват да се позоваваме на тях в други части на конфигурационния файл, във формат `@имеНаСървис`. Ако не е необходимо сървисът да се именува, можем просто да използваме само тире: - -```neon -services: - - PDO('sqlite::memory:') -``` - -За да получим сървис от DI контейнера, можем да използваме метода `getService()` с името на сървиса като параметър, или метода `getByType()` с типа на сървиса: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Създаване на сървис -=================== - -Обикновено създаваме сървис просто като създадем инстанция на определен клас. Например: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Ако е необходимо да разширим конфигурацията с допълнителни ключове, дефиницията може да се разпише на няколко реда: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Ключът `create` има псевдоним `factory`, и двата варианта са често срещани в практиката. Въпреки това препоръчваме да използвате `create`. - -Аргументите на конструктора или създаващия метод могат алтернативно да бъдат записани в ключа `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Сървисите не е задължително да се създават само чрез просто създаване на инстанция на клас, те могат да бъдат и резултат от извикване на статични методи или методи на други сървиси: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Обърнете внимание, че за простота вместо `->` се използва `::`, вижте [#изразителни средства]. Ще се генерират тези фабрични методи: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -DI контейнерът трябва да знае типа на създадения сървис. Ако създаваме сървис чрез метод, който няма указан тип на връщаната стойност, трябва изрично да посочим този тип в конфигурацията: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Аргументи -========= - -Предаваме аргументи на конструктора и методите по начин, много подобен на самия PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -За по-добра четимост можем да разпишем аргументите на отделни редове. В такъв случай използването на запетаи е по избор: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Можете също да именувате аргументите и тогава не е нужно да се притеснявате за техния ред: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Ако искате да пропуснете някои аргументи и да използвате тяхната стойност по подразбиране или да вмъкнете сървис чрез [autowiring|autowiring], използвайте долна черта: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Като аргументи могат да се предават сървиси, да се използват параметри и много повече, вижте [#изразителни средства]. - - -Setup -===== - -В секцията `setup` дефинираме методите, които трябва да се извикат при създаването на сървиса. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Това в PHP би изглеждало така: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Освен извикване на методи, може също да се предават стойности на свойства. Поддържа се и добавяне на елемент към масив, което трябва да се запише в кавички, за да не колидира със синтаксиса на NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Което в PHP кода би изглеждало по следния начин: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -В setup обаче могат да се извикват и статични методи или методи на други сървиси. Ако е необходимо да предадете като аргумент текущия сървис, посочете го като `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Обърнете внимание, че за простота вместо `->` се използва `::`, вижте [#изразителни средства]. Ще се генерира такъв фабричен метод: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Изразителни средства -==================== - -Nette DI ни дава изключително богати изразителни средства, с помощта на които можем да запишем почти всичко. В конфигурационните файлове така можем да използваме [параметри |configuration#Параметри]: - -```neon -# параметър -%wwwDir% - -# стойност на параметър под ключ -%mailer.user% - -# параметър вътре в низ -'%wwwDir%/images' -``` - -Освен това да създаваме обекти, да извикваме методи и функции: - -```neon -# създаване на обект -DateTime() - -# извикване на статичен метод -Collator::create(%locale%) - -# извикване на PHP функция -::getenv(DB_USER) -``` - -Да се позоваваме на сървиси или по тяхното име, или чрез типа: - -```neon -# сървис по име -@database - -# сървис по тип -@Nette\Database\Connection -``` - -Да използваме first-class callable синтаксис: .{data-version:3.2.0} - -```neon -# създаване на callback, аналог на [@user, logout] -@user::logout(...) -``` - -Да използваме константи: - -```neon -# константа на клас -FilesystemIterator::SKIP_DOTS - -# глобална константа се получава с PHP функцията constant() -::constant(PHP_VERSION) -``` - -Извикванията на методи могат да се верижат точно както в PHP. Само за простота вместо `->` се използва `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Тези изрази можете да използвате навсякъде, при [създаване на сървиси |#Създаване на сървис], в [#аргументи], в секцията [#setup] или [параметри |configuration#Параметри]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Специални функции ------------------ - -В конфигурационните файлове можете да използвате тези специални функции: - -- `not()` отрицание на стойност -- `bool()`, `int()`, `float()`, `string()` преобразуване без загуба към дадения тип -- `typed()` създава масив от всички сървиси от указания тип -- `tagged()` създава масив от всички сървиси с дадения таг - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -В сравнение с класическото преобразуване в PHP, като например `(int)`, преобразуването без загуба ще хвърли изключение за нечислови стойности. - -Функцията `typed()` създава масив от всички сървиси от дадения тип (клас или интерфейс). Пропуска сървисите, които имат изключен autowiring. Могат да се посочат и повече типове, разделени със запетая. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Масив от сървиси от определен тип можете да предавате като аргумент и автоматично чрез [autowiring |autowiring#Масив от сървиси]. - -Функцията `tagged()` пък създава масив от всички сървиси с определен таг. И тук можете да специфицирате повече тагове, разделени със запетая. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Ключът `autowired` позволява да се повлияе на поведението на autowiring за конкретен сървис. За детайли вижте [глава за autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # сървисът foo е изключен от autowiring -``` - - -Lazy сървиси .{data-version:3.2.4} -================================== - -Lazy loading е техника, която отлага създаването на сървис до момента, в който той действително е необходим. В глобалната конфигурация може да се [активиране на lazy създаване |configuration#Lazy сървиси] за всички сървиси наведнъж. За отделни сървиси след това можете да презапишете това поведение: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Когато сървисът е дефиниран като lazy, при неговото изискване от DI контейнера получаваме специален прокси обект. Той изглежда и се държи точно като реалния сървис, но реалната инициализация (извикване на конструктора и setup) се извършва едва при първото извикване на някой от неговите методи или свойства. - -.[note] -Lazy loading може да се използва само за потребителски класове, а не за вътрешни PHP класове. Изисква PHP 8.4 или по-нова версия. - - -Тагове -====== - -Таговете служат за добавяне на допълнителна информация към сървисите. На сървис можете да добавите един или повече тагове: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Таговете могат също да носят стойности: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -За да получите всички сървиси с определени тагове, можете да използвате функцията `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -В DI контейнера можете да получите имената на всички сървиси с определен таг чрез метода `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names е масив, съдържащ името на сървиса и стойността на тага -// напр. ['foo' => 'monolog.logger.event', ...] -``` - - -Режим Inject -============ - -С помощта на флага `inject: true` се активира предаването на зависимости чрез публични променливи с анотация [inject |best-practices:inject-method-attribute#Атрибути Inject] и методи [inject*() |best-practices:inject-method-attribute#Методи inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -По подразбиране `inject` е активирано само за презентери. - - -Модификация на сървиси -====================== - -DI контейнерът съдържа много сървиси, които са били добавени чрез вградено или [потребителско разширение|extensions]. Можете да променяте дефинициите на тези сървиси директно в конфигурацията. Например, можете да промените класа на сървиса `application.application`, който стандартно е `Nette\Application\Application`, на друг: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Флагът `alteration` е информативен и казва, че само модифицираме съществуващ сървис. - -Можем също да допълним setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -При презаписване на сървис можем да искаме да премахнем оригиналните аргументи, елементи от setup или тагове, за което служи `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Ако искате да премахнете сървис, добавен от разширение, можете да го направите така: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/el/@home.texy b/dependency-injection/el/@home.texy deleted file mode 100644 index 8c439b2f8a..0000000000 --- a/dependency-injection/el/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -Το Dependency Injection είναι ένα πρότυπο σχεδίασης που θα αλλάξει ριζικά την οπτική σας για τον κώδικα και την ανάπτυξη. Θα σας ανοίξει τον δρόμο στον κόσμο των καθαρά σχεδιασμένων και βιώσιμων εφαρμογών. - -- [Τι είναι το Dependency Injection; |introduction] -- [Καθολική κατάσταση και singletons |global-state] -- [Πέρασμα εξαρτήσεων |passing-dependencies] -- [Τι είναι ο DI container; |container] -- [Συχνές Ερωτήσεις|faq] - - -Το πακέτο `nette/di` παρέχει έναν εξαιρετικά προηγμένο μεταγλωττισμένο DI container για PHP. - -- [Nette DI Container |nette-container] -- [Διαμόρφωση |configuration] -- [Ορισμός υπηρεσιών |services] -- [Autowiring |autowiring] -- [Δημιουργημένα factories |factory] -- [Δημιουργία επεκτάσεων για το Nette DI|extensions] diff --git a/dependency-injection/el/@left-menu.texy b/dependency-injection/el/@left-menu.texy deleted file mode 100644 index 4cfa98f34c..0000000000 --- a/dependency-injection/el/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Τι είναι το DI; |introduction] -- [Καθολική κατάσταση και singletons |global-state] -- [Πέρασμα εξαρτήσεων |passing-dependencies] -- [Τι είναι ο DI container; |container] -- [Συχνές Ερωτήσεις|faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Διαμόρφωση |configuration] -- [Ορισμός υπηρεσιών |services] -- [Autowiring |autowiring] -- [Δημιουργημένα factories |factory] -- [Δημιουργία επεκτάσεων για το Nette DI|extensions] diff --git a/dependency-injection/el/@meta.texy b/dependency-injection/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/dependency-injection/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/dependency-injection/el/autowiring.texy b/dependency-injection/el/autowiring.texy deleted file mode 100644 index 024d4079d1..0000000000 --- a/dependency-injection/el/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Το Autowiring είναι ένα εξαιρετικό χαρακτηριστικό που μπορεί να περάσει αυτόματα τις απαιτούμενες υπηρεσίες στον κατασκευαστή και σε άλλες μεθόδους, οπότε δεν χρειάζεται να τις γράψουμε καθόλου. Σας εξοικονομεί πολύ χρόνο. - -Χάρη σε αυτό, μπορούμε να παραλείψουμε τη συντριπτική πλειοψηφία των ορισμάτων κατά τη σύνταξη ορισμών υπηρεσιών. Αντί για: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Αρκεί να γράψουμε: - -```neon -services: - articles: Model\ArticleRepository -``` - -Το Autowiring καθοδηγείται από τους τύπους, οπότε για να λειτουργήσει, η κλάση `ArticleRepository` πρέπει να οριστεί κάπως έτσι: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Για να είναι δυνατή η χρήση του autowiring, πρέπει να υπάρχει **ακριβώς μία υπηρεσία** για κάθε τύπο στο container. Αν υπήρχαν περισσότερες, το autowiring δεν θα ήξερε ποια να περάσει και θα προκαλούσε εξαίρεση: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # ΠΡΟΚΑΛΕΙ ΕΞΑΙΡΕΣΗ, ταιριάζουν και η mainDb και η tempDb -``` - -Η λύση θα ήταν είτε να παρακάμψουμε το autowiring και να δηλώσουμε ρητά το όνομα της υπηρεσίας (δηλ. `articles: Model\ArticleRepository(@mainDb)`). Πιο έξυπνο όμως είναι να [απενεργοποιήσουμε |#Απενεργοποίηση του autowiring] το autowiring για μία από τις υπηρεσίες, ή να [δώσουμε προτεραιότητα |#Προτίμηση autowiring] στην πρώτη υπηρεσία. - - -Απενεργοποίηση του autowiring ------------------------------ - -Μπορούμε να απενεργοποιήσουμε το autowiring μιας υπηρεσίας χρησιμοποιώντας την επιλογή `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # η υπηρεσία tempDb εξαιρείται από το autowiring - - articles: Model\ArticleRepository # επομένως περνάει τη mainDb στον κατασκευαστή -``` - -Η υπηρεσία `articles` δεν προκαλεί εξαίρεση ότι υπάρχουν δύο κατάλληλες υπηρεσίες τύπου `PDO` (δηλ. `mainDb` και `tempDb`) που μπορούν να περάσουν στον κατασκευαστή, επειδή βλέπει μόνο την υπηρεσία `mainDb`. - -.[note] -Η διαμόρφωση του autowiring στο Nette λειτουργεί διαφορετικά από ό,τι στο Symfony, όπου η επιλογή `autowire: false` λέει ότι το autowiring δεν πρέπει να χρησιμοποιείται για τα ορίσματα του κατασκευαστή της συγκεκριμένης υπηρεσίας. Στο Nette, το autowiring χρησιμοποιείται πάντα, είτε για τα ορίσματα του κατασκευαστή, είτε για οποιαδήποτε άλλη μέθοδο. Η επιλογή `autowired: false` λέει ότι η παρουσία της συγκεκριμένης υπηρεσίας δεν πρέπει να περνιέται πουθενά μέσω autowiring. - - -Προτίμηση autowiring --------------------- - -Εάν έχουμε πολλές υπηρεσίες του ίδιου τύπου και σε μία από αυτές δηλώσουμε την επιλογή `autowired`, αυτή η υπηρεσία γίνεται η προτιμώμενη: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # γίνεται η προτιμώμενη - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Η υπηρεσία `articles` δεν προκαλεί εξαίρεση ότι υπάρχουν δύο κατάλληλες υπηρεσίες τύπου `PDO` (δηλ. `mainDb` και `tempDb`), αλλά χρησιμοποιεί την προτιμώμενη υπηρεσία, δηλαδή τη `mainDb`. - - -Πίνακας υπηρεσιών ------------------ - -Το Autowiring μπορεί επίσης να περάσει πίνακες υπηρεσιών ενός συγκεκριμένου τύπου. Επειδή στην PHP δεν είναι δυνατό να γραφτεί εγγενώς ο τύπος των στοιχείων του πίνακα, είναι απαραίτητο, εκτός από τον τύπο `array`, να συμπληρωθεί και ένα phpDoc σχόλιο με τον τύπο του στοιχείου στη μορφή `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -Το DI container στη συνέχεια περνά αυτόματα έναν πίνακα υπηρεσιών που αντιστοιχούν στον συγκεκριμένο τύπο. Παραλείπει τις υπηρεσίες που έχουν απενεργοποιημένο το autowiring. - -Ο τύπος στο σχόλιο μπορεί επίσης να είναι στη μορφή `array<int, Class>` ή `list<Class>`. Εάν δεν μπορείτε να επηρεάσετε τη μορφή του phpDoc σχολίου, μπορείτε να περάσετε τον πίνακα υπηρεσιών απευθείας στη διαμόρφωση χρησιμοποιώντας το [`typed()` |services#Ειδικές συναρτήσεις]. - - -Σκαλωτά ορίσματα ----------------- - -Το Autowiring μπορεί να αντικαταστήσει μόνο αντικείμενα και πίνακες αντικειμένων. Τα σκαλωτά ορίσματα (π.χ. συμβολοσειρές, αριθμοί, booleans) [τα γράφουμε στη διαμόρφωση |services#Ορίσματα]. Μια εναλλακτική λύση είναι να δημιουργήσετε ένα [settings-object |best-practices:passing-settings-to-presenters], το οποίο ενσωματώνει την σκαλωτή τιμή (ή περισσότερες τιμές) σε μορφή αντικειμένου, το οποίο στη συνέχεια μπορεί να περάσει ξανά μέσω autowiring. - -```php -class MySettings -{ - public function __construct( - // το readonly είναι δυνατό να χρησιμοποιηθεί από την PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Δημιουργείτε μια υπηρεσία από αυτό προσθέτοντάς το στη διαμόρφωση: - -```neon -services: - - MySettings('any value') -``` - -Όλες οι κλάσεις στη συνέχεια το ζητούν μέσω autowiring. - - -Περιορισμός του autowiring --------------------------- - -Για μεμονωμένες υπηρεσίες, το autowiring μπορεί να περιοριστεί μόνο σε συγκεκριμένες κλάσεις ή interfaces. - -Κανονικά, το autowiring περνά την υπηρεσία σε κάθε παράμετρο μεθόδου, ο τύπος της οποίας αντιστοιχεί στην υπηρεσία. Ο περιορισμός σημαίνει ότι θέτουμε συνθήκες που πρέπει να πληρούν οι τύποι που αναφέρονται στις παραμέτρους των μεθόδων, ώστε η υπηρεσία να τους περάσει. - -Ας το δείξουμε με ένα παράδειγμα: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Αν τις καταχωρούσαμε όλες ως υπηρεσίες, το autowiring θα αποτύγχανε: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # ΠΡΟΚΑΛΕΙ ΕΞΑΙΡΕΣΗ, ταιριάζουν οι υπηρεσίες parent και child - childDep: ChildDependent # το autowiring περνά την υπηρεσία child στον κατασκευαστή -``` - -Η υπηρεσία `parentDep` προκαλεί εξαίρεση `Multiple services of type ParentClass found: parent, child`, επειδή στον κατασκευαστή της ταιριάζουν και οι δύο υπηρεσίες `parent` και `child`, και το autowiring δεν μπορεί να αποφασίσει ποια να επιλέξει. - -Για την υπηρεσία `child`, μπορούμε επομένως να περιορίσουμε το autowiring της στον τύπο `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # μπορεί να γραφτεί και 'autowired: self' - - parentDep: ParentDependent # το autowiring περνά την υπηρεσία parent στον κατασκευαστή - childDep: ChildDependent # το autowiring περνά την υπηρεσία child στον κατασκευαστή -``` - -Τώρα, στον κατασκευαστή της υπηρεσίας `parentDep` περνιέται η υπηρεσία `parent`, επειδή τώρα είναι το μόνο κατάλληλο αντικείμενο. Το autowiring δεν περνά πλέον την υπηρεσία `child` εκεί. Ναι, η υπηρεσία `child` εξακολουθεί να είναι τύπου `ParentClass`, αλλά η περιοριστική συνθήκη που δόθηκε για τον τύπο της παραμέτρου δεν ισχύει πλέον, δηλ. δεν ισχύει ότι το `ParentClass` *είναι υπερτύπος* του `ChildClass`. - -Για την υπηρεσία `child`, το `autowired: ChildClass` θα μπορούσε επίσης να γραφτεί ως `autowired: self`, καθώς το `self` είναι ένα placeholder για την κλάση της τρέχουσας υπηρεσίας. - -Στο κλειδί `autowired`, είναι δυνατόν να αναφερθούν και πολλές κλάσεις ή interfaces ως πίνακας: - -```neon -autowired: [BarClass, FooInterface] -``` - -Ας δοκιμάσουμε να συμπληρώσουμε το παράδειγμα και με interfaces: - -```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) - {} -} -``` - -Όταν η υπηρεσία `child` δεν περιορίζεται καθόλου, θα ταιριάζει στους κατασκευαστές όλων των κλάσεων `FooDependent`, `BarDependent`, `ParentDependent` και `ChildDependent` και το autowiring θα την περάσει εκεί. - -Αν όμως περιορίσουμε το autowiring της σε `ChildClass` χρησιμοποιώντας `autowired: ChildClass` (ή `self`), το autowiring θα την περάσει μόνο στον κατασκευαστή του `ChildDependent`, επειδή απαιτεί όρισμα τύπου `ChildClass` και ισχύει ότι το `ChildClass` *είναι τύπου* `ChildClass`. Κανένας άλλος τύπος που αναφέρεται στις άλλες παραμέτρους δεν είναι υπερτύπος του `ChildClass`, οπότε η υπηρεσία δεν περνιέται. - -Αν το περιορίσουμε σε `ParentClass` χρησιμοποιώντας `autowired: ParentClass`, το autowiring θα την περάσει ξανά στον κατασκευαστή του `ChildDependent` (επειδή το απαιτούμενο `ChildClass` είναι υπερτύπος του `ParentClass`) και τώρα και στον κατασκευαστή του `ParentDependent`, επειδή ο απαιτούμενος τύπος `ParentClass` είναι επίσης κατάλληλος. - -Αν το περιορίσουμε σε `FooInterface`, θα εξακολουθεί να γίνεται autowired στο `ParentDependent` (το απαιτούμενο `ParentClass` είναι υπερτύπος του `FooInterface`) και στο `ChildDependent`, αλλά επιπλέον και στον κατασκευαστή του `FooDependent`, όχι όμως στο `BarDependent`, επειδή το `BarInterface` δεν είναι υπερτύπος του `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # το autowiring περνά το child στον κατασκευαστή - barDep: BarDependent # ΠΡΟΚΑΛΕΙ ΕΞΑΙΡΕΣΗ, καμία υπηρεσία δεν ταιριάζει - parentDep: ParentDependent # το autowiring περνά το child στον κατασκευαστή - childDep: ChildDependent # το autowiring περνά το child στον κατασκευαστή -``` diff --git a/dependency-injection/el/configuration.texy b/dependency-injection/el/configuration.texy deleted file mode 100644 index 03e6ecb64d..0000000000 --- a/dependency-injection/el/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Διαμόρφωση του DI Container -*************************** - -.[perex] -Επισκόπηση των επιλογών διαμόρφωσης για το Nette DI Container. - - -Αρχείο διαμόρφωσης -================== - -Το Nette DI Container ελέγχεται εύκολα μέσω αρχείων διαμόρφωσης. Αυτά συνήθως γράφονται σε [μορφή NEON |neon:format]. Για την επεξεργασία, συνιστούμε [editors με υποστήριξη |best-practices:editors-and-tools#IDE editor] αυτής της μορφής. - -<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]":[#Επεκτάσεις]: "Εγκατάσταση πρόσθετων επεκτάσεων DI .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Εισαγωγή αρχείων]: "Εισαγωγή αρχείων .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Παράμετροι]: "Παράμετροι .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Αναζήτηση]: "Αυτόματη καταχώρηση υπηρεσιών .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Υπηρεσίες .[prism-token prism-comment]" -</pre> - -.[note] -Για να γράψετε μια συμβολοσειρά που περιέχει τον χαρακτήρα `%`, πρέπει να τον διαφύγετε διπλασιάζοντάς τον σε `%%`. - - -Παράμετροι -========== - -Στη διαμόρφωση, μπορείτε να ορίσετε παραμέτρους που μπορούν στη συνέχεια να χρησιμοποιηθούν ως μέρος των ορισμών υπηρεσιών. Με αυτόν τον τρόπο, μπορείτε να κάνετε τη διαμόρφωση πιο σαφή ή να ενοποιήσετε και να απομονώσετε τιμές που θα αλλάξουν. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Αναφερόμαστε στην παράμετρο `dsn` οπουδήποτε στη διαμόρφωση γράφοντας `%dsn%`. Οι παράμετροι μπορούν να χρησιμοποιηθούν και μέσα σε συμβολοσειρές όπως `'%wwwDir%/images'`. - -Οι παράμετροι δεν χρειάζεται να είναι μόνο συμβολοσειρές ή αριθμοί, μπορούν επίσης να περιέχουν πίνακες: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Αναφερόμαστε σε ένα συγκεκριμένο κλειδί ως `%mailer.user%`. - -Εάν χρειάζεστε στον κώδικά σας, για παράδειγμα σε μια κλάση, να μάθετε την τιμή οποιασδήποτε παραμέτρου, τότε περάστε την σε αυτήν την κλάση. Για παράδειγμα, στον κατασκευαστή. Δεν υπάρχει κανένα καθολικό αντικείμενο που να αντιπροσωπεύει τη διαμόρφωση, το οποίο οι κλάσεις θα ρωτούσαν για τις τιμές των παραμέτρων. Αυτό θα παραβίαζε την αρχή του dependency injection. - - -Υπηρεσίες -========= - -Βλ. [ξεχωριστό κεφάλαιο |services]. - - -Decorator -========= - -Πώς να τροποποιήσετε μαζικά όλες τις υπηρεσίες ενός συγκεκριμένου τύπου; Για παράδειγμα, να καλέσετε μια συγκεκριμένη μέθοδο σε όλους τους presenters που κληρονομούν από έναν συγκεκριμένο κοινό πρόγονο? Γι' αυτό υπάρχει ο decorator. - -```neon -decorator: - # για όλες τις υπηρεσίες που είναι παρουσίες αυτής της κλάσης ή interface - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # καλέστε αυτή τη μέθοδο - - $absoluteUrls = true # και ορίστε τη μεταβλητή -``` - -Ο decorator μπορεί επίσης να χρησιμοποιηθεί για τον ορισμό [tags |services#Tags] ή την ενεργοποίηση της λειτουργίας [inject |services#Λειτουργία Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Τεχνικές ρυθμίσεις του DI container. - -```neon -di: - # εμφάνιση του DIC στο Tracy Bar; - debugger: ... # (bool) η προεπιλογή είναι true - - # τύποι παραμέτρων που δεν γίνονται ποτέ autowired - excluded: ... # (string[]) - - # επιτρέπεται η lazy δημιουργία υπηρεσιών; - lazy: ... # (bool) η προεπιλογή είναι false - - # κλάση από την οποία κληρονομεί το DI container - parentClass: ... # (string) η προεπιλογή είναι Nette\DI\Container -``` - - -Lazy υπηρεσίες .{data-version:3.2.4} ------------------------------------- - -Η ρύθμιση `lazy: true` ενεργοποιεί τη lazy (καθυστερημένη) δημιουργία υπηρεσιών. Αυτό σημαίνει ότι οι υπηρεσίες δεν δημιουργούνται πραγματικά τη στιγμή που τις ζητάμε από το DI container, αλλά τη στιγμή της πρώτης τους χρήσης. Αυτό μπορεί να επιταχύνει την εκκίνηση της εφαρμογής και να μειώσει τις απαιτήσεις μνήμης, καθώς δημιουργούνται μόνο οι υπηρεσίες που είναι πραγματικά απαραίτητες στο συγκεκριμένο request. - -Για μια συγκεκριμένη υπηρεσία, η lazy δημιουργία μπορεί να [αλλάξει |services#Lazy υπηρεσίες]. - -.[note] -Τα lazy αντικείμενα μπορούν να χρησιμοποιηθούν μόνο για κλάσεις χρήστη, όχι για εσωτερικές κλάσεις PHP. Απαιτεί PHP 8.4 ή νεότερη έκδοση. - - -Εξαγωγή μεταδεδομένων ---------------------- - -Η κλάση του DI container περιέχει επίσης πολλά μεταδεδομένα. Μπορείτε να τη μειώσετε περιορίζοντας την εξαγωγή μεταδεδομένων. - -```neon -di: - export: - # εξαγωγή παραμέτρων; - parameters: false # (bool) η προεπιλογή είναι true - - # εξαγωγή tags και ποια; - tags: # (string[]|bool) η προεπιλογή είναι όλα - - event.subscriber - - # εξαγωγή δεδομένων για autowiring και ποια; - types: # (string[]|bool) η προεπιλογή είναι όλα - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Εάν δεν χρησιμοποιείτε τον πίνακα `$container->getParameters()`, μπορείτε να απενεργοποιήσετε την εξαγωγή παραμέτρων. Επιπλέον, μπορείτε να εξάγετε μόνο τα tags μέσω των οποίων λαμβάνετε υπηρεσίες με τη μέθοδο `$container->findByTag(...)`. Εάν δεν καλείτε καθόλου τη μέθοδο, μπορείτε να απενεργοποιήσετε εντελώς την εξαγωγή tags χρησιμοποιώντας `false`. - -Μπορείτε να μειώσετε σημαντικά τα μεταδεδομένα για [autowiring |autowiring] αναφέροντας τις κλάσεις που χρησιμοποιείτε ως παράμετρο της μεθόδου `$container->getByType()`. Και πάλι, εάν δεν καλείτε καθόλου τη μέθοδο (ή μόνο στο [bootstrap |application:bootstrapping] για να λάβετε το `Nette\Application\Application`), μπορείτε να απενεργοποιήσετε εντελώς την εξαγωγή χρησιμοποιώντας `false`. - - -Επεκτάσεις -========== - -Καταχώρηση πρόσθετων επεκτάσεων DI. Με αυτόν τον τρόπο προσθέτουμε, για παράδειγμα, την επέκταση DI `Dibi\Bridges\Nette\DibiExtension22` με το όνομα `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Στη συνέχεια, τη διαμορφώνουμε στην ενότητα `dibi`: - -```neon -dibi: - host: localhost -``` - -Ως επέκταση μπορεί να προστεθεί και μια κλάση που έχει παραμέτρους: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Εισαγωγή αρχείων -================ - -Μπορούμε να εισάγουμε άλλα αρχεία διαμόρφωσης στην ενότητα `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Το όνομα `parameters.php` δεν είναι τυπογραφικό λάθος, η διαμόρφωση μπορεί επίσης να γραφτεί σε ένα αρχείο PHP, το οποίο την επιστρέφει ως πίνακα: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Εάν εμφανιστούν στοιχεία με τα ίδια κλειδιά σε αρχεία διαμόρφωσης, θα αντικατασταθούν ή, στην περίπτωση [πινάκων, θα συγχωνευθούν |#Συγχώνευση]. Το αρχείο που εισάγεται αργότερα έχει υψηλότερη προτεραιότητα από το προηγούμενο. Το αρχείο στο οποίο αναφέρεται η ενότητα `includes` έχει υψηλότερη προτεραιότητα από τα αρχεία που εισάγονται σε αυτό. - - -Αναζήτηση -========= - -Η αυτόματη προσθήκη υπηρεσιών στο DI container διευκολύνει εξαιρετικά την εργασία. Το Nette προσθέτει αυτόματα presenters στο container, αλλά μπορεί εύκολα να προσθέσει και οποιεσδήποτε άλλες κλάσεις. - -Αρκεί να αναφέρετε σε ποιους καταλόγους (και υποκαταλόγους) πρέπει να αναζητήσει κλάσεις: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Συνήθως, όμως, δεν θέλουμε να προσθέσουμε απολύτως όλες τις κλάσεις και τα interfaces, γι' αυτό μπορούμε να τα φιλτράρουμε: - -```neon -search: - - in: %appDir%/Forms - - # φιλτράρισμα με βάση το όνομα αρχείου (string|string[]) - files: - - *Factory.php - - # φιλτράρισμα με βάση το όνομα κλάσης (string|string[]) - classes: - - *Factory -``` - -Ή μπορούμε να επιλέξουμε κλάσεις που κληρονομούν ή υλοποιούν τουλάχιστον μία από τις αναφερόμενες κλάσεις: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Μπορούν επίσης να οριστούν κανόνες εξαίρεσης, δηλ. μάσκες ονόματος κλάσης ή κληρονομικοί πρόγονοι, που εάν ταιριάζουν, η υπηρεσία δεν προστίθεται στο DI container: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Σε όλες τις υπηρεσίες μπορούν να οριστούν tags: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Συγχώνευση -========== - -Εάν εμφανιστούν στοιχεία με τα ίδια κλειδιά σε περισσότερα αρχεία διαμόρφωσης, θα αντικατασταθούν ή, στην περίπτωση πινάκων, θα συγχωνευθούν. Το αρχείο που εισάγεται αργότερα έχει υψηλότερη προτεραιότητα από το προηγούμενο. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>αποτέλεσμα</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Στους πίνακες, η συγχώνευση μπορεί να αποτραπεί αναφέροντας ένα θαυμαστικό μετά το όνομα του κλειδιού: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>αποτέλεσμα</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Διαμόρφωση Dependency Injection}} diff --git a/dependency-injection/el/container.texy b/dependency-injection/el/container.texy deleted file mode 100644 index 9b371ad0da..0000000000 --- a/dependency-injection/el/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Τι είναι το DI Container; -************************* - -.[perex] -Ένα dependency injection container (DIC) είναι μια κλάση που μπορεί να δημιουργήσει και να διαμορφώσει αντικείμενα. - -Μπορεί να σας εκπλήξει, αλλά σε πολλές περιπτώσεις δεν χρειάζεστε ένα dependency injection container για να επωφεληθείτε από το dependency injection (συντομογραφία DI). Άλλωστε, ακόμη και στο [εισαγωγικό κεφάλαιο |introduction] δείξαμε το DI με συγκεκριμένα παραδείγματα και δεν χρειαζόταν κανένα container. - -Ωστόσο, εάν χρειάζεται να διαχειριστείτε μεγάλο αριθμό διαφορετικών αντικειμένων με πολλές εξαρτήσεις, ένα dependency injection container θα είναι πραγματικά χρήσιμο. Αυτό ισχύει, για παράδειγμα, για τις web εφαρμογές που βασίζονται σε ένα framework. - -Στο προηγούμενο κεφάλαιο, παρουσιάσαμε τις κλάσεις `Article` και `UserController`. Και οι δύο έχουν κάποιες εξαρτήσεις, δηλαδή τη βάση δεδομένων και το factory `ArticleFactory`. Και για αυτές τις κλάσεις θα δημιουργήσουμε τώρα ένα container. Φυσικά, για ένα τόσο απλό παράδειγμα, δεν έχει νόημα να έχουμε ένα container. Αλλά θα το δημιουργήσουμε για να δείξουμε πώς μοιάζει και πώς λειτουργεί. - -Εδώ είναι ένα απλό hardcoded container για το αναφερόμενο παράδειγμα: - -```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()); - } -} -``` - -Η χρήση θα έμοιαζε ως εξής: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Απλώς ρωτάμε το container για το αντικείμενο και δεν χρειάζεται πλέον να γνωρίζουμε τίποτα για το πώς να το δημιουργήσουμε και ποιες είναι οι εξαρτήσεις του. όλα αυτά τα γνωρίζει το container. Οι εξαρτήσεις εισάγονται αυτόματα από το container. Σε αυτό έγκειται η δύναμή του. - -Το container έχει προς το παρόν όλα τα δεδομένα γραμμένα απευθείας στον κώδικα. Θα κάνουμε λοιπόν το επόμενο βήμα και θα προσθέσουμε παραμέτρους, ώστε το container να είναι πραγματικά χρήσιμο: - -```php -class Container -{ - public function __construct( - private array $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' => '***', -]); -``` - -Οι προσεκτικοί αναγνώστες μπορεί να έχουν παρατηρήσει ένα συγκεκριμένο πρόβλημα. Κάθε φορά που λαμβάνω ένα αντικείμενο `UserController`, δημιουργείται επίσης μια νέα παρουσία του `ArticleFactory` και της βάσης δεδομένων. Αυτό σίγουρα δεν το θέλουμε. - -Θα προσθέσουμε λοιπόν μια μέθοδο `getService()`, η οποία θα επιστρέφει πάντα τις ίδιες παρουσίες: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // το getService('Database') θα καλέσει το createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Κατά την πρώτη κλήση, π.χ. `$container->getService('Database')`, θα ζητήσει από το `createDatabase()` να δημιουργήσει το αντικείμενο της βάσης δεδομένων, το οποίο θα αποθηκεύσει στον πίνακα `$services` και κατά την επόμενη κλήση θα το επιστρέψει απευθείας. - -Θα τροποποιήσουμε και το υπόλοιπο container, ώστε να χρησιμοποιεί το `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Παρεμπιπτόντως, ο όρος υπηρεσία (service) αναφέρεται σε οποιοδήποτε αντικείμενο διαχειρίζεται το container. Γι' αυτό και το όνομα της μεθόδου `getService()`. - -Έτοιμο. Έχουμε ένα πλήρως λειτουργικό DI container! Και μπορούμε να το χρησιμοποιήσουμε: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Όπως βλέπετε, η σύνταξη ενός DIC δεν είναι κάτι περίπλοκο. Αξίζει να θυμηθούμε ότι τα ίδια τα αντικείμενα δεν γνωρίζουν ότι τα δημιουργεί κάποιο container. Έτσι, είναι δυνατόν να δημιουργηθεί με αυτόν τον τρόπο οποιοδήποτε αντικείμενο στην PHP χωρίς παρέμβαση στον πηγαίο κώδικά του. - -Η χειροκίνητη δημιουργία και συντήρηση της κλάσης του container μπορεί γρήγορα να γίνει εφιάλτης. Στο επόμενο κεφάλαιο, θα μιλήσουμε λοιπόν για το [Nette DI Container |nette-container], το οποίο μπορεί να δημιουργείται και να ενημερώνεται σχεδόν από μόνο του. - - -{{maintitle: Τι είναι το dependency injection container;}} diff --git a/dependency-injection/el/extensions.texy b/dependency-injection/el/extensions.texy deleted file mode 100644 index 3f37bb6398..0000000000 --- a/dependency-injection/el/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Δημιουργία επεκτάσεων για το Nette DI -************************************* - -.[perex] -Η δημιουργία του DI container, εκτός από τα αρχεία διαμόρφωσης, επηρεάζεται και από τις λεγόμενες *επεκτάσεις*. Τις ενεργοποιούμε στο αρχείο διαμόρφωσης στην ενότητα `extensions`. - -Έτσι προσθέτουμε την επέκταση που αντιπροσωπεύεται από την κλάση `BlogExtension` με το όνομα `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Κάθε επέκταση του compiler κληρονομεί από το [api:Nette\DI\CompilerExtension] και μπορεί να υλοποιήσει τις ακόλουθες μεθόδους, οι οποίες καλούνται διαδοχικά κατά τη συναρμολόγηση του DI container: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Αυτή η μέθοδος καλείται πρώτη. Ορίζει το schema για την επικύρωση των παραμέτρων διαμόρφωσης. - -Διαμορφώνουμε την επέκταση στην ενότητα της οποίας το όνομα είναι το ίδιο με αυτό με το οποίο προστέθηκε η επέκταση, δηλαδή `blog`: - -```neon -# ίδιο όνομα με την επέκταση -blog: - postsPerPage: 10 - allowComments: false -``` - -Δημιουργούμε ένα schema που περιγράφει όλες τις επιλογές διαμόρφωσης, συμπεριλαμβανομένων των τύπων τους, των επιτρεπόμενων τιμών και, ενδεχομένως, των προεπιλεγμένων τιμών τους: - -```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), - ]); - } -} -``` - -Θα βρείτε την τεκμηρίωση στη σελίδα [Schema |schema:]. Επιπλέον, μπορείτε να καθορίσετε ποιες επιλογές μπορούν να είναι [δυναμικές |application:bootstrapping#Δυναμικές Παράμετροι] χρησιμοποιώντας το `dynamic()`, π.χ. `Expect::int()->dynamic()`. - -Έχουμε πρόσβαση στη διαμόρφωση μέσω της μεταβλητής `$this->config`, η οποία είναι ένα αντικείμενο `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Χρησιμοποιείται για την προσθήκη υπηρεσιών στο container. Γι' αυτό χρησιμοποιείται το [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']); - } -} -``` - -Η σύμβαση είναι να προτάσσεται στις υπηρεσίες που προστίθενται από την επέκταση το όνομά της, ώστε να μην προκύπτουν συγκρούσεις ονομάτων. Αυτό το κάνει η μέθοδος `prefix()`, οπότε αν η επέκταση ονομάζεται `blog`, η υπηρεσία θα ονομάζεται `blog.articles`. - -Εάν χρειαστεί να μετονομάσουμε μια υπηρεσία, μπορούμε, για λόγους διατήρησης της συμβατότητας προς τα πίσω, να δημιουργήσουμε ένα ψευδώνυμο (alias) με το αρχικό όνομα. Παρόμοια το κάνει το Nette, π.χ. για την υπηρεσία `routing.router`, η οποία είναι διαθέσιμη και με το προηγούμενο όνομα `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Φόρτωση υπηρεσιών από αρχείο ----------------------------- - -Δεν χρειάζεται να δημιουργούμε υπηρεσίες μόνο μέσω του API της κλάσης ContainerBuilder, αλλά και με τη γνωστή σύνταξη που χρησιμοποιείται στο αρχείο διαμόρφωσης NEON στην ενότητα services. Το πρόθεμα `@extension` αντιπροσωπεύει την τρέχουσα επέκταση. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Φορτώνουμε τις υπηρεσίες: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // φόρτωση του αρχείου διαμόρφωσης για την επέκταση - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Η μέθοδος καλείται τη στιγμή που το container περιέχει όλες τις υπηρεσίες που προστέθηκαν από τις μεμονωμένες επεκτάσεις στις μεθόδους `loadConfiguration` καθώς και από τα αρχεία διαμόρφωσης χρήστη. Σε αυτή τη φάση της συναρμολόγησης, μπορούμε λοιπόν να τροποποιήσουμε τους ορισμούς των υπηρεσιών ή να συμπληρώσουμε τις συνδέσεις μεταξύ τους. Για την αναζήτηση υπηρεσιών στο container με βάση τα tags, μπορεί να χρησιμοποιηθεί η μέθοδος `findByTag()`, ενώ με βάση την κλάση ή το interface, η μέθοδος `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] -======================== - -Σε αυτή τη φάση, η κλάση του container έχει ήδη δημιουργηθεί με τη μορφή αντικειμένου [ClassType |php-generator:#Κλάσεις], περιέχει όλες τις μεθόδους που δημιουργούν τις υπηρεσίες και είναι έτοιμη για εγγραφή στην cache. Τον τελικό κώδικα της κλάσης μπορούμε ακόμα να τον τροποποιήσουμε σε αυτή τη στιγμή. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -Η κλάση Configurator, μετά τη [δημιουργία του container |application:bootstrapping#index.php], καλεί τον κώδικα αρχικοποίησης, ο οποίος δημιουργείται με εγγραφή στο αντικείμενο `$this->initialization` χρησιμοποιώντας τη [μέθοδο addBody() |php-generator:#Σώματα μεθόδων και συναρτήσεων]. - -Ας δείξουμε ένα παράδειγμα για το πώς, για παράδειγμα, με τον κώδικα αρχικοποίησης να ξεκινήσουμε τη session ή να εκκινήσουμε υπηρεσίες που έχουν το tag `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // αυτόματη εκκίνηση της session - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // οι υπηρεσίες με tag run πρέπει να δημιουργηθούν μετά την παρουσίαση του container - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/el/factory.texy b/dependency-injection/el/factory.texy deleted file mode 100644 index 54f04fde20..0000000000 --- a/dependency-injection/el/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Δημιουργημένα Factories -*********************** - -.[perex] -Το Nette DI μπορεί να δημιουργήσει αυτόματα κώδικα factory βάσει interfaces, εξοικονομώντας σας τη συγγραφή κώδικα. - -Ένα factory είναι μια κλάση που παράγει και διαμορφώνει αντικείμενα. Τους περνάει δηλαδή και τις εξαρτήσεις τους. Μην το συγχέετε με το σχεδιαστικό πρότυπο *factory method*, το οποίο περιγράφει έναν συγκεκριμένο τρόπο χρήσης των factories και δεν σχετίζεται με αυτό το θέμα. - -Πώς μοιάζει ένα τέτοιο factory, το δείξαμε στο [εισαγωγικό κεφάλαιο |introduction#Factory]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Το Nette DI μπορεί να δημιουργήσει αυτόματα τον κώδικα των factories. Το μόνο που έχετε να κάνετε είναι να δημιουργήσετε ένα interface και το Nette DI θα δημιουργήσει την υλοποίηση. Το interface πρέπει να έχει ακριβώς μία μέθοδο με το όνομα `create` και να δηλώνει τον τύπο επιστροφής: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Δηλαδή, το factory `ArticleFactory` έχει μια μέθοδο `create`, η οποία δημιουργεί αντικείμενα `Article`. Η κλάση `Article` μπορεί να μοιάζει κάπως έτσι: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Προσθέτουμε το factory στο αρχείο διαμόρφωσης: - -```neon -services: - - ArticleFactory -``` - -Το Nette DI θα δημιουργήσει την αντίστοιχη υλοποίηση του factory. - -Στον κώδικα που χρησιμοποιεί το factory, ζητάμε λοιπόν το αντικείμενο σύμφωνα με το interface και το Nette DI θα χρησιμοποιήσει τη δημιουργημένη υλοποίηση: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // αφήνουμε το factory να δημιουργήσει το αντικείμενο - $article = $this->articleFactory->create(); - } -} -``` - - -Παραμετροποιημένο factory -========================= - -Η μέθοδος του factory `create` μπορεί να δέχεται παραμέτρους, τις οποίες στη συνέχεια περνά στον κατασκευαστή. Ας συμπληρώσουμε, για παράδειγμα, την κλάση `Article` με το ID του συγγραφέα του άρθρου: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Προσθέτουμε την παράμετρο και στο factory: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Χάρη στο γεγονός ότι η παράμετρος στον κατασκευαστή και η παράμετρος στο factory ονομάζονται το ίδιο, το Nette DI τις περνά εντελώς αυτόματα. - - -Προηγμένος ορισμός -================== - -Ο ορισμός μπορεί να γραφτεί και σε πολυγραμμική μορφή χρησιμοποιώντας το κλειδί `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Κατά τη σύνταξη με αυτόν τον μακρύτερο τρόπο, είναι δυνατόν να αναφερθούν επιπλέον ορίσματα για τον κατασκευαστή στο κλειδί `arguments` και συμπληρωματική διαμόρφωση μέσω του `setup`, όπως και στις κανονικές υπηρεσίες. - -Παράδειγμα: εάν η μέθοδος `create()` δεν δεχόταν την παράμετρο `$authorId`, θα μπορούσαμε να δηλώσουμε μια σταθερή τιμή στη διαμόρφωση, η οποία θα περνούσε στον κατασκευαστή του `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Ή αντίστροφα, εάν η `create()` δεχόταν την παράμετρο `$authorId`, αλλά δεν ήταν μέρος του κατασκευαστή και περνούσε μέσω της μεθόδου `Article::setAuthorId()`, θα αναφερόμασταν σε αυτήν στην ενότητα `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Το Nette μπορεί, εκτός από factories, να δημιουργεί και τα λεγόμενα accessors. Πρόκειται για αντικείμενα με μια μέθοδο `get()`, η οποία επιστρέφει μια συγκεκριμένη υπηρεσία από το DI container. Η επανειλημμένη κλήση του `get()` επιστρέφει πάντα την ίδια παρουσία. - -Οι accessors παρέχουν lazy-loading στις εξαρτήσεις. Ας υποθέσουμε ότι έχουμε μια κλάση που καταγράφει σφάλματα σε μια ειδική βάση δεδομένων. Εάν αυτή η κλάση λάμβανε τη σύνδεση με τη βάση δεδομένων ως εξάρτηση μέσω του κατασκευαστή, η σύνδεση θα έπρεπε πάντα να δημιουργείται, παρόλο που στην πράξη ένα σφάλμα εμφανίζεται μόνο σπάνια και, επομένως, τις περισσότερες φορές η σύνδεση θα παρέμενε αχρησιμοποίητη. Αντί γι' αυτό, η κλάση περνά έναν accessor και μόνο όταν κληθεί το `get()` του, δημιουργείται το αντικείμενο της βάσης δεδομένων: - -Πώς να δημιουργήσετε έναν accessor; Αρκεί να γράψετε ένα interface και το Nette DI θα δημιουργήσει την υλοποίηση. Το interface πρέπει να έχει ακριβώς μία μέθοδο με το όνομα `get` και να δηλώνει τον τύπο επιστροφής: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Προσθέτουμε τον accessor στο αρχείο διαμόρφωσης, όπου ορίζεται επίσης η υπηρεσία που θα επιστρέφει: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Επειδή ο accessor επιστρέφει μια υπηρεσία τύπου `PDO` και στη διαμόρφωση υπάρχει μόνο μία τέτοια υπηρεσία, θα επιστρέφει ακριβώς αυτήν. Εάν υπήρχαν περισσότερες υπηρεσίες αυτού του τύπου, θα καθορίζαμε την επιστρεφόμενη υπηρεσία χρησιμοποιώντας το όνομα, π.χ. `- PDOAccessor(@db1)`. - - -Πολλαπλό factory/accessor -========================= -Τα factories και οι accessors μας μπορούσαν μέχρι τώρα πάντα να παράγουν ή να επιστρέφουν μόνο ένα αντικείμενο. Ωστόσο, είναι πολύ εύκολο να δημιουργηθούν και πολλαπλά factories συνδυασμένα με accessors. Το interface μιας τέτοιας κλάσης θα περιέχει οποιονδήποτε αριθμό μεθόδων με ονόματα `create<name>()` και `get<name>()`, π.χ.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Έτσι, αντί να περνάμε πολλά δημιουργημένα factories και accessors, περνάμε ένα πιο σύνθετο factory που μπορεί να κάνει περισσότερα. - -Εναλλακτικά, αντί για πολλές μεθόδους, μπορούμε να χρησιμοποιήσουμε το `get()` με παράμετρο: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Τότε ισχύει ότι το `MultiFactory::getArticle()` κάνει το ίδιο πράγμα με το `MultiFactoryAlt::get('article')`. Ωστόσο, η εναλλακτική σύνταξη έχει το μειονέκτημα ότι δεν είναι σαφές ποιες τιμές `$name` υποστηρίζονται και λογικά δεν είναι δυνατό στο interface να διακριθούν διαφορετικές τιμές επιστροφής για διαφορετικά `$name`. - - -Ορισμός με λίστα ----------------- -Με αυτόν τον τρόπο μπορεί να οριστεί ένα πολλαπλό factory στη διαμόρφωση: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # ορίζει το createArticle() - db: PDO(%dsn%, %user%, %password%) # ορίζει το getDb() - ) -``` - -Ή μπορούμε στον ορισμό του factory να αναφερθούμε σε υπάρχουσες υπηρεσίες μέσω αναφοράς: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # ορίζει το createArticle() - db: @\PDO # ορίζει το getDb() - ) -``` - - -Ορισμός με tags ---------------- - -Η δεύτερη επιλογή είναι να χρησιμοποιήσουμε για τον ορισμό [tags |services#Tags]: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/el/faq.texy b/dependency-injection/el/faq.texy deleted file mode 100644 index ba6c075467..0000000000 --- a/dependency-injection/el/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Συχνές ερωτήσεις για το DI (FAQ) -******************************** - - -Είναι το DI άλλο όνομα για το IoC; ----------------------------------- - -Το *Inversion of Control* (IoC) είναι μια αρχή που εστιάζει στον τρόπο εκτέλεσης του κώδικα - εάν ο κώδικάς σας εκτελεί ξένο κώδικα ή εάν ο κώδικάς σας ενσωματώνεται σε ξένο κώδικα, ο οποίος στη συνέχεια τον καλεί. Το IoC είναι ένας ευρύς όρος που περιλαμβάνει [γεγονότα |nette:glossary#Events], το λεγόμενο [Hollywood principle |application:components#Hollywood Style] και άλλες πτυχές. Μέρος αυτής της έννοιας είναι και τα factories, για τα οποία μιλά ο [Κανόνας #3: άφησέ το στο factory |introduction#Κανόνας αρ. 3: άφησέ το στο factory], και τα οποία αντιπροσωπεύουν μια αντιστροφή για τον τελεστή `new`. - -Το *Dependency Injection* (DI) εστιάζει στον τρόπο με τον οποίο ένα αντικείμενο μαθαίνει για ένα άλλο αντικείμενο, δηλαδή για τις εξαρτήσεις του. Πρόκειται για ένα σχεδιαστικό πρότυπο που απαιτεί τη ρητή μεταβίβαση εξαρτήσεων μεταξύ αντικειμένων. - -Μπορούμε λοιπόν να πούμε ότι το DI είναι μια συγκεκριμένη μορφή IoC. Ωστόσο, δεν είναι όλες οι μορφές IoC κατάλληλες από την άποψη της καθαρότητας του κώδικα. Για παράδειγμα, μεταξύ των αντι-προτύπων (antipatterns) περιλαμβάνονται τεχνικές που λειτουργούν με [καθολική κατάσταση |global-state] ή το λεγόμενο [Service Locator |#Τι είναι το Service Locator]. - - -Τι είναι το Service Locator; ----------------------------- - -Πρόκειται για μια εναλλακτική λύση στο Dependency Injection. Λειτουργεί δημιουργώντας ένα κεντρικό αποθετήριο όπου καταχωρούνται όλες οι διαθέσιμες υπηρεσίες ή εξαρτήσεις. Όταν ένα αντικείμενο χρειάζεται μια εξάρτηση, τη ζητά από το Service Locator. - -Σε σύγκριση με το Dependency Injection, ωστόσο, χάνει σε διαφάνεια: οι εξαρτήσεις δεν περνούν απευθείας στα αντικείμενα και δεν είναι τόσο εύκολα αναγνωρίσιμες, πράγμα που απαιτεί την εξέταση του κώδικα για να αποκαλυφθούν και να κατανοηθούν όλες οι συνδέσεις. Ο έλεγχος (testing) είναι επίσης πιο περίπλοκος, επειδή δεν μπορούμε απλώς να περάσουμε mock αντικείμενα στα υπό έλεγχο αντικείμενα, αλλά πρέπει να το κάνουμε μέσω του Service Locator. Επιπλέον, το Service Locator διαταράσσει τον σχεδιασμό του κώδικα, καθώς τα μεμονωμένα αντικείμενα πρέπει να γνωρίζουν την ύπαρξή του, πράγμα που διαφέρει από το Dependency Injection, όπου τα αντικείμενα δεν έχουν επίγνωση του DI container. - - -Πότε είναι καλύτερο να μην χρησιμοποιηθεί το DI; ------------------------------------------------- - -Δεν είναι γνωστές δυσκολίες που να σχετίζονται με τη χρήση του σχεδιαστικού προτύπου Dependency Injection. Αντίθετα, η λήψη εξαρτήσεων από καθολικά διαθέσιμα σημεία οδηγεί σε [μια ολόκληρη σειρά επιπλοκών |global-state], όπως και η χρήση του Service Locator. Επομένως, είναι σκόπιμο να χρησιμοποιείται πάντα το DI. Αυτό δεν είναι μια δογματική προσέγγιση, αλλά απλώς δεν έχει βρεθεί καλύτερη εναλλακτική λύση. - -Παρ' όλα αυτά, υπάρχουν ορισμένες καταστάσεις όπου δεν περνάμε τα αντικείμενα και τα λαμβάνουμε από τον καθολικό χώρο. Για παράδειγμα, κατά τον εντοπισμό σφαλμάτων στον κώδικα, όταν χρειάζεται να εκτυπώσετε την τιμή μιας μεταβλητής σε ένα συγκεκριμένο σημείο του προγράμματος, να μετρήσετε τη διάρκεια ενός συγκεκριμένου τμήματος του προγράμματος ή να καταγράψετε ένα μήνυμα. Σε τέτοιες περιπτώσεις, όπου πρόκειται για προσωρινές ενέργειες που θα αφαιρεθούν αργότερα από τον κώδικα, είναι θεμιτό να χρησιμοποιηθεί ένας καθολικά διαθέσιμος dumper, χρονόμετρο ή logger. Αυτά τα εργαλεία, δηλαδή, δεν ανήκουν στον σχεδιασμό του κώδικα. - - -Έχει η χρήση του DI τα μειονεκτήματά της; ------------------------------------------ - -Συνεπάγεται η χρήση του Dependency Injection κάποια μειονεκτήματα, όπως για παράδειγμα αυξημένη δυσκολία στη συγγραφή κώδικα ή χειρότερη απόδοση; Τι χάνουμε όταν αρχίζουμε να γράφουμε κώδικα σύμφωνα με το DI; - -Το DI δεν επηρεάζει την απόδοση ή τις απαιτήσεις μνήμης της εφαρμογής. Ορισμένο ρόλο μπορεί να παίξει η απόδοση του DI Container, ωστόσο στην περίπτωση του [Nette DI |nette-container], το container μεταγλωττίζεται σε καθαρή PHP, οπότε η επιβάρυνσή του κατά την εκτέλεση της εφαρμογής είναι ουσιαστικά μηδενική. - -Κατά τη συγγραφή κώδικα, συχνά είναι απαραίτητο να δημιουργηθούν κατασκευαστές που δέχονται εξαρτήσεις. Παλαιότερα αυτό μπορούσε να είναι χρονοβόρο, ωστόσο χάρη στα σύγχρονα IDE και το [constructor property promotion |https://blog.nette.org/el/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], είναι πλέον θέμα δευτερολέπτων. Τα factories μπορούν εύκολα να δημιουργηθούν με το Nette DI και το plugin για το PhpStorm με ένα κλικ του ποντικιού. Από την άλλη πλευρά, εξαλείφεται η ανάγκη συγγραφής singletons και στατικών σημείων πρόσβασης. - -Μπορούμε να συμπεράνουμε ότι μια σωστά σχεδιασμένη εφαρμογή που χρησιμοποιεί DI δεν είναι ούτε συντομότερη ούτε μακρύτερη σε σύγκριση με μια εφαρμογή που χρησιμοποιεί singletons. Τα τμήματα του κώδικα που εργάζονται με εξαρτήσεις απλώς αφαιρούνται από τις μεμονωμένες κλάσεις και μεταφέρονται σε νέα σημεία, δηλαδή στο DI container και στα factories. - - -Πώς να ξαναγράψετε μια legacy εφαρμογή σε DI; ---------------------------------------------- - -Η μετάβαση από μια legacy εφαρμογή στο Dependency Injection μπορεί να είναι μια απαιτητική διαδικασία, ειδικά σε μεγάλες και πολύπλοκες εφαρμογές. Είναι σημαντικό να προσεγγίσετε αυτή τη διαδικασία συστηματικά. - -- Κατά τη μετάβαση στο Dependency Injection, είναι σημαντικό όλα τα μέλη της ομάδας να κατανοούν τις αρχές και τις διαδικασίες που χρησιμοποιούνται. -- Πρώτα, πραγματοποιήστε μια ανάλυση της υπάρχουσας εφαρμογής και εντοπίστε τα βασικά στοιχεία και τις εξαρτήσεις τους. Δημιουργήστε ένα σχέδιο για το ποια τμήματα θα αναδιαρθρωθούν και με ποια σειρά. -- Υλοποιήστε ένα DI container ή, ακόμα καλύτερα, χρησιμοποιήστε μια υπάρχουσα βιβλιοθήκη, για παράδειγμα το Nette DI. -- Σταδιακά αναδιαρθρώστε τα μεμονωμένα τμήματα της εφαρμογής ώστε να χρησιμοποιούν το Dependency Injection. Αυτό μπορεί να περιλαμβάνει τροποποιήσεις των κατασκευαστών ή των μεθόδων ώστε να δέχονται εξαρτήσεις ως παραμέτρους. -- Τροποποιήστε τα σημεία στον κώδικα όπου δημιουργούνται αντικείμενα με εξαρτήσεις, ώστε αντί γι' αυτό οι εξαρτήσεις να εισάγονται από το container. Αυτό μπορεί να περιλαμβάνει τη χρήση factories. - -Θυμηθείτε ότι η μετάβαση στο Dependency Injection είναι μια επένδυση στην ποιότητα του κώδικα και τη μακροπρόθεσμη συντηρησιμότητα της εφαρμογής. Αν και μπορεί να είναι δύσκολο να πραγματοποιηθούν αυτές οι αλλαγές, το αποτέλεσμα θα πρέπει να είναι ένας καθαρότερος, πιο αρθρωτός και εύκολα ελεγχόμενος κώδικας, ο οποίος είναι έτοιμος για μελλοντική επέκταση και συντήρηση. - - -Γιατί προτιμάται η σύνθεση (composition) έναντι της κληρονομικότητας; ---------------------------------------------------------------------- -Είναι προτιμότερο να χρησιμοποιείται η [σύνθεση |nette:introduction-to-object-oriented-programming#Σύνθεση] αντί της [κληρονομικότητας |nette:introduction-to-object-oriented-programming#Κληρονομικότητα], επειδή χρησιμεύει στην επαναχρησιμοποίηση του κώδικα, χωρίς να χρειάζεται να ανησυχούμε για τις συνέπειες των αλλαγών. Παρέχει δηλαδή μια πιο χαλαρή σύνδεση, όπου δεν χρειάζεται να φοβόμαστε ότι η αλλαγή κάποιου κώδικα θα προκαλέσει την ανάγκη αλλαγής άλλου εξαρτώμενου κώδικα. Τυπικό παράδειγμα είναι η κατάσταση που ονομάζεται [constructor hell |passing-dependencies#Constructor hell]. - - -Μπορεί να χρησιμοποιηθεί το Nette DI Container εκτός του Nette; ---------------------------------------------------------------- - -Σίγουρα. Το Nette DI Container είναι μέρος του Nette, αλλά έχει σχεδιαστεί ως μια αυτόνομη βιβλιοθήκη που μπορεί να χρησιμοποιηθεί ανεξάρτητα από τα υπόλοιπα μέρη του framework. Αρκεί να την εγκαταστήσετε μέσω του Composer, να δημιουργήσετε ένα αρχείο διαμόρφωσης με τον ορισμό των υπηρεσιών σας και στη συνέχεια, με λίγες γραμμές κώδικα PHP, να δημιουργήσετε το DI container. Και αμέσως μπορείτε να αρχίσετε να επωφελείστε από το Dependency Injection στα έργα σας. - -Πώς μοιάζει η συγκεκριμένη χρήση, συμπεριλαμβανομένων των κωδίκων, περιγράφεται στο κεφάλαιο [Nette DI Container |nette-container]. - - -Γιατί η διαμόρφωση είναι σε αρχεία NEON; ----------------------------------------- - -Το NEON είναι μια απλή και ευανάγνωστη γλώσσα διαμόρφωσης, η οποία αναπτύχθηκε στο πλαίσιο του Nette για τη ρύθμιση εφαρμογών, υπηρεσιών και των εξαρτήσεών τους. Σε σύγκριση με το JSON ή το YAML, προσφέρει για τον σκοπό αυτό πολύ πιο διαισθητικές και ευέλικτες δυνατότητες. Στο NEON μπορούν να περιγραφούν φυσικά συνδέσεις, οι οποίες στο Symfony & YAMLu δεν θα ήταν δυνατόν να γραφτούν είτε καθόλου, είτε μόνο μέσω πολύπλοκης περιγραφής. - - -Δεν επιβραδύνει την εφαρμογή η ανάλυση (parsing) των αρχείων NEON; ------------------------------------------------------------------- - -Παρόλο που τα αρχεία NEON αναλύονται πολύ γρήγορα, αυτή η πτυχή δεν έχει καμία σημασία. Ο λόγος είναι ότι η ανάλυση των αρχείων πραγματοποιείται μόνο μία φορά κατά την πρώτη εκκίνηση της εφαρμογής. Στη συνέχεια, δημιουργείται ο κώδικας του DI container, αποθηκεύεται στον δίσκο και εκτελείται σε κάθε επόμενο αίτημα, χωρίς να είναι απαραίτητη η περαιτέρω ανάλυση. - -Έτσι λειτουργεί στο περιβάλλον παραγωγής. Κατά την ανάπτυξη, τα αρχεία NEON αναλύονται κάθε φορά που αλλάζει το περιεχόμενό τους, ώστε ο προγραμματιστής να έχει πάντα τον τρέχοντα DI container. Η ίδια η ανάλυση είναι, όπως ειπώθηκε, θέμα στιγμής. - - -Πώς μπορώ να αποκτήσω πρόσβαση στις παραμέτρους στο αρχείο διαμόρφωσης από την κλάση μου; ------------------------------------------------------------------------------------------ - -Ας θυμηθούμε τον [Κανόνα #1: άφησέ το να σου περαστεί |introduction#Κανόνας αρ. 1: αφήστε το να σας παραδοθεί]. Εάν η κλάση απαιτεί πληροφορίες από το αρχείο διαμόρφωσης, δεν χρειάζεται να σκεφτούμε πώς να φτάσουμε σε αυτές τις πληροφορίες, αντίθετα απλώς τις ζητάμε - για παράδειγμα, μέσω του κατασκευαστή της κλάσης. Και πραγματοποιούμε τη μεταβίβαση στο αρχείο διαμόρφωσης. - -Σε αυτό το παράδειγμα, το `%myParameter%` είναι ένα placeholder για την τιμή της παραμέτρου `myParameter`, η οποία περνά στον κατασκευαστή της κλάσης `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Για να περάσετε πολλαπλές παραμέτρους ή να χρησιμοποιήσετε autowiring, είναι σκόπιμο να [ενσωματώσετε τις παραμέτρους σε ένα αντικείμενο |best-practices:passing-settings-to-presenters]. - - -Υποστηρίζει το Nette το PSR-11: Container interface; ----------------------------------------------------- - -Το Nette DI Container δεν υποστηρίζει απευθείας το PSR-11. Ωστόσο, εάν χρειάζεστε διαλειτουργικότητα μεταξύ του Nette DI Container και βιβλιοθηκών ή frameworks που αναμένουν το PSR-11 Container Interface, μπορείτε να δημιουργήσετε έναν [απλό προσαρμογέα |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], ο οποίος θα χρησιμεύσει ως γέφυρα μεταξύ του Nette DI Container και του PSR-11. diff --git a/dependency-injection/el/global-state.texy b/dependency-injection/el/global-state.texy deleted file mode 100644 index 5207b38638..0000000000 --- a/dependency-injection/el/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Καθολική κατάσταση και singletons -********************************* - -.[perex] -Προειδοποίηση: Οι ακόλουθες κατασκευές είναι σημάδι κακώς σχεδιασμένου κώδικα: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` ή `static::$var` - -Εμφανίζονται κάποιες από αυτές τις κατασκευές στον κώδικά σας; Τότε έχετε την ευκαιρία να τον βελτιώσετε. Ίσως σκέφτεστε ότι πρόκειται για συνήθεις κατασκευές, τις οποίες βλέπετε ίσως και σε παραδείγματα λύσεων διαφόρων βιβλιοθηκών και frameworks. Αν ισχύει αυτό, τότε ο σχεδιασμός του κώδικά τους δεν είναι καλός. - -Τώρα σίγουρα δεν μιλάμε για κάποια ακαδημαϊκή καθαρότητα. Όλες αυτές οι κατασκευές έχουν ένα κοινό: χρησιμοποιούν καθολική κατάσταση. Και αυτή έχει καταστροφική επίδραση στην ποιότητα του κώδικα. Οι κλάσεις λένε ψέματα για τις εξαρτήσεις τους. Ο κώδικας γίνεται απρόβλεπτος. Μπερδεύει τους προγραμματιστές και μειώνει την αποδοτικότητά τους. - -Σε αυτό το κεφάλαιο θα εξηγήσουμε γιατί συμβαίνει αυτό και πώς να αποφύγετε την καθολική κατάσταση. - - -Καθολική σύζευξη ----------------- - -Σε έναν ιδανικό κόσμο, ένα αντικείμενο θα έπρεπε να μπορεί να επικοινωνεί μόνο με αντικείμενα που του έχουν [περαστεί απευθείας |passing-dependencies]. Εάν δημιουργήσω δύο αντικείμενα `A` και `B` και ποτέ δεν περάσω αναφορά μεταξύ τους, τότε ούτε το `A`, ούτε το `B`, μπορούν να φτάσουν στο άλλο αντικείμενο ή να αλλάξουν την κατάστασή του. Αυτό είναι ένα πολύ επιθυμητό χαρακτηριστικό του κώδικα. Είναι παρόμοιο με το να έχετε μια μπαταρία και μια λάμπα. η λάμπα δεν θα ανάψει αν δεν τη συνδέσετε με την μπαταρία με ένα καλώδιο. - -Αυτό όμως δεν ισχύει για τις καθολικές (στατικές) μεταβλητές ή τα singletons. Το αντικείμενο `A` θα μπορούσε *ασύρματα* να φτάσει στο αντικείμενο `C` και να το τροποποιήσει χωρίς καμία μεταβίβαση αναφοράς, καλώντας το `C::changeSomething()`. Εάν το αντικείμενο `B` αρπάξει επίσης το καθολικό `C`, τότε τα `A` και `B` μπορούν να αλληλεπιδράσουν μέσω του `C`. - -Η χρήση καθολικών μεταβλητών εισάγει στο σύστημα μια νέα μορφή *ασύρματης* σύζευξης, η οποία δεν είναι ορατή από έξω. Δημιουργεί ένα παραπέτασμα καπνού που περιπλέκει την κατανόηση και τη χρήση του κώδικα. Για να κατανοήσουν πραγματικά οι προγραμματιστές τις εξαρτήσεις, πρέπει να διαβάσουν κάθε γραμμή του πηγαίου κώδικα. Αντί απλώς να εξοικειωθούν με τα interfaces των κλάσεων. Επιπλέον, πρόκειται για μια εντελώς περιττή σύζευξη. Η καθολική κατάσταση χρησιμοποιείται επειδή είναι εύκολα προσβάσιμη από οπουδήποτε και επιτρέπει, για παράδειγμα, την εγγραφή στη βάση δεδομένων μέσω της καθολικής (στατικής) μεθόδου `DB::insert()`. Αλλά όπως θα δείξουμε, το πλεονέκτημα που προσφέρει είναι ασήμαντο, ενώ αντίθετα οι επιπλοκές που προκαλεί είναι μοιραίες. - -.[note] -Από την άποψη της συμπεριφοράς, δεν υπάρχει διαφορά μεταξύ καθολικής και στατικής μεταβλητής. Είναι εξίσου επιβλαβείς. - - -Απόκοσμη δράση από απόσταση ---------------------------- - -"Απόκοσμη δράση από απόσταση" (Spooky action at a distance) - έτσι ονόμασε περίφημα το 1935 ο Άλμπερτ Αϊνστάιν ένα φαινόμενο στην κβαντική φυσική που του προκαλούσε ανατριχίλα. -Πρόκειται για την κβαντική διεμπλοκή, της οποίας η ιδιαιτερότητα είναι ότι όταν μετράτε πληροφορίες για ένα σωματίδιο, επηρεάζετε αμέσως το άλλο σωματίδιο, ακόμα κι αν απέχουν εκατομμύρια έτη φωτός. Αυτό φαινομενικά παραβιάζει τον θεμελιώδη νόμο του σύμπαντος ότι τίποτα δεν μπορεί να ταξιδέψει γρηγορότερα από το φως. - -Στον κόσμο του λογισμικού, μπορούμε να ονομάσουμε "απόκοσμη δράση από απόσταση" μια κατάσταση όπου εκκινούμε μια διαδικασία, την οποία θεωρούμε απομονωμένη (επειδή δεν της περάσαμε καμία αναφορά), αλλά σε απομακρυσμένα σημεία του συστήματος συμβαίνουν απροσδόκητες αλληλεπιδράσεις και αλλαγές κατάστασης, για τις οποίες δεν είχαμε ιδέα. Αυτό μπορεί να συμβεί μόνο μέσω της καθολικής κατάστασης. - -Φανταστείτε ότι εντάσσεστε σε μια ομάδα προγραμματιστών ενός έργου που έχει μια εκτεταμένη, ώριμη βάση κώδικα. Ο νέος σας προϊστάμενος σας ζητά να υλοποιήσετε μια νέα λειτουργία και εσείς, ως σωστός προγραμματιστής, ξεκινάτε γράφοντας ένα τεστ. Επειδή όμως είστε νέοι στο έργο, κάνετε πολλά διερευνητικά τεστ του τύπου "τι θα συμβεί αν καλέσω αυτή τη μέθοδο". Και δοκιμάζετε να γράψετε το ακόλουθο τεστ: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // ο αριθμός της κάρτας σας - $cc->charge(100); -} -``` - -Εκτελείτε τον κώδικα, ίσως αρκετές φορές, και μετά από λίγο παρατηρείτε ειδοποιήσεις στο κινητό σας από την τράπεζα ότι κάθε φορά που εκτελείται, χρεώνονται 100 δολάρια από την πιστωτική σας κάρτα 🤦‍♂️ - -Πώς στο καλό μπόρεσε το τεστ να προκαλέσει πραγματική χρέωση χρημάτων; Η λειτουργία με πιστωτική κάρτα δεν είναι εύκολη. Πρέπει να επικοινωνήσετε με μια web υπηρεσία τρίτου μέρους, πρέπει να γνωρίζετε τη διεύθυνση URL αυτής της web υπηρεσίας, πρέπει να συνδεθείτε και ούτω καθεξής. Καμία από αυτές τις πληροφορίες δεν περιέχεται στο τεστ. Ακόμα χειρότερα, ούτε καν γνωρίζετε πού βρίσκονται αυτές οι πληροφορίες, και επομένως ούτε πώς να κάνετε mock τις εξωτερικές εξαρτήσεις, ώστε κάθε εκτέλεση να μην οδηγεί ξανά σε χρέωση 100 δολαρίων. Και πώς έπρεπε να γνωρίζετε, ως νέος προγραμματιστής, ότι αυτό που ετοιμαζόσασταν να κάνετε θα οδηγούσε στο να γίνετε 100 δολάρια φτωχότεροι; - -Αυτή είναι η απόκοσμη δράση από απόσταση! - -Δεν σας μένει παρά να ψάξετε για πολλή ώρα σε πολλούς πηγαίους κώδικες, να ρωτήσετε παλαιότερους και πιο έμπειρους συναδέλφους, μέχρι να καταλάβετε πώς λειτουργούν οι συνδέσεις στο έργο. Αυτό οφείλεται στο ότι, κοιτάζοντας το interface της κλάσης `CreditCard`, δεν μπορείτε να προσδιορίσετε την καθολική κατάσταση που πρέπει να αρχικοποιηθεί. Ακόμη και η ματιά στον πηγαίο κώδικα της κλάσης δεν σας αποκαλύπτει ποια μέθοδο αρχικοποίησης πρέπει να καλέσετε. Στην καλύτερη περίπτωση, μπορείτε να βρείτε μια καθολική μεταβλητή στην οποία γίνεται πρόσβαση και από αυτήν να προσπαθήσετε να μαντέψετε πώς να την αρχικοποιήσετε. - -Οι κλάσεις σε ένα τέτοιο έργο είναι παθολογικοί ψεύτες. Η πιστωτική κάρτα προσποιείται ότι αρκεί να την παρουσιάσετε και να καλέσετε τη μέθοδο `charge()`. Κρυφά, όμως, συνεργάζεται με μια άλλη κλάση `PaymentGateway`, η οποία αντιπροσωπεύει την πύλη πληρωμών. Ακόμη και το interface της λέει ότι μπορεί να αρχικοποιηθεί ξεχωριστά, αλλά στην πραγματικότητα αντλεί διαπιστευτήρια από κάποιο αρχείο διαμόρφωσης και ούτω καθεξής. Για τους προγραμματιστές που έγραψαν αυτόν τον κώδικα, είναι σαφές ότι η `CreditCard` χρειάζεται την `PaymentGateway`. Έγραψαν τον κώδικα με αυτόν τον τρόπο. Αλλά για οποιονδήποτε είναι νέος στο έργο, είναι ένα απόλυτο μυστήριο και εμποδίζει τη μάθηση. - -Πώς να διορθώσετε την κατάσταση; Εύκολα. **Αφήστε το API να δηλώσει τις εξαρτήσεις.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Παρατηρήστε πώς οι συνδέσεις μέσα στον κώδικα γίνονται ξαφνικά προφανείς. Με το γεγονός ότι η μέθοδος `charge()` δηλώνει ότι χρειάζεται την `PaymentGateway`, δεν χρειάζεται να ρωτήσετε κανέναν πώς συνδέεται ο κώδικας. Γνωρίζετε ότι πρέπει να δημιουργήσετε την παρουσία της, και όταν προσπαθήσετε να το κάνετε, θα διαπιστώσετε ότι πρέπει να δώσετε παραμέτρους πρόσβασης. Χωρίς αυτές, ο κώδικας δεν θα μπορούσε καν να εκτελεστεί. - -Και κυρίως, τώρα μπορείτε να κάνετε mock την πύλη πληρωμών, ώστε να μην χρεώνεστε 100 δολάρια κάθε φορά που εκτελείτε το τεστ. - -Η καθολική κατάσταση κάνει τα αντικείμενά σας να μπορούν κρυφά να έχουν πρόσβαση σε πράγματα που δεν δηλώνονται στα API τους, και ως αποτέλεσμα, μετατρέπει τα API σας σε παθολογικούς ψεύτες. - -Ίσως να μην το είχατε σκεφτεί έτσι προηγουμένως, αλλά κάθε φορά που χρησιμοποιείτε καθολική κατάσταση, δημιουργείτε μυστικούς ασύρματους διαύλους επικοινωνίας. Η απόκοσμη δράση από απόσταση αναγκάζει τους προγραμματιστές να διαβάζουν κάθε γραμμή κώδικα για να κατανοήσουν τις πιθανές αλληλεπιδράσεις, μειώνει την παραγωγικότητα των προγραμματιστών και μπερδεύει τα νέα μέλη της ομάδας. Εάν είστε εσείς αυτός που δημιούργησε τον κώδικα, γνωρίζετε τις πραγματικές εξαρτήσεις, αλλά οποιοσδήποτε έρθει μετά από εσάς είναι αβοήθητος. - -Μην γράφετε κώδικα που χρησιμοποιεί καθολική κατάσταση, προτιμήστε τη μεταβίβαση εξαρτήσεων. Δηλαδή, dependency injection. - - -Ευθραυστότητα της καθολικής κατάστασης --------------------------------------- - -Στον κώδικα που χρησιμοποιεί καθολική κατάσταση και singletons, δεν είναι ποτέ σίγουρο πότε και ποιος άλλαξε αυτή την κατάσταση. Αυτός ο κίνδυνος εμφανίζεται ήδη κατά την αρχικοποίηση. Ο ακόλουθος κώδικας υποτίθεται ότι δημιουργεί μια σύνδεση βάσης δεδομένων και αρχικοποιεί την πύλη πληρωμών, ωστόσο προκαλεί συνεχώς εξαίρεση και η εύρεση της αιτίας είναι εξαιρετικά χρονοβόρα: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Πρέπει να εξετάσετε λεπτομερώς τον κώδικα για να διαπιστώσετε ότι το αντικείμενο `PaymentGateway` έχει ασύρματη πρόσβαση σε άλλα αντικείμενα, ορισμένα από τα οποία απαιτούν σύνδεση βάσης δεδομένων. Δηλαδή, είναι απαραίτητο να αρχικοποιήσετε τη βάση δεδομένων πριν από την `PaymentGateway`. Ωστόσο, το παραπέτασμα καπνού της καθολικής κατάστασης το κρύβει αυτό από εσάς. Πόσο χρόνο θα είχατε εξοικονομήσει εάν τα API των μεμονωμένων κλάσεων δεν εξαπατούσαν και δήλωναν τις εξαρτήσεις τους; - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Ένα παρόμοιο πρόβλημα εμφανίζεται και κατά τη χρήση καθολικής πρόσβασης στη σύνδεση της βάσης δεδομένων: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Κατά την κλήση της μεθόδου `save()`, δεν είναι βέβαιο εάν έχει ήδη δημιουργηθεί η σύνδεση με τη βάση δεδομένων και ποιος φέρει την ευθύνη για τη δημιουργία της. Εάν θέλουμε, για παράδειγμα, να αλλάξουμε τη σύνδεση της βάσης δεδομένων κατά την εκτέλεση, ίσως για λόγους δοκιμών, θα έπρεπε πιθανότατα να δημιουργήσουμε επιπλέον μεθόδους όπως `DB::reconnect(...)` ή `DB::reconnectForTest()`. - -Ας εξετάσουμε ένα παράδειγμα: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Πού έχουμε τη βεβαιότητα ότι κατά την κλήση του `$article->save()` χρησιμοποιείται όντως η δοκιμαστική βάση δεδομένων; Τι γίνεται αν η μέθοδος `Foo::doSomething()` άλλαξε την καθολική σύνδεση της βάσης δεδομένων; Για να το διαπιστώσουμε, θα έπρεπε να εξετάσουμε τον πηγαίο κώδικα της κλάσης `Foo` και πιθανώς και πολλών άλλων κλάσεων. Αυτή η προσέγγιση, ωστόσο, θα έδινε μόνο μια βραχυπρόθεσμη απάντηση, καθώς η κατάσταση μπορεί να αλλάξει στο μέλλον. - -Και τι γίνεται αν μεταφέρουμε τη σύνδεση με τη βάση δεδομένων σε μια στατική μεταβλητή μέσα στην κλάση `Article`; - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Αυτό δεν άλλαξε απολύτως τίποτα. Το πρόβλημα είναι η καθολική κατάσταση και είναι εντελώς αδιάφορο σε ποια κλάση κρύβεται. Σε αυτή την περίπτωση, όπως και στην προηγούμενη, δεν έχουμε καμία ένδειξη κατά την κλήση της μεθόδου `$article->save()` για το σε ποια βάση δεδομένων θα γίνει η εγγραφή. Οποιοσδήποτε στο άλλο άκρο της εφαρμογής θα μπορούσε ανά πάσα στιγμή να αλλάξει τη βάση δεδομένων χρησιμοποιώντας το `Article::setDb()`. Κάτω από τα χέρια μας. - -Η καθολική κατάσταση καθιστά την εφαρμογή μας **εξαιρετικά εύθραυστη**. - -Υπάρχει όμως ένας απλός τρόπος για να αντιμετωπίσουμε αυτό το πρόβλημα. Αρκεί να αφήσουμε το API να δηλώσει τις εξαρτήσεις, εξασφαλίζοντας έτσι τη σωστή λειτουργικότητα. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Χάρη σε αυτή την προσέγγιση, εξαλείφεται η ανησυχία για κρυφές και απροσδόκητες αλλαγές στη σύνδεση της βάσης δεδομένων. Τώρα έχουμε τη βεβαιότητα για το πού αποθηκεύεται το άρθρο και καμία τροποποίηση του κώδικα μέσα σε μια άλλη άσχετη κλάση δεν μπορεί πλέον να αλλάξει την κατάσταση. Ο κώδικας δεν είναι πλέον εύθραυστος, αλλά σταθερός. - -Μην γράφετε κώδικα που χρησιμοποιεί καθολική κατάσταση, προτιμήστε τη μεταβίβαση εξαρτήσεων. Δηλαδή, dependency injection. - - -Singleton ---------- - -Το Singleton είναι ένα σχεδιαστικό πρότυπο που, σύμφωνα με τον "ορισμό":https://en.wikipedia.org/wiki/Singleton_pattern από τη γνωστή δημοσίευση Gang of Four, περιορίζει μια κλάση σε μία μόνο παρουσία και προσφέρει καθολική πρόσβαση σε αυτήν. Η υλοποίηση αυτού του προτύπου συνήθως μοιάζει με τον ακόλουθο κώδικα: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // και άλλες μέθοδοι που εκτελούν τις λειτουργίες της συγκεκριμένης κλάσης -} -``` - -Δυστυχώς, το singleton εισάγει καθολική κατάσταση στην εφαρμογή. Και όπως δείξαμε παραπάνω, η καθολική κατάσταση είναι ανεπιθύμητη. Επομένως, το singleton θεωρείται αντι-πρότυπο (antipattern). - -Μην χρησιμοποιείτε singletons στον κώδικά σας και αντικαταστήστε τα με άλλους μηχανισμούς. Τα singletons πραγματικά δεν τα χρειάζεστε. Εάν, ωστόσο, χρειάζεται να εγγυηθείτε την ύπαρξη μιας μόνο παρουσίας της κλάσης για ολόκληρη την εφαρμογή, αφήστε το στον [DI container |container]. Δημιουργήστε έτσι ένα application singleton, δηλαδή μια υπηρεσία. Με αυτόν τον τρόπο, η κλάση παύει να ασχολείται με τη διασφάλιση της μοναδικότητάς της (δηλ. δεν θα έχει μέθοδο `getInstance()` και στατική μεταβλητή) και θα εκτελεί μόνο τις λειτουργίες της. Έτσι, παύει να παραβιάζει την αρχή της μοναδικής ευθύνης (single responsibility principle). - - -Καθολική κατάσταση έναντι δοκιμών ---------------------------------- - -Κατά τη συγγραφή δοκιμών, υποθέτουμε ότι κάθε δοκιμή είναι μια απομονωμένη μονάδα και ότι καμία εξωτερική κατάσταση δεν εισέρχεται σε αυτήν. Και καμία κατάσταση δεν εξέρχεται από τις δοκιμές. Μετά την ολοκλήρωση της δοκιμής, όλη η σχετική κατάσταση με τη δοκιμή θα πρέπει να αφαιρεθεί αυτόματα από τον garbage collector. Χάρη σε αυτό, οι δοκιμές είναι απομονωμένες. Επομένως, μπορούμε να εκτελέσουμε τις δοκιμές με οποιαδήποτε σειρά. - -Εάν, ωστόσο, υπάρχουν καθολικές καταστάσεις/singletons, όλες αυτές οι ευχάριστες υποθέσεις καταρρέουν. Η κατάσταση μπορεί να εισέλθει και να εξέλθει από τη δοκιμή. Ξαφνικά, η σειρά των δοκιμών μπορεί να έχει σημασία. - -Για να μπορέσουμε καν να δοκιμάσουμε τα singletons, οι προγραμματιστές συχνά πρέπει να χαλαρώσουν τις ιδιότητές τους, για παράδειγμα επιτρέποντας την αντικατάσταση της παρουσίας με μια άλλη. Τέτοιες λύσεις είναι στην καλύτερη περίπτωση ένα hack, που δημιουργεί κώδικα δύσκολο στη συντήρηση και την κατανόηση. Κάθε δοκιμή ή μέθοδος `tearDown()`, που επηρεάζει οποιαδήποτε καθολική κατάσταση, πρέπει να αναιρέσει αυτές τις αλλαγές. - -Η καθολική κατάσταση είναι ο μεγαλύτερος πονοκέφαλος στις δοκιμές μονάδας (unit testing)! - -Πώς να διορθώσετε την κατάσταση; Εύκολα. Μην γράφετε κώδικα που χρησιμοποιεί singletons, προτιμήστε τη μεταβίβαση εξαρτήσεων. Δηλαδή, dependency injection. - - -Καθολικές σταθερές ------------------- - -Η καθολική κατάσταση δεν περιορίζεται μόνο στη χρήση singletons και στατικών μεταβλητών, αλλά μπορεί να αφορά και τις καθολικές σταθερές. - -Οι σταθερές, η τιμή των οποίων δεν μας προσφέρει καμία νέα (`M_PI`) ή χρήσιμη (`PREG_BACKTRACK_LIMIT_ERROR`) πληροφορία, είναι σαφώς εντάξει. Αντίθετα, οι σταθερές που χρησιμεύουν ως τρόπος για να περάσουμε *ασύρματα* πληροφορίες μέσα στον κώδικα, δεν είναι τίποτα άλλο από κρυφές εξαρτήσεις. Όπως για παράδειγμα το `LOG_FILE` στο ακόλουθο παράδειγμα. Η χρήση της σταθεράς `FILE_APPEND` είναι απολύτως σωστή. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Σε αυτή την περίπτωση, θα έπρεπε να δηλώσουμε μια παράμετρο στον κατασκευαστή της κλάσης `Foo`, ώστε να γίνει μέρος του API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Τώρα μπορούμε να περάσουμε την πληροφορία για τη διαδρομή του αρχείου καταγραφής και να την αλλάξουμε εύκολα ανάλογα με τις ανάγκες, πράγμα που διευκολύνει τις δοκιμές και τη συντήρηση του κώδικα. - - -Καθολικές συναρτήσεις και στατικές μέθοδοι ------------------------------------------- - -Θέλουμε να τονίσουμε ότι η ίδια η χρήση στατικών μεθόδων και καθολικών συναρτήσεων δεν είναι προβληματική. Εξηγήσαμε σε τι συνίσταται η ακαταλληλότητα της χρήσης του `DB::insert()` και παρόμοιων μεθόδων, αλλά πάντα αφορούσε μόνο την καθολική κατάσταση, η οποία είναι αποθηκευμένη σε κάποια στατική μεταβλητή. Η μέθοδος `DB::insert()` απαιτεί την ύπαρξη στατικής μεταβλητής, επειδή σε αυτήν είναι αποθηκευμένη η σύνδεση με τη βάση δεδομένων. Χωρίς αυτή τη μεταβλητή, θα ήταν αδύνατο να υλοποιηθεί η μέθοδος. - -Η χρήση ντετερμινιστικών στατικών μεθόδων και συναρτήσεων, όπως `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` και πολλών άλλων, είναι απολύτως σύμφωνη με το dependency injection. Αυτές οι συναρτήσεις επιστρέφουν πάντα τα ίδια αποτελέσματα για τις ίδιες παραμέτρους εισόδου και είναι επομένως προβλέψιμες. Δεν χρησιμοποιούν καμία καθολική κατάσταση. - -Υπάρχουν, ωστόσο, και συναρτήσεις στην PHP που δεν είναι ντετερμινιστικές. Σε αυτές ανήκει, για παράδειγμα, η συνάρτηση `htmlspecialchars()`. Η τρίτη της παράμετρος `$encoding`, εάν δεν αναφέρεται, έχει ως προεπιλεγμένη τιμή την τιμή της επιλογής διαμόρφωσης `ini_get('default_charset')`. Επομένως, συνιστάται να αναφέρεται πάντα αυτή η παράμετρος και να αποφεύγεται έτσι η πιθανή απρόβλεπτη συμπεριφορά της συνάρτησης. Το Nette το κάνει αυτό με συνέπεια. - -Ορισμένες συναρτήσεις, όπως `strtolower()`, `strtoupper()` και παρόμοιες, στο πρόσφατο παρελθόν συμπεριφέρονταν μη ντετερμινιστικά και εξαρτώνταν από τη ρύθμιση `setlocale()`. Αυτό προκαλούσε πολλές επιπλοκές, συχνότερα κατά την εργασία με την τουρκική γλώσσα. Αυτή, δηλαδή, διακρίνει το πεζό και το κεφαλαίο γράμμα `I` με και χωρίς τελεία. Έτσι, το `strtolower('I')` επέστρεφε τον χαρακτήρα `ı` και το `strtoupper('i')` τον χαρακτήρα `İ`, πράγμα που οδηγούσε στο να αρχίσουν οι εφαρμογές να προκαλούν μια σειρά από μυστηριώδη σφάλματα. Αυτό το πρόβλημα, ωστόσο, διορθώθηκε στην έκδοση PHP 8.2 και οι συναρτήσεις δεν εξαρτώνται πλέον από το locale. - -Πρόκειται για ένα ωραίο παράδειγμα του πώς η καθολική κατάσταση ταλαιπώρησε χιλιάδες προγραμματιστές σε όλο τον κόσμο. Η λύση ήταν η αντικατάστασή της με dependency injection. - - -Πότε είναι δυνατόν να χρησιμοποιηθεί η καθολική κατάσταση? ----------------------------------------------------------- - -Υπάρχουν ορισμένες συγκεκριμένες καταστάσεις όπου είναι δυνατόν να χρησιμοποιηθεί η καθολική κατάσταση. Για παράδειγμα, κατά τον εντοπισμό σφαλμάτων στον κώδικα, όταν χρειάζεται να εκτυπώσετε την τιμή μιας μεταβλητής ή να μετρήσετε τη διάρκεια ενός συγκεκριμένου τμήματος του προγράμματος. Σε τέτοιες περιπτώσεις, που αφορούν προσωρινές ενέργειες οι οποίες θα αφαιρεθούν αργότερα από τον κώδικα, είναι δυνατόν να χρησιμοποιηθεί θεμιτά ένας καθολικά διαθέσιμος dumper ή χρονόμετρο. Αυτά τα εργαλεία, δηλαδή, δεν αποτελούν μέρος του σχεδιασμού του κώδικα. - -Ένα άλλο παράδειγμα είναι οι συναρτήσεις για την εργασία με κανονικές εκφράσεις `preg_*`, οι οποίες εσωτερικά αποθηκεύουν τις μεταγλωττισμένες κανονικές εκφράσεις σε μια στατική cache στη μνήμη. Όταν λοιπόν καλείτε την ίδια κανονική έκφραση πολλές φορές σε διαφορετικά σημεία του κώδικα, μεταγλωττίζεται μόνο μία φορά. Η cache εξοικονομεί απόδοση και ταυτόχρονα είναι για τον χρήστη εντελώς αόρατη, επομένως μια τέτοια χρήση μπορεί να θεωρηθεί θεμιτή. - - -Σύνοψη ------- - -Συζητήσαμε γιατί έχει νόημα: - -1) Να αφαιρέσετε όλες τις στατικές μεταβλητές από τον κώδικα -2) Να δηλώσετε τις εξαρτήσεις -3) Και να χρησιμοποιείτε dependency injection - -Όταν σκέφτεστε τον σχεδιασμό του κώδικα, σκεφτείτε ότι κάθε `static $foo` αποτελεί πρόβλημα. Για να είναι ο κώδικάς σας ένα περιβάλλον που σέβεται το DI, είναι απαραίτητο να εξαλείψετε εντελώς την καθολική κατάσταση και να την αντικαταστήσετε με dependency injection. - -Κατά τη διάρκεια αυτής της διαδικασίας, ίσως διαπιστώσετε ότι είναι απαραίτητο να χωρίσετε την κλάση, επειδή έχει περισσότερες από μία ευθύνες. Μην το φοβάστε. επιδιώξτε την αρχή της μοναδικής ευθύνης. - -*Θα ήθελα να ευχαριστήσω τον Miško Hevery, του οποίου τα άρθρα, όπως το [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], αποτελούν τη βάση αυτού του κεφαλαίου.* diff --git a/dependency-injection/el/introduction.texy b/dependency-injection/el/introduction.texy deleted file mode 100644 index 1711cd3f9b..0000000000 --- a/dependency-injection/el/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Τι είναι το Dependency Injection; -********************************* - -.[perex] -Αυτό το κεφάλαιο θα σας εισαγάγει στις βασικές πρακτικές προγραμματισμού που πρέπει να ακολουθείτε κατά τη συγγραφή όλων των εφαρμογών. Αυτά είναι τα θεμέλια που απαιτούνται για τη συγγραφή καθαρού, κατανοητού και συντηρήσιμου κώδικα. - -Εάν υιοθετήσετε αυτούς τους κανόνες και τους ακολουθήσετε, το Nette θα σας βοηθήσει σε κάθε βήμα. Θα χειριστεί τις εργασίες ρουτίνας για εσάς και θα σας προσφέρει μέγιστη άνεση, ώστε να μπορείτε να επικεντρωθείτε στην ίδια τη λογική. - -Οι αρχές που θα παρουσιάσουμε εδώ είναι αρκετά απλές. Δεν χρειάζεται να ανησυχείτε για τίποτα. - - -Θυμάστε το πρώτο σας πρόγραμμα; -------------------------------- - -Δεν ξέρουμε σε ποια γλώσσα το γράψατε, αλλά αν ήταν PHP, πιθανότατα θα έμοιαζε κάπως έτσι: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // εκτυπώνει 24 -``` - -Λίγες ασήμαντες γραμμές κώδικα, αλλά περιέχουν τόσες πολλές βασικές έννοιες. Ότι υπάρχουν μεταβλητές. Ότι ο κώδικας χωρίζεται σε μικρότερες μονάδες, όπως συναρτήσεις. Ότι τους περνάμε ορίσματα εισόδου και επιστρέφουν αποτελέσματα. Λείπουν μόνο οι συνθήκες και οι βρόχοι. - -Το γεγονός ότι περνάμε δεδομένα εισόδου σε μια συνάρτηση και αυτή επιστρέφει ένα αποτέλεσμα είναι μια απολύτως κατανοητή έννοια που χρησιμοποιείται και σε άλλους τομείς, όπως τα μαθηματικά. - -Μια συνάρτηση έχει την υπογραφή της, η οποία αποτελείται από το όνομά της, μια λίστα παραμέτρων και τους τύπους τους, και τέλος τον τύπο της τιμής επιστροφής. Ως χρήστες, μας ενδιαφέρει η υπογραφή· συνήθως δεν χρειάζεται να γνωρίζουμε τίποτα για την εσωτερική υλοποίηση. - -Τώρα φανταστείτε η υπογραφή της συνάρτησης να έμοιαζε κάπως έτσι: - -```php -function soucet(float $x): float -``` - -Άθροισμα με μία παράμετρο; Αυτό είναι περίεργο… Και τι θα λέγατε για αυτό; - -```php -function soucet(): float -``` - -Αυτό είναι πραγματικά πολύ περίεργο, έτσι δεν είναι; Πώς χρησιμοποιείται η συνάρτηση; - -```php -echo soucet(); // τι θα εκτυπώσει άραγε; -``` - -Κοιτάζοντας έναν τέτοιο κώδικα, θα ήμασταν μπερδεμένοι. Όχι μόνο ένας αρχάριος δεν θα τον καταλάβαινε, αλλά ούτε και ένας έμπειρος προγραμματιστής δεν καταλαβαίνει τέτοιο κώδικα. - -Αναρωτιέστε πώς θα έμοιαζε μια τέτοια συνάρτηση εσωτερικά; Από πού θα έπαιρνε τους προσθετέους; Προφανώς, θα τους έβρισκε *με κάποιο τρόπο* μόνη της, ίσως κάπως έτσι: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -Στο σώμα της συνάρτησης, ανακαλύψαμε κρυφές εξαρτήσεις από άλλες καθολικές συναρτήσεις ή στατικές μεθόδους. Για να μάθουμε από πού προέρχονται πραγματικά οι προσθετέοι, πρέπει να ψάξουμε περαιτέρω. - - -Όχι από εδώ! ------------- - -Ο σχεδιασμός που μόλις δείξαμε είναι η ουσία πολλών αρνητικών χαρακτηριστικών: - -- η υπογραφή της συνάρτησης προσποιούνταν ότι δεν χρειαζόταν προσθετέους, πράγμα που μας μπέρδεψε -- δεν ξέρουμε καθόλου πώς να κάνουμε τη συνάρτηση να προσθέσει δύο άλλους αριθμούς -- έπρεπε να κοιτάξουμε τον κώδικα για να δούμε από πού έπαιρνε τους προσθετέους -- ανακαλύψαμε κρυφές εξαρτήσεις -- για πλήρη κατανόηση, είναι απαραίτητο να εξετάσουμε και αυτές τις εξαρτήσεις - -Και είναι καθόλου έργο της συνάρτησης πρόσθεσης να αποκτά εισόδους; Φυσικά και όχι. Η ευθύνη της είναι μόνο η ίδια η πρόσθεση. - - -Δεν θέλουμε να συναντήσουμε τέτοιο κώδικα, και σίγουρα δεν θέλουμε να τον γράψουμε. Η διόρθωση είναι απλή: επιστροφή στα βασικά και απλή χρήση παραμέτρων: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Κανόνας αρ. 1: αφήστε το να σας παραδοθεί ------------------------------------------ - -Ο πιο σημαντικός κανόνας είναι: **όλα τα δεδομένα που χρειάζονται οι συναρτήσεις ή οι κλάσεις πρέπει να τους παραδίδονται**. - -Αντί να επινοείτε κρυφούς τρόπους με τους οποίους θα μπορούσαν να τα αποκτήσουν μόνοι τους, απλά περάστε τις παραμέτρους. Θα εξοικονομήσετε χρόνο που απαιτείται για την επινόηση κρυφών μονοπατιών, τα οποία σίγουρα δεν θα βελτιώσουν τον κώδικά σας. - -Αν ακολουθείτε πάντα και παντού αυτόν τον κανόνα, βρίσκεστε στο δρόμο για κώδικα χωρίς κρυφές εξαρτήσεις. Για κώδικα που είναι κατανοητός όχι μόνο στον συγγραφέα, αλλά και σε οποιονδήποτε τον διαβάσει μετά από αυτόν. Όπου όλα είναι κατανοητά από τις υπογραφές των συναρτήσεων και των κλάσεων και δεν χρειάζεται να ψάχνετε για κρυμμένα μυστικά στην υλοποίηση. - -Αυτή η τεχνική ονομάζεται τεχνικά **dependency injection**. Και αυτά τα δεδομένα ονομάζονται **εξαρτήσεις (dependencies).** Στην πραγματικότητα, είναι απλή παράδοση παραμέτρων, τίποτα περισσότερο. - -.[note] -Παρακαλώ μην συγχέετε το dependency injection, το οποίο είναι ένα πρότυπο σχεδίασης, με το "dependency injection container", το οποίο είναι ένα εργαλείο, δηλαδή κάτι διαμετρικά αντίθετο. Θα ασχοληθούμε με τα containers αργότερα. - - -Από συναρτήσεις σε κλάσεις --------------------------- - -Και πώς σχετίζονται οι κλάσεις με αυτό; Μια κλάση είναι μια πιο σύνθετη οντότητα από μια απλή συνάρτηση, ωστόσο ο κανόνας αρ. 1 ισχύει πλήρως και εδώ. Απλώς υπάρχουν [περισσότερες επιλογές για την παράδοση ορισμάτων |passing-dependencies]. Για παράδειγμα, αρκετά παρόμοια με την περίπτωση μιας συνάρτησης: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Ή χρησιμοποιώντας άλλες μεθόδους, ή απευθείας τον κατασκευαστή: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Και τα δύο παραδείγματα είναι πλήρως σύμφωνα με το dependency injection. - - -Πραγματικά παραδείγματα ------------------------ - -Στον πραγματικό κόσμο, δεν θα γράφετε κλάσεις για την πρόσθεση αριθμών. Ας προχωρήσουμε σε παραδείγματα από την πράξη. - -Έστω μια κλάση `Article` που αντιπροσωπεύει ένα άρθρο σε ένα blog: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // αποθηκεύουμε το άρθρο στη βάση δεδομένων - } -} -``` - -και η χρήση θα είναι η εξής: - -```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(); -``` - -Η μέθοδος `save()` αποθηκεύει το άρθρο σε έναν πίνακα βάσης δεδομένων. Η υλοποίησή της με τη βοήθεια του [Nette Database |database:] θα ήταν παιχνιδάκι, αν δεν υπήρχε ένα εμπόδιο: πού παίρνει η `Article` τη σύνδεση με τη βάση δεδομένων, δηλαδή το αντικείμενο της κλάσης `Nette\Database\Connection`; - -Φαίνεται ότι έχουμε πολλές επιλογές. Μπορεί να την πάρει από κάπου από μια στατική μεταβλητή. Ή να κληρονομήσει από μια κλάση που εξασφαλίζει τη σύνδεση με τη βάση δεδομένων. Ή να χρησιμοποιήσει το λεγόμενο [singleton |global-state#Singleton]. Ή τις λεγόμενες facades, που χρησιμοποιούνται στο 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], - ); - } -} -``` - -Υπέροχα, λύσαμε το πρόβλημα. - -Ή μήπως όχι; - -Ας θυμηθούμε τον [##Κανόνας αρ. 1: αφήστε το να σας παραδοθεί]: όλες οι εξαρτήσεις που χρειάζεται η κλάση πρέπει να της παραδίδονται. Επειδή αν παραβιάσουμε τον κανόνα, έχουμε πάρει τον δρόμο για βρώμικο κώδικα γεμάτο κρυφές εξαρτήσεις, ασάφεια, και το αποτέλεσμα θα είναι μια εφαρμογή που θα είναι επώδυνο να συντηρηθεί και να αναπτυχθεί. - -Ο χρήστης της κλάσης `Article` δεν έχει ιδέα πού αποθηκεύει η μέθοδος `save()` το άρθρο. Σε έναν πίνακα βάσης δεδομένων; Σε ποιον, τον παραγωγικό ή τον δοκιμαστικό; Και πώς μπορεί να αλλάξει αυτό; - -Ο χρήστης πρέπει να δει πώς υλοποιείται η μέθοδος `save()` και βρίσκει τη χρήση της μεθόδου `DB::insert()`. Άρα πρέπει να ψάξει περαιτέρω, πώς αυτή η μέθοδος αποκτά τη σύνδεση με τη βάση δεδομένων. Και οι κρυφές εξαρτήσεις μπορούν να σχηματίσουν μια αρκετά μεγάλη αλυσίδα. - -Σε καθαρό και καλά σχεδιασμένο κώδικα, δεν υπάρχουν ποτέ κρυφές εξαρτήσεις, facades του Laravel ή στατικές μεταβλητές. Σε καθαρό και καλά σχεδιασμένο κώδικα, παραδίδονται ορίσματα: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Ακόμα πιο πρακτικό, όπως θα δούμε παρακάτω, θα είναι με τον κατασκευαστή: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Αν είστε έμπειρος προγραμματιστής, ίσως σκέφτεστε ότι η `Article` δεν θα έπρεπε καθόλου να έχει τη μέθοδο `save()`, θα έπρεπε να αντιπροσωπεύει ένα καθαρά δεδομενικό component και η αποθήκευση θα έπρεπε να γίνεται από ένα ξεχωριστό repository. Αυτό έχει νόημα. Αλλά αυτό θα μας πήγαινε πολύ πέρα από το θέμα, το οποίο είναι το dependency injection, και την προσπάθεια να δώσουμε απλά παραδείγματα. - -Αν γράφετε μια κλάση που απαιτεί, για παράδειγμα, μια βάση δεδομένων για τη λειτουργία της, μην επινοείτε από πού να την πάρετε, αλλά αφήστε την να σας παραδοθεί. Ίσως ως παράμετρος του κατασκευαστή ή άλλης μεθόδου. Αναγνωρίστε τις εξαρτήσεις. Αναγνωρίστε τις στο API της κλάσης σας. Θα αποκτήσετε κατανοητό και προβλέψιμο κώδικα. - -Και τι θα λέγατε για αυτήν την κλάση, η οποία καταγράφει μηνύματα σφάλματος: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Τι πιστεύετε, τηρήσαμε τον [##Κανόνας αρ. 1: αφήστε το να σας παραδοθεί]? - -Δεν τον τηρήσαμε. - -Η κλάση *αποκτά μόνη της* την κρίσιμη πληροφορία, δηλαδή τον κατάλογο με το αρχείο καταγραφής, από μια σταθερά. - -Δείτε το παράδειγμα χρήσης: - -```php -$logger = new Logger; -$logger->log('Η θερμοκρασία είναι 23 °C'); -$logger->log('Η θερμοκρασία είναι 10 °C'); -``` - -Χωρίς γνώση της υλοποίησης, θα μπορούσατε να απαντήσετε στην ερώτηση πού γράφονται τα μηνύματα; Θα σκεφτόσασταν ότι για τη λειτουργία απαιτείται η ύπαρξη της σταθεράς `LOG_DIR`; Και θα μπορούσατε να δημιουργήσετε μια δεύτερη παρουσία που θα γράφει αλλού; Σίγουρα όχι. - -Ας διορθώσουμε την κλάση: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Η κλάση είναι τώρα πολύ πιο κατανοητή, διαμορφώσιμη και επομένως πιο χρήσιμη. - -```php -$logger = new Logger('/path/to/log.txt'); -$logger->log('Η θερμοκρασία είναι 15 °C'); -``` - - -Αλλά αυτό δεν με ενδιαφέρει! ----------------------------- - -*«Όταν δημιουργώ ένα αντικείμενο Article και καλώ την save(), δεν θέλω να ασχολούμαι με τη βάση δεδομένων, απλά θέλω να αποθηκευτεί σε αυτήν που έχω ορίσει στη διαμόρφωση.»* - -*«Όταν χρησιμοποιώ το Logger, απλά θέλω το μήνυμα να καταγραφεί, και δεν θέλω να ασχολούμαι με το πού. Ας χρησιμοποιηθεί η καθολική ρύθμιση.»* - -Αυτές είναι σωστές παρατηρήσεις. - -Ως παράδειγμα, θα δείξουμε μια κλάση που στέλνει newsletters, η οποία καταγράφει πώς πήγε: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Τα emails στάλθηκαν'); - - } catch (Exception $e) { - $logger->log('Παρουσιάστηκε σφάλμα κατά την αποστολή'); - throw $e; - } - } -} -``` - -Ο βελτιωμένος `Logger`, ο οποίος δεν χρησιμοποιεί πλέον τη σταθερά `LOG_DIR`, απαιτεί τη διαδρομή προς το αρχείο στον κατασκευαστή. Πώς να το λύσουμε αυτό; Η κλάση `NewsletterDistributor` δεν ενδιαφέρεται καθόλου για το πού γράφονται τα μηνύματα, θέλει απλώς να τα γράψει. - -Η λύση είναι και πάλι ο [##Κανόνας αρ. 1: αφήστε το να σας παραδοθεί]: παραδίδουμε όλα τα δεδομένα που χρειάζεται η κλάση. - -Άρα αυτό σημαίνει ότι παραδίδουμε τη διαδρομή προς το αρχείο καταγραφής μέσω του κατασκευαστή, την οποία στη συνέχεια χρησιμοποιούμε κατά τη δημιουργία του αντικειμένου `Logger`; - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ ΟΧΙ ΕΤΣΙ! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Όχι έτσι! Η διαδρομή **δεν ανήκει** στα δεδομένα που χρειάζεται η κλάση `NewsletterDistributor`· αυτά τα χρειάζεται ο `Logger`. Αντιλαμβάνεστε τη διαφορά; Η κλάση `NewsletterDistributor` χρειάζεται τον logger ως τέτοιο. Άρα αυτόν θα παραδώσουμε: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Τα emails στάλθηκαν'); - - } catch (Exception $e) { - $this->logger->log('Παρουσιάστηκε σφάλμα κατά την αποστολή'); - throw $e; - } - } -} -``` - -Τώρα είναι σαφές από τις υπογραφές της κλάσης `NewsletterDistributor` ότι η καταγραφή αποτελεί μέρος της λειτουργικότητάς της. Και η εργασία της αντικατάστασης του logger με έναν άλλο, για παράδειγμα για δοκιμές, είναι εντελώς ασήμαντη. Επιπλέον, αν ο κατασκευαστής της κλάσης `Logger` άλλαζε, αυτό δεν θα είχε καμία επίδραση στην κλάση μας. - - -Κανόνας αρ. 2: πάρε ό,τι είναι δικό σου ---------------------------------------- - -Μην μπερδεύεστε και μην αφήνετε να σας παραδίδουν τις εξαρτήσεις των εξαρτήσεών σας. Αφήστε να σας παραδίδουν μόνο τις δικές σας εξαρτήσεις. - -Χάρη σε αυτό, ο κώδικας που χρησιμοποιεί άλλα αντικείμενα θα είναι εντελώς ανεξάρτητος από τις αλλαγές στους κατασκευαστές τους. Το API του θα είναι πιο αληθινό. Και κυρίως, θα είναι ασήμαντο να αντικαταστήσετε αυτές τις εξαρτήσεις με άλλες. - - -Νέο μέλος της οικογένειας -------------------------- - -Στην ομάδα ανάπτυξης, αποφασίστηκε να δημιουργηθεί ένας δεύτερος logger, ο οποίος γράφει στη βάση δεδομένων. Έτσι, δημιουργούμε την κλάση `DatabaseLogger`. Έχουμε λοιπόν δύο κλάσεις, `Logger` και `DatabaseLogger`, η μία γράφει σε αρχείο, η άλλη στη βάση δεδομένων... δεν σας φαίνεται κάτι περίεργο στην ονομασία; Δεν θα ήταν καλύτερα να μετονομάσουμε τον `Logger` σε `FileLogger`; Σίγουρα ναι. - -Αλλά θα το κάνουμε έξυπνα. Κάτω από το αρχικό όνομα, θα δημιουργήσουμε ένα interface: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… το οποίο θα υλοποιούν και οι δύο loggers: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -Και χάρη σε αυτό, δεν θα χρειαστεί να αλλάξουμε τίποτα στον υπόλοιπο κώδικα όπου χρησιμοποιείται ο logger. Για παράδειγμα, ο κατασκευαστής της κλάσης `NewsletterDistributor` θα είναι ακόμα ικανοποιημένος με το ότι απαιτεί `Logger` ως παράμετρο. Και θα εξαρτάται από εμάς ποια παρουσία θα του παραδώσουμε. - -**Γι' αυτό ποτέ δεν δίνουμε στα ονόματα των interfaces την κατάληξη `Interface` ή το πρόθεμα `I`.** Διαφορετικά, δεν θα ήταν δυνατόν να αναπτύξουμε τον κώδικα τόσο όμορφα. - - -Χιούστον, έχουμε πρόβλημα -------------------------- - -Ενώ σε ολόκληρη την εφαρμογή μπορούμε να αρκεστούμε σε μία μόνο παρουσία του logger, είτε αρχείου είτε βάσης δεδομένων, και απλά να τον παραδίδουμε παντού όπου κάτι καταγράφεται, η κατάσταση είναι εντελώς διαφορετική στην περίπτωση της κλάσης `Article`. Οι παρουσίες της δημιουργούνται ανάλογα με τις ανάγκες, ακόμα και πολλές φορές. Πώς να αντιμετωπίσουμε την εξάρτηση από τη βάση δεδομένων στον κατασκευαστή της; - -Ως παράδειγμα μπορεί να χρησιμεύσει ένας controller, ο οποίος μετά την υποβολή μιας φόρμας πρέπει να αποθηκεύσει το άρθρο στη βάση δεδομένων: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Μια πιθανή λύση προσφέρεται άμεσα: αφήνουμε το αντικείμενο της βάσης δεδομένων να παραδοθεί μέσω του κατασκευαστή στον `EditController` και χρησιμοποιούμε `$article = new Article($this->db)`. - -Όπως και στην προηγούμενη περίπτωση με τον `Logger` και τη διαδρομή προς το αρχείο, αυτή δεν είναι η σωστή προσέγγιση. Η βάση δεδομένων δεν είναι εξάρτηση του `EditController`, αλλά του `Article`. Η παράδοση της βάσης δεδομένων λοιπόν αντιβαίνει στον [Κανόνα αρ. 2: πάρε ό,τι είναι δικό σου |#Κανόνας αρ. 2: πάρε ό τι είναι δικό σου]. Όταν αλλάξει ο κατασκευαστής της κλάσης `Article` (προστεθεί μια νέα παράμετρος), θα είναι απαραίτητο να τροποποιηθεί ο κώδικας σε όλα τα σημεία όπου δημιουργούνται παρουσίες. Ουφ. - -Χιούστον, τι προτείνεις; - - -Κανόνας αρ. 3: άφησέ το στο factory ------------------------------------ - -Καταργώντας τις κρυφές εξαρτήσεις και παραδίδοντας όλες τις εξαρτήσεις ως ορίσματα, αποκτήσαμε πιο διαμορφώσιμες και ευέλικτες κλάσεις. Και επομένως χρειαζόμαστε κάτι ακόμα, το οποίο θα δημιουργήσει και θα διαμορφώσει αυτές τις πιο ευέλικτες κλάσεις για εμάς. Θα το ονομάσουμε factories. - -Ο κανόνας λέει: αν μια κλάση έχει εξαρτήσεις, άφησε τη δημιουργία των παρουσιών της σε ένα factory. - -Τα factories είναι μια πιο έξυπνη αντικατάσταση του τελεστή `new` στον κόσμο του dependency injection. - -.[note] -Παρακαλώ μην συγχέετε με το πρότυπο σχεδίασης *factory method*, το οποίο περιγράφει έναν συγκεκριμένο τρόπο χρήσης των factories και δεν σχετίζεται με αυτό το θέμα. - - -Factory -------- - -Ένα factory είναι μια μέθοδος ή μια κλάση που παράγει και διαμορφώνει αντικείμενα. Την κλάση που παράγει `Article` θα την ονομάσουμε `ArticleFactory` και θα μπορούσε να μοιάζει κάπως έτσι: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Η χρήση της στον controller θα είναι η εξής: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // αφήνουμε το factory να δημιουργήσει το αντικείμενο - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Αν αυτή τη στιγμή αλλάξει η υπογραφή του κατασκευαστή της κλάσης `Article`, το μόνο μέρος του κώδικα που πρέπει να αντιδράσει σε αυτό είναι το ίδιο το factory `ArticleFactory`. Όλος ο υπόλοιπος κώδικας που λειτουργεί με αντικείμενα `Article`, όπως για παράδειγμα ο `EditController`, δεν θα επηρεαστεί καθόλου. - -Ίσως τώρα χτυπάτε το κεφάλι σας, αν βοηθήσαμε καθόλου. Η ποσότητα του κώδικα αυξήθηκε και όλο αυτό αρχίζει να φαίνεται ύποπτα περίπλοκο. - -Μην ανησυχείτε, σε λίγο θα φτάσουμε στο Nette DI container. Και αυτός έχει πολλούς άσους στο μανίκι του, οι οποίοι θα απλοποιήσουν εξαιρετικά την κατασκευή εφαρμογών που χρησιμοποιούν dependency injection. Έτσι, για παράδειγμα, αντί για την κλάση `ArticleFactory`, θα αρκεί να [γράψουμε απλώς ένα interface |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Αλλά προτρέχουμε, περιμένετε λίγο ακόμα :-) - - -Σύνοψη ------- - -Στην αρχή αυτού του κεφαλαίου, υποσχεθήκαμε ότι θα δείξουμε μια διαδικασία για τον σχεδιασμό καθαρού κώδικα. Αρκεί στις κλάσεις - -1) [να παραδίδονται οι εξαρτήσεις που χρειάζονται |#Κανόνας αρ. 1: αφήστε το να σας παραδοθεί] -2) [και αντίστροφα, να μην παραδίδονται ό,τι δεν χρειάζονται άμεσα |#Κανόνας αρ. 2: πάρε ό τι είναι δικό σου] -3) [και ότι τα αντικείμενα με εξαρτήσεις κατασκευάζονται καλύτερα σε factories |#Κανόνας αρ. 3: άφησέ το στο factory] - -Μπορεί να μην φαίνεται έτσι με την πρώτη ματιά, αλλά αυτοί οι τρεις κανόνες έχουν εκτεταμένες συνέπειες. Οδηγούν σε μια ριζικά διαφορετική άποψη για τον σχεδιασμό του κώδικα. Αξίζει τον κόπο; Οι προγραμματιστές που εγκατέλειψαν τις παλιές συνήθειες και άρχισαν να χρησιμοποιούν με συνέπεια το dependency injection θεωρούν αυτό το βήμα ως μια κρίσιμη στιγμή στην επαγγελματική τους ζωή. Τους άνοιξε τον κόσμο των σαφών και συντηρήσιμων εφαρμογών. - -Τι γίνεται όμως αν ο κώδικας δεν χρησιμοποιεί με συνέπεια το dependency injection; Τι γίνεται αν βασίζεται σε στατικές μεθόδους ή singletons; Προκαλεί αυτό προβλήματα; [Προκαλεί, και μάλιστα πολύ σοβαρά |global-state]. diff --git a/dependency-injection/el/nette-container.texy b/dependency-injection/el/nette-container.texy deleted file mode 100644 index 7489be893e..0000000000 --- a/dependency-injection/el/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Το Nette DI είναι μία από τις πιο ενδιαφέρουσες βιβλιοθήκες του Nette. Μπορεί να δημιουργεί και να ενημερώνει αυτόματα μεταγλωττισμένα DI containers, τα οποία είναι εξαιρετικά γρήγορα και εκπληκτικά εύκολα στη διαμόρφωση. - -Τη μορφή των υπηρεσιών που πρόκειται να δημιουργήσει το DI container την ορίζουμε συνήθως χρησιμοποιώντας αρχεία διαμόρφωσης σε [μορφή NEON|neon:format]. Το container που δημιουργήσαμε χειροκίνητα στο [προηγούμενο κεφάλαιο|container], θα γραφόταν ως εξής: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Η σύνταξη είναι πραγματικά συνοπτική. - -Όλες οι εξαρτήσεις που δηλώνονται στους κατασκευαστές των κλάσεων `ArticleFactory` και `UserController`, το Nette DI τις βρίσκει και τις παραδίδει αυτόματα χάρη στο λεγόμενο [autowiring|autowiring], επομένως δεν χρειάζεται να δηλωθεί τίποτα στο αρχείο διαμόρφωσης. Έτσι, ακόμα κι αν αλλάξουν οι παράμετροι, δεν χρειάζεται να αλλάξετε τίποτα στη διαμόρφωση. Το Nette container θα αναδημιουργηθεί αυτόματα. Μπορείτε να επικεντρωθείτε αποκλειστικά στην ανάπτυξη της εφαρμογής. - -Αν θέλουμε να παραδώσουμε εξαρτήσεις χρησιμοποιώντας setters, χρησιμοποιούμε την ενότητα [setup |services#Setup] για αυτό. - -Το Nette DI παράγει απευθείας τον κώδικα PHP του container. Το αποτέλεσμα είναι λοιπόν ένα αρχείο `.php`, το οποίο μπορείτε να ανοίξετε και να μελετήσετε. Χάρη σε αυτό, βλέπετε ακριβώς πώς λειτουργεί το container. Μπορείτε επίσης να το κάνετε debug στο IDE και να το εκτελέσετε βήμα-βήμα. Και κυρίως: ο παραγόμενος κώδικας PHP είναι εξαιρετικά γρήγορος. - -Το Nette DI μπορεί επίσης να παράγει κώδικα για [factories|factory] βάσει ενός παρεχόμενου interface. Επομένως, αντί για την κλάση `ArticleFactory`, θα αρκεί να δημιουργήσουμε μόνο ένα interface στην εφαρμογή: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ολόκληρο το παράδειγμα μπορείτε να το βρείτε [στο GitHub|https://github.com/nette-examples/di-example-doc]. - - -Αυτόνομη χρήση --------------- - -Η ενσωμάτωση της βιβλιοθήκης Nette DI σε μια εφαρμογή είναι πολύ εύκολη. Πρώτα την εγκαθιστούμε με το Composer (επειδή η λήψη zip είναι τόοοσο παλιομοδίτικη): - -```shell -composer require nette/di -``` - -Ο παρακάτω κώδικας δημιουργεί μια παρουσία του DI container σύμφωνα με τη διαμόρφωση που είναι αποθηκευμένη στο αρχείο `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Το container δημιουργείται μόνο μία φορά, ο κώδικας του γράφεται στην cache (κατάλογος `__DIR__ . '/temp'`) και στα επόμενα αιτήματα απλώς φορτώνεται από εκεί. - -Για τη δημιουργία και λήψη υπηρεσιών χρησιμοποιούνται οι μέθοδοι `getService()` ή `getByType()`. Έτσι δημιουργούμε το αντικείμενο `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -Κατά την ανάπτυξη, είναι χρήσιμο να ενεργοποιήσετε τη λειτουργία auto-refresh, όπου το container αναδημιουργείται αυτόματα εάν αλλάξει οποιαδήποτε κλάση ή αρχείο διαμόρφωσης. Αρκεί να δώσετε `true` ως δεύτερο όρισμα στον κατασκευαστή `ContainerLoader`. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Χρήση με το Nette Framework ---------------------------- - -Όπως δείξαμε, η χρήση του Nette DI δεν περιορίζεται σε εφαρμογές γραμμένες στο Nette Framework, μπορείτε να το ενσωματώσετε οπουδήποτε με μόλις 3 γραμμές κώδικα. Ωστόσο, εάν αναπτύσσετε εφαρμογές στο Nette Framework, τη διαμόρφωση και τη δημιουργία του container την αναλαμβάνει το [Bootstrap |application:bootstrapping#Διαμόρφωση του DI Container]. diff --git a/dependency-injection/el/passing-dependencies.texy b/dependency-injection/el/passing-dependencies.texy deleted file mode 100644 index 9d5c8b4701..0000000000 --- a/dependency-injection/el/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Παράδοση εξαρτήσεων -******************* - -<div class=perex> - -Τα ορίσματα, ή στην ορολογία του DI "εξαρτήσεις", μπορούν να παραδοθούν στις κλάσεις με τους ακόλουθους κύριους τρόπους: - -* παράδοση μέσω κατασκευαστή -* παράδοση μέσω μεθόδου (του λεγόμενου setter) -* ρύθμιση μεταβλητής -* με μέθοδο, annotation ή attribute *inject* - -</div> - -Τώρα θα δείξουμε τις διάφορες παραλλαγές με συγκεκριμένα παραδείγματα. - - -Παράδοση μέσω κατασκευαστή -========================== - -Οι εξαρτήσεις παραδίδονται τη στιγμή της δημιουργίας του αντικειμένου ως ορίσματα του κατασκευαστή: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Αυτή η μορφή είναι κατάλληλη για υποχρεωτικές εξαρτήσεις που η κλάση χρειάζεται απαραίτητα για τη λειτουργία της, καθώς χωρίς αυτές δεν θα είναι δυνατή η δημιουργία της παρουσίας. - -Από την PHP 8.0, μπορούμε να χρησιμοποιήσουμε μια συντομότερη μορφή σύνταξης ([constructor property promotion |https://blog.nette.org/el/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), η οποία είναι λειτουργικά ισοδύναμη: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Από την PHP 8.1, η μεταβλητή μπορεί να επισημανθεί με τη σημαία `readonly`, η οποία δηλώνει ότι το περιεχόμενο της μεταβλητής δεν θα αλλάξει πλέον: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -Το DI container παραδίδει αυτόματα τις εξαρτήσεις στον κατασκευαστή χρησιμοποιώντας [autowiring |autowiring]. Τα ορίσματα που δεν μπορούν να παραδοθούν με αυτόν τον τρόπο (π.χ. strings, αριθμοί, booleans) τα [γράφουμε στη διαμόρφωση |services#Ορίσματα]. - - -Constructor hell ----------------- - -Ο όρος *constructor hell* περιγράφει την κατάσταση όπου ένας απόγονος κληρονομεί από μια γονική κλάση, της οποίας ο κατασκευαστής απαιτεί εξαρτήσεις, και ταυτόχρονα ο απόγονος απαιτεί εξαρτήσεις. Ταυτόχρονα, πρέπει να αναλάβει και να παραδώσει και τις γονικές: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Το πρόβλημα προκύπτει τη στιγμή που θα θέλαμε να αλλάξουμε τον κατασκευαστή της κλάσης `BaseClass`, για παράδειγμα, όταν προστεθεί μια νέα εξάρτηση. Τότε είναι απαραίτητο να τροποποιηθούν και όλοι οι κατασκευαστές των απογόνων. Κάτι που καθιστά μια τέτοια τροποποίηση κόλαση. - -Πώς να το αποτρέψουμε αυτό; Η λύση είναι **να προτιμάμε τη [σύνθεση έναντι κληρονομικότητας |faq#Γιατί προτιμάται η σύνθεση composition έναντι της κληρονομικότητας]**. - -Δηλαδή, θα σχεδιάσουμε τον κώδικα διαφορετικά. Θα αποφεύγουμε τις [αφηρημένες |nette:introduction-to-object-oriented-programming#Αφηρημένες κλάσεις] `Base*` κλάσεις. Αντί η `MyClass` να αποκτά μια συγκεκριμένη λειτουργικότητα κληρονομώντας από την `BaseClass`, θα της παραδοθεί αυτή η λειτουργικότητα ως εξάρτηση: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Παράδοση μέσω setter -==================== - -Οι εξαρτήσεις παραδίδονται καλώντας μια μέθοδο, η οποία τις αποθηκεύει σε μια ιδιωτική μεταβλητή. Η συνήθης σύμβαση ονομασίας αυτών των μεθόδων είναι η μορφή `set*()`, γι' αυτό ονομάζονται setters, αλλά μπορούν φυσικά να ονομάζονται και οτιδήποτε άλλο. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Αυτός ο τρόπος είναι κατάλληλος για προαιρετικές εξαρτήσεις που δεν είναι απαραίτητες για τη λειτουργία της κλάσης, καθώς δεν εγγυάται ότι το αντικείμενο θα λάβει πραγματικά την εξάρτηση (δηλαδή ότι ο χρήστης θα καλέσει τη μέθοδο). - -Ταυτόχρονα, αυτός ο τρόπος επιτρέπει την επανειλημμένη κλήση του setter και την αλλαγή της εξάρτησης. Εάν αυτό δεν είναι επιθυμητό, προσθέτουμε έναν έλεγχο στη μέθοδο, ή από την PHP 8.1 επισημαίνουμε την property `$cache` με τη σημαία `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -Η κλήση του setter ορίζεται στη διαμόρφωση του DI container στο [κλειδί setup |services#Setup]. Και εδώ χρησιμοποιείται η αυτόματη παράδοση εξαρτήσεων μέσω autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Ρύθμιση μεταβλητής -================== - -Οι εξαρτήσεις παραδίδονται γράφοντας απευθείας στη μεταβλητή μέλους: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Αυτός ο τρόπος θεωρείται ακατάλληλος, επειδή η μεταβλητή μέλους πρέπει να δηλωθεί ως `public`. Και επομένως δεν έχουμε έλεγχο ότι η παραδοθείσα εξάρτηση θα είναι πράγματι του συγκεκριμένου τύπου (ίσχυε πριν την PHP 7.4) και χάνουμε τη δυνατότητα να αντιδράσουμε στη νέα εκχωρημένη εξάρτηση με δικό μας κώδικα, για παράδειγμα, να αποτρέψουμε την επακόλουθη αλλαγή. Ταυτόχρονα, η μεταβλητή γίνεται μέρος του δημόσιου interface της κλάσης, κάτι που μπορεί να μην είναι επιθυμητό. - -Η ρύθμιση της μεταβλητής ορίζεται στη διαμόρφωση του DI container στην [ενότητα setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Ενώ οι τρεις προηγούμενοι τρόποι ισχύουν γενικά σε όλες τις αντικειμενοστραφείς γλώσσες, η έγχυση με μέθοδο, annotation ή attribute *inject* είναι ειδική αποκλειστικά για τους presenters στο Nette. Αυτά συζητούνται σε [ξεχωριστό κεφάλαιο |best-practices:inject-method-attribute]. - - -Ποιον τρόπο να επιλέξω; -======================= - -- ο κατασκευαστής είναι κατάλληλος για υποχρεωτικές εξαρτήσεις που η κλάση χρειάζεται απαραίτητα για τη λειτουργία της -- ο setter είναι αντίθετα κατάλληλος για προαιρετικές εξαρτήσεις, ή εξαρτήσεις που μπορεί να χρειαστεί να αλλάξουν περαιτέρω -- οι δημόσιες μεταβλητές δεν είναι κατάλληλες diff --git a/dependency-injection/el/services.texy b/dependency-injection/el/services.texy deleted file mode 100644 index c611508ddd..0000000000 --- a/dependency-injection/el/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Ορισμός υπηρεσιών -***************** - -.[perex] -Η διαμόρφωση είναι το μέρος όπου διδάσκουμε στο DI container πώς να συναρμολογεί τις επιμέρους υπηρεσίες και πώς να τις συνδέει με άλλες εξαρτήσεις. Το Nette παρέχει έναν πολύ σαφή και κομψό τρόπο για να το πετύχουμε αυτό. - -Η ενότητα `services` στο αρχείο διαμόρφωσης μορφής NEON είναι το μέρος όπου ορίζουμε τις δικές μας υπηρεσίες και τις διαμορφώσεις τους. Ας δούμε ένα απλό παράδειγμα ορισμού μιας υπηρεσίας με όνομα `database`, η οποία αντιπροσωπεύει μια παρουσία της κλάσης `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Η παραπάνω διαμόρφωση θα οδηγήσει στην ακόλουθη μέθοδο factory στο [DI container|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Τα ονόματα των υπηρεσιών μας επιτρέπουν να αναφερόμαστε σε αυτές σε άλλα μέρη του αρχείου διαμόρφωσης, με τη μορφή `@ονομαΥπηρεσιας`. Εάν δεν χρειάζεται να ονομάσουμε την υπηρεσία, μπορούμε απλά να χρησιμοποιήσουμε μόνο μια παύλα: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Για να λάβουμε μια υπηρεσία από το DI container, μπορούμε να χρησιμοποιήσουμε τη μέθοδο `getService()` με το όνομα της υπηρεσίας ως παράμετρο, ή τη μέθοδο `getByType()` με τον τύπο της υπηρεσίας: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Δημιουργία υπηρεσίας -==================== - -Συνήθως δημιουργούμε μια υπηρεσία απλά δημιουργώντας μια παρουσία μιας συγκεκριμένης κλάσης. Για παράδειγμα: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Εάν χρειάζεται να επεκτείνουμε τη διαμόρφωση με επιπλέον κλειδιά, μπορούμε να αναπτύξουμε τον ορισμό σε πολλές γραμμές: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Το κλειδί `create` έχει ένα alias `factory`, και οι δύο παραλλαγές είναι συνηθισμένες στην πράξη. Ωστόσο, συνιστούμε τη χρήση του `create`. - -Τα ορίσματα του κατασκευαστή ή της μεθόδου δημιουργίας μπορούν εναλλακτικά να γραφτούν στο κλειδί `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Οι υπηρεσίες δεν χρειάζεται να δημιουργούνται μόνο με την απλή δημιουργία μιας παρουσίας κλάσης, μπορούν επίσης να είναι το αποτέλεσμα της κλήσης στατικών μεθόδων ή μεθόδων άλλων υπηρεσιών: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Σημειώστε ότι για λόγους απλότητας, αντί για `->` χρησιμοποιείται `::`, δείτε [#Εκφραστικά μέσα]. Θα δημιουργηθούν αυτές οι μέθοδοι factory: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -Το DI container πρέπει να γνωρίζει τον τύπο της δημιουργημένης υπηρεσίας. Εάν δημιουργούμε μια υπηρεσία χρησιμοποιώντας μια μέθοδο που δεν έχει καθορισμένο τύπο επιστροφής, πρέπει να δηλώσουμε ρητά αυτόν τον τύπο στη διαμόρφωση: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Ορίσματα -======== - -Στον κατασκευαστή και τις μεθόδους παραδίδουμε ορίσματα με τρόπο πολύ παρόμοιο με την ίδια την PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Για καλύτερη αναγνωσιμότητα, μπορούμε να αναπτύξουμε τα ορίσματα σε ξεχωριστές γραμμές. Σε αυτή την περίπτωση, η χρήση κομμάτων είναι προαιρετική: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Μπορείτε επίσης να ονομάσετε τα ορίσματα και δεν χρειάζεται να ανησυχείτε για τη σειρά τους: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Εάν θέλετε να παραλείψετε ορισμένα ορίσματα και να χρησιμοποιήσετε την προεπιλεγμένη τους τιμή ή να εισαγάγετε μια υπηρεσία χρησιμοποιώντας [autowiring|autowiring], χρησιμοποιήστε την κάτω παύλα: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Ως ορίσματα μπορούν να παραδοθούν υπηρεσίες, να χρησιμοποιηθούν παράμετροι και πολλά άλλα, δείτε [#Εκφραστικά μέσα]. - - -Setup -===== - -Στην ενότητα `setup` ορίζουμε τις μεθόδους που πρέπει να κληθούν κατά τη δημιουργία της υπηρεσίας. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Αυτό θα έμοιαζε έτσι στην PHP: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Εκτός από την κλήση μεθόδων, μπορούν επίσης να παραδοθούν τιμές σε properties. Υποστηρίζεται επίσης η προσθήκη ενός στοιχείου σε έναν πίνακα, το οποίο πρέπει να γραφτεί σε εισαγωγικά για να μην συγκρούεται με τη σύνταξη NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Αυτό θα έμοιαζε ως εξής στον κώδικα PHP: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -Στο setup, ωστόσο, μπορούν να κληθούν και στατικές μέθοδοι ή μέθοδοι άλλων υπηρεσιών. Εάν χρειάζεται να παραδώσετε την τρέχουσα υπηρεσία ως όρισμα, δηλώστε την ως `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Σημειώστε ότι για λόγους απλότητας, αντί για `->` χρησιμοποιείται `::`, δείτε [#Εκφραστικά μέσα]. Θα δημιουργηθεί μια τέτοια μέθοδος factory: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Εκφραστικά μέσα -=============== - -Το Nette DI μας δίνει εξαιρετικά πλούσια εκφραστικά μέσα, με τα οποία μπορούμε να γράψουμε σχεδόν οτιδήποτε. Στα αρχεία διαμόρφωσης μπορούμε έτσι να χρησιμοποιούμε [παραμέτρους |configuration#Παράμετροι]: - -```neon -# παράμετρος -%wwwDir% - -# τιμή παραμέτρου κάτω από κλειδί -%mailer.user% - -# παράμετρος μέσα σε string -'%wwwDir%/images' -``` - -Επίσης, να δημιουργούμε αντικείμενα, να καλούμε μεθόδους και συναρτήσεις: - -```neon -# δημιουργία αντικειμένου -DateTime() - -# κλήση στατικής μεθόδου -Collator::create(%locale%) - -# κλήση συνάρτησης PHP -::getenv(DB_USER) -``` - -Να αναφερόμαστε σε υπηρεσίες είτε με το όνομά τους είτε με τον τύπο τους: - -```neon -# υπηρεσία βάσει ονόματος -@database - -# υπηρεσία βάσει τύπου -@Nette\Database\Connection -``` - -Να χρησιμοποιούμε first-class callable syntax: .{data-version:3.2.0} - -```neon -# δημιουργία callback, αντίστοιχο του [@user, logout] -@user::logout(...) -``` - -Να χρησιμοποιούμε σταθερές: - -```neon -# σταθερά κλάσης -FilesystemIterator::SKIP_DOTS - -# καθολική σταθερά λαμβάνεται με τη συνάρτηση PHP constant() -::constant(PHP_VERSION) -``` - -Οι κλήσεις μεθόδων μπορούν να αλυσιδωθούν όπως στην PHP. Απλώς για λόγους απλότητας, αντί για `->` χρησιμοποιείται `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Αυτές τις εκφράσεις μπορείτε να τις χρησιμοποιείτε οπουδήποτε, κατά τη [δημιουργία υπηρεσιών |#Δημιουργία υπηρεσίας], στα [#ορίσματα], στην ενότητα [#setup] ή στις [παραμέτρους |configuration#Παράμετροι]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Ειδικές συναρτήσεις -------------------- - -Στα αρχεία διαμόρφωσης μπορείτε να χρησιμοποιείτε αυτές τις ειδικές συναρτήσεις: - -- `not()` άρνηση της τιμής -- `bool()`, `int()`, `float()`, `string()` μετατροπή τύπου χωρίς απώλειες στον καθορισμένο τύπο -- `typed()` δημιουργεί έναν πίνακα όλων των υπηρεσιών του καθορισμένου τύπου -- `tagged()` δημιουργεί έναν πίνακα όλων των υπηρεσιών με το δεδομένο tag - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -Σε αντίθεση με την κλασική μετατροπή τύπου στην PHP, όπως π.χ. `(int)`, η μετατροπή τύπου χωρίς απώλειες θα προκαλέσει εξαίρεση για μη αριθμητικές τιμές. - -Η συνάρτηση `typed()` δημιουργεί έναν πίνακα όλων των υπηρεσιών του δεδομένου τύπου (κλάση ή interface). Παραλείπει τις υπηρεσίες που έχουν απενεργοποιημένο το autowiring. Μπορούν να δηλωθούν και περισσότεροι τύποι διαχωρισμένοι με κόμμα. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Μπορείτε επίσης να παραδώσετε αυτόματα έναν πίνακα υπηρεσιών ενός συγκεκριμένου τύπου ως όρισμα χρησιμοποιώντας [autowiring |autowiring#Πίνακας υπηρεσιών]. - -Η συνάρτηση `tagged()` στη συνέχεια δημιουργεί έναν πίνακα όλων των υπηρεσιών με ένα συγκεκριμένο tag. Και εδώ μπορείτε να καθορίσετε περισσότερα tags διαχωρισμένα με κόμμα. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Το κλειδί `autowired` επιτρέπει την επίδραση στη συμπεριφορά του autowiring για μια συγκεκριμένη υπηρεσία. Για λεπτομέρειες, δείτε το [κεφάλαιο για το autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # η υπηρεσία foo εξαιρείται από το autowiring -``` - - -Lazy υπηρεσίες .{data-version:3.2.4} -==================================== - -Το Lazy loading είναι μια τεχνική που αναβάλλει τη δημιουργία μιας υπηρεσίας μέχρι τη στιγμή που πραγματικά χρειάζεται. Στην καθολική διαμόρφωση, μπορείτε να [ενεργοποιήσετε την τεμπέλικη δημιουργία |configuration#Lazy υπηρεσίες] για όλες τις υπηρεσίες ταυτόχρονα. Για μεμονωμένες υπηρεσίες, μπορείτε στη συνέχεια να παρακάμψετε αυτή τη συμπεριφορά: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Όταν μια υπηρεσία ορίζεται ως lazy, κατά την αίτησή της από το DI container, λαμβάνουμε ένα ειδικό αντικείμενο υποκατάστατο. Αυτό φαίνεται και συμπεριφέρεται το ίδιο με την πραγματική υπηρεσία, αλλά η πραγματική αρχικοποίηση (κλήση του κατασκευαστή και του setup) πραγματοποιείται μόνο κατά την πρώτη κλήση οποιασδήποτε μεθόδου ή property της. - -.[note] -Το Lazy loading μπορεί να χρησιμοποιηθεί μόνο για κλάσεις χρήστη, όχι για εσωτερικές κλάσεις PHP. Απαιτεί PHP 8.4 ή νεότερη έκδοση. - - -Tags -==== - -Τα tags χρησιμοποιούνται για την προσθήκη συμπληρωματικών πληροφοριών στις υπηρεσίες. Μπορείτε να προσθέσετε ένα ή περισσότερα tags σε μια υπηρεσία: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Τα tags μπορούν επίσης να φέρουν τιμές: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Για να λάβετε όλες τις υπηρεσίες με συγκεκριμένα tags, μπορείτε να χρησιμοποιήσετε τη συνάρτηση `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -Στο DI container, μπορείτε να λάβετε τα ονόματα όλων των υπηρεσιών με ένα συγκεκριμένο tag χρησιμοποιώντας τη μέθοδο `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// Το $names είναι ένας πίνακας που περιέχει το όνομα της υπηρεσίας και την τιμή του tag -// π.χ. ['foo' => 'monolog.logger.event', ...] -``` - - -Λειτουργία Inject -================= - -Με τη χρήση της σημαίας `inject: true` ενεργοποιείται η παράδοση εξαρτήσεων μέσω δημόσιων μεταβλητών με την annotation [inject |best-practices:inject-method-attribute#Attributes Inject] και μεθόδων [inject*() |best-practices:inject-method-attribute#Μέθοδοι inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Στην προεπιλεγμένη ρύθμιση, το `inject` ενεργοποιείται μόνο για τους presenters. - - -Τροποποίηση υπηρεσιών -===================== - -Το DI container περιέχει πολλές υπηρεσίες που έχουν προστεθεί μέσω ενσωματωμένης ή [επέκτασης χρήστη|extensions]. Μπορείτε να τροποποιήσετε τους ορισμούς αυτών των υπηρεσιών απευθείας στη διαμόρφωση. Για παράδειγμα, μπορείτε να αλλάξετε την κλάση της υπηρεσίας `application.application`, η οποία είναι συνήθως `Nette\Application\Application`, σε άλλη: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Η σημαία `alteration` είναι πληροφοριακή και λέει ότι απλώς τροποποιούμε μια υπάρχουσα υπηρεσία. - -Μπορούμε επίσης να συμπληρώσουμε το setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Κατά την αντικατάσταση μιας υπηρεσίας, μπορεί να θέλουμε να αφαιρέσουμε τα αρχικά ορίσματα, στοιχεία setup ή tags, για τα οποία χρησιμοποιείται το `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Εάν θέλετε να αφαιρέσετε μια υπηρεσία που προστέθηκε από επέκταση, μπορείτε να το κάνετε ως εξής: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/hu/@home.texy b/dependency-injection/hu/@home.texy deleted file mode 100644 index efbfe2814b..0000000000 --- a/dependency-injection/hu/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -A Dependency Injection egy tervezési minta, amely alapvetően megváltoztatja a kódra és a fejlesztésre vonatkozó nézeteit. Megnyitja az utat a tisztán megtervezett és fenntartható alkalmazások világába. - -- [Mi az a Dependency Injection? |introduction] -- [Globális állapot és singletonok |global-state] -- [Függőségek átadása |passing-dependencies] -- [Mi az a DI konténer? |container] -- [Gyakran Ismételt Kérdések|faq] - - -A `nette/di` csomag egy rendkívül fejlett, fordított DI konténert biztosít PHP-hoz. - -- [Nette DI Konténer |nette-container] -- [Konfiguráció |configuration] -- [Szolgáltatások definiálása |services] -- [Autowiring |autowiring] -- [Generált factory-k |factory] -- [Bővítmények készítése Nette DI-hez|extensions] diff --git a/dependency-injection/hu/@left-menu.texy b/dependency-injection/hu/@left-menu.texy deleted file mode 100644 index c82c0c7af5..0000000000 --- a/dependency-injection/hu/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Mi az a DI? |introduction] -- [Globális állapot és singletonok |global-state] -- [Függőségek átadása |passing-dependencies] -- [Mi az a DI konténer? |container] -- [Gyakran Ismételt Kérdések|faq] - - -Nette DI --------- -- [Nette DI Konténer |nette-container] -- [Konfiguráció |configuration] -- [Szolgáltatások definiálása |services] -- [Autowiring |autowiring] -- [Generált factory-k |factory] -- [Bővítmények készítése Nette DI-hez|extensions] diff --git a/dependency-injection/hu/@meta.texy b/dependency-injection/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/dependency-injection/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/dependency-injection/hu/autowiring.texy b/dependency-injection/hu/autowiring.texy deleted file mode 100644 index fff308f812..0000000000 --- a/dependency-injection/hu/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Az Autowiring egy nagyszerű funkció, amely automatikusan átadja a szükséges szolgáltatásokat a konstruktornak és más metódusoknak, így egyáltalán nem kell őket megírnunk. Rengeteg időt takarít meg Önnek. - -Ennek köszönhetően a szolgáltatásdefiníciók írásakor a legtöbb argumentumot elhagyhatjuk. Helyette: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Elég ennyit írni: - -```neon -services: - articles: Model\ArticleRepository -``` - -Az autowiring típusok alapján működik, tehát ahhoz, hogy működjön, az `ArticleRepository` osztályt valahogy így kell definiálni: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Az autowiring használatához minden típushoz **pontosan egy szolgáltatásnak** kell lennie a konténerben. Ha több lenne belőlük, az autowiring nem tudná, melyiket adja át, és kivételt dobna: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # KIVÉTELT DOB, a mainDb és a tempDb is megfelel -``` - -A megoldás az lenne, ha vagy megkerülnénk az autowiringot, és explicit módon megadnánk a szolgáltatás nevét (azaz `articles: Model\ArticleRepository(@mainDb)`). De ügyesebb az egyik szolgáltatás autowiringját [kikapcsolni |#Autowiring kikapcsolása], vagy az első szolgáltatást [előnyben részesíteni |#Autowiring preferencia]. - - -Autowiring kikapcsolása ------------------------ - -Egy szolgáltatás autowiringját kikapcsolhatjuk az `autowired: no` opcióval: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # a tempDb szolgáltatás ki van zárva az autowiringból - - articles: Model\ArticleRepository # tehát a konstruktorba a mainDb-t adja át -``` - -Az `articles` szolgáltatás nem dob kivételt, hogy két megfelelő `PDO` típusú szolgáltatás létezik (azaz `mainDb` és `tempDb`), amelyeket át lehet adni a konstruktorba, mert csak a `mainDb` szolgáltatást látja. - -.[note] -Az autowiring konfigurációja a Nette-ben másképp működik, mint a Symfony-ban, ahol az `autowire: false` opció azt mondja, hogy ne használja az autowiringot az adott szolgáltatás konstruktorának argumentumaihoz. A Nette-ben az autowiring mindig használatos, akár a konstruktor argumentumaihoz, akár bármely más metódushoz. Az `autowired: false` opció azt mondja, hogy az adott szolgáltatás példányát ne adják át sehova autowiring segítségével. - - -Autowiring preferencia ----------------------- - -Ha több azonos típusú szolgáltatásunk van, és az egyiknél megadjuk az `autowired` opciót, ez a szolgáltatás preferálttá válik: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # preferálttá válik - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Az `articles` szolgáltatás nem dob kivételt, hogy két megfelelő `PDO` típusú szolgáltatás létezik (azaz `mainDb` és `tempDb`), hanem a preferált szolgáltatást használja, tehát a `mainDb`-t. - - -Szolgáltatások tömbje ---------------------- - -Az autowiring képes átadni egy adott típusú szolgáltatások tömbjét is. Mivel PHP-ban natívan nem lehet megadni a tömb elemeinek típusát, a `array` típus mellett egy phpDoc kommentet is hozzá kell adni az elem típusával `ClassName[]` formában: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -A DI konténer ezután automatikusan átadja az adott típusnak megfelelő szolgáltatások tömbjét. Kihagyja azokat a szolgáltatásokat, amelyeknek ki van kapcsolva az autowiringja. - -A kommentben szereplő típus lehet `array<int, Class>` vagy `list<Class>` formájú is. Ha nem tudja befolyásolni a phpDoc komment formáját, átadhatja a szolgáltatások tömbjét közvetlenül a konfigurációban a [`typed()` |services#Speciális függvények] segítségével. - - -Skalár argumentumok -------------------- - -Az autowiring csak objektumokat és objektumok tömbjeit tudja beilleszteni. A skalár argumentumokat (pl. stringek, számok, logikai értékek) [a konfigurációban írjuk le |services#Argumentumok]. Alternatíva egy [settings-objektum |best-practices:passing-settings-to-presenters] létrehozása, amely a skalár értéket (vagy több értéket) objektum formájába csomagolja, és ezt aztán újra át lehet adni autowiring segítségével. - -```php -class MySettings -{ - public function __construct( - // a readonly PHP 8.1-től használható - public readonly bool $value, - ) - {} -} -``` - -Szolgáltatást hozhat létre belőle a konfigurációhoz való hozzáadással: - -```neon -services: - - MySettings('any value') -``` - -Ezután minden osztály autowiring segítségével kérheti azt. - - -Autowiring szűkítése --------------------- - -Az egyes szolgáltatások autowiringját le lehet szűkíteni csak bizonyos osztályokra vagy interfészekre. - -Normális esetben az autowiring átadja a szolgáltatást minden olyan metódusparaméternek, amelynek típusa megfelel a szolgáltatásnak. A szűkítés azt jelenti, hogy feltételeket szabunk, amelyeknek a metódusparamétereknél megadott típusoknak meg kell felelniük ahhoz, hogy a szolgáltatást átadják nekik. - -Nézzünk egy példát: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Ha mindegyiket szolgáltatásként regisztrálnánk, az autowiring meghiúsulna: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # KIVÉTELT DOB, a parent és a child szolgáltatás is megfelel - childDep: ChildDependent # az autowiring a child szolgáltatást adja át a konstruktorba -``` - -A `parentDep` szolgáltatás `Multiple services of type ParentClass found: parent, child` kivételt dob, mert a konstruktorába mind a `parent`, mind a `child` szolgáltatás illeszkedik, és az autowiring nem tudja eldönteni, melyiket válassza. - -Ezért a `child` szolgáltatásnál leszűkíthetjük az autowiringját a `ChildClass` típusra: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # 'autowired: self'-et is lehet írni - - parentDep: ParentDependent # az autowiring a parent szolgáltatást adja át a konstruktorba - childDep: ChildDependent # az autowiring a child szolgáltatást adja át a konstruktorba -``` - -Most a `parentDep` szolgáltatás konstruktorába a `parent` szolgáltatás kerül átadásra, mert most ez az egyetlen megfelelő objektum. A `child` szolgáltatást az autowiring már nem adja át oda. Igen, a `child` szolgáltatás továbbra is `ParentClass` típusú, de már nem teljesül a paraméter típusára vonatkozó szűkítő feltétel, azaz nem igaz, hogy a `ParentClass` *felülírja* a `ChildClass`-t. - -A `child` szolgáltatásnál az `autowired: ChildClass`-t `autowired: self`-ként is lehetne írni, mivel a `self` az aktuális szolgáltatás osztályának helyettesítő jelölése. - -Az `autowired` kulcsban több osztályt vagy interfészt is meg lehet adni tömbként: - -```neon -autowired: [BarClass, FooInterface] -``` - -Próbáljuk meg a példát kiegészíteni egy interfésszel: - -```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) - {} -} -``` - -Ha a `child` szolgáltatást semmilyen módon nem korlátozzuk, akkor illeszkedni fog az összes `FooDependent`, `BarDependent`, `ParentDependent` és `ChildDependent` osztály konstruktorába, és az autowiring oda fogja átadni. - -Ha azonban az autowiringját leszűkítjük a `ChildClass`-ra az `autowired: ChildClass` (vagy `self`) segítségével, az autowiring csak a `ChildDependent` konstruktorába adja át, mert az `ChildClass` típusú argumentumot igényel, és igaz, hogy a `ChildClass` *típusa* `ChildClass`. A többi paraméternél megadott további típusok egyike sem felülírja a `ChildClass`-t, így a szolgáltatás nem kerül átadásra. - -Ha a `ParentClass`-ra korlátozzuk az `autowired: ParentClass` segítségével, az autowiring ismét átadja a `ChildDependent` konstruktorába (mert a szükséges `ChildClass` felülírja a `ParentClass`-t), és újonnan a `ParentDependent` konstruktorába is, mert a szükséges `ParentClass` típus szintén megfelelő. - -Ha a `FooInterface`-re korlátozzuk, akkor továbbra is autowire-olva lesz a `ParentDependent`-be (a szükséges `ParentClass` felülírja a `FooInterface`-t) és a `ChildDependent`-be, de ráadásul a `FooDependent` konstruktorába is, viszont nem a `BarDependent`-be, mert a `BarInterface` nem felülírja a `FooInterface`-t. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # az autowiring a child-ot adja át a konstruktorba - barDep: BarDependent # KIVÉTELT DOB, egyetlen szolgáltatás sem felel meg - parentDep: ParentDependent # az autowiring a child-ot adja át a konstruktorba - childDep: ChildDependent # az autowiring a child-ot adja át a konstruktorba -``` diff --git a/dependency-injection/hu/configuration.texy b/dependency-injection/hu/configuration.texy deleted file mode 100644 index a4f889fa85..0000000000 --- a/dependency-injection/hu/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -DI konténer konfigurációja -************************** - -.[perex] -A Nette DI konténer konfigurációs opcióinak áttekintése. - - -Konfigurációs fájl -================== - -A Nette DI konténer könnyen vezérelhető konfigurációs fájlok segítségével. Ezek általában [NEON formátumban|neon:format] íródnak. A szerkesztéshez [támogatással rendelkező szerkesztőket |best-practices:editors-and-tools#IDE szerkesztő] ajánlunk ehhez a formátumhoz. - -<pre> -"decorator .[prism-token prism-atrule]":[#Decorator]: "Dekorátor .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI konténer .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Kiterjesztések]: "További DI kiterjesztések telepítése .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Fájlok beillesztése]: "Fájlok beillesztése .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Paraméterek]: "Paraméterek .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Szolgáltatások automatikus regisztrálása .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Szolgáltatások .[prism-token prism-comment]" -</pre> - -.[note] -Ha `%` karaktert tartalmazó stringet szeretne írni, duplázással kell escapelni `%%`-ra. - - -Paraméterek -=========== - -A konfigurációban definiálhat paramétereket, amelyeket aztán a szolgáltatásdefiníciók részeként használhat. Ezzel áttekinthetőbbé teheti a konfigurációt, vagy egységesítheti és kiemelheti azokat az értékeket, amelyek változni fognak. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -A `dsn` paraméterre bárhol a konfigurációban `%dsn%` írással hivatkozhatunk. A paramétereket stringeken belül is használhatjuk, mint például `'%wwwDir%/images'`. - -A paraméterek nem csak stringek vagy számok lehetnek, tartalmazhatnak tömböket is: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Egy konkrét kulcsra `%mailer.user%`-ként hivatkozhatunk. - -Ha a kódjában, például egy osztályban, meg kell tudnia bármely paraméter értékét, adja át azt ennek az osztálynak. Például a konstruktorban. Nincs globális objektum, amely a konfigurációt képviselné, és amelytől az osztályok lekérdeznék a paraméterértékeket. Ez megsértené a dependency injection elvét. - - -Szolgáltatások -============== - -Lásd a [külön fejezetben|services]. - - -Decorator -========= - -Hogyan lehet tömegesen módosítani egy adott típusú összes szolgáltatást? Például meghívni egy bizonyos metódust minden olyan presenter esetén, amely egy konkrét közös őstől öröklődik? Erre való a decorator. - -```neon -decorator: - # minden olyan szolgáltatásnál, amely ennek az osztálynak vagy interfésznek a példánya - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # hívd meg ezt a metódust - - $absoluteUrls = true # és állítsd be a változót -``` - -A decorator használható [tagekkel |services#Tagek] beállítására vagy az [inject |services#Inject mód] mód bekapcsolására is. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -A DI konténer technikai beállításai. - -```neon -di: - # megjeleníteni a DIC-t a Tracy Bar-ban? - debugger: ... # (bool) alapértelmezett true - - # soha nem autowire-olandó paramétertípusok - excluded: ... # (string[]) - - # engedélyezni a szolgáltatások lazy létrehozását? - lazy: ... # (bool) alapértelmezett false - - # osztály, amelytől a DI konténer öröklődik - parentClass: ... # (string) alapértelmezett Nette\DI\Container -``` - - -Lazy szolgáltatások .{data-version:3.2.4} ------------------------------------------ - -A `lazy: true` beállítás aktiválja a szolgáltatások lazy (késleltetett) létrehozását. Ez azt jelenti, hogy a szolgáltatások nem jönnek létre ténylegesen abban a pillanatban, amikor lekérjük őket a DI konténerből, hanem csak az első használatuk pillanatában. Ez gyorsíthatja az alkalmazás indítását és csökkentheti a memóriaterhelést, mivel csak azok a szolgáltatások jönnek létre, amelyekre az adott kérésben valóban szükség van. - -Egy konkrét szolgáltatásnál a lazy létrehozást [módosítani |services#Lazy szolgáltatások] lehet. - -.[note] -A lazy objektumok csak felhasználói osztályokhoz használhatók, nem belső PHP osztályokhoz. PHP 8.4 vagy újabb verziót igényel. - - -Metaadatok exportálása ----------------------- - -A DI konténer osztálya sok metaadatot is tartalmaz. Csökkentheti a méretét azáltal, hogy redukálja a metaadatok exportálását. - -```neon -di: - export: - # exportálni a paramétereket? - parameters: false # (bool) alapértelmezett true - - # exportálni a tageket és melyeket? - tags: # (string[]|bool) alapértelmezés szerint mindet - - event.subscriber - - # exportálni az autowiring adatokat és melyeket? - types: # (string[]|bool) alapértelmezés szerint mindet - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Ha nem használja a `$container->getParameters()` tömböt, kikapcsolhatja a paraméterek exportálását. Továbbá exportálhatja csak azokat a tageket, amelyeken keresztül szolgáltatásokat szerez a `$container->findByTag(...)` metódussal. Ha egyáltalán nem hívja meg a metódust, teljesen kikapcsolhatja a tagek exportálását `false`-szal. - -Jelentősen redukálhatja az [autowiring |autowiring] metaadatait azáltal, hogy megadja azokat az osztályokat, amelyeket a `$container->getByType()` metódus paramétereként használ. És ismét, ha egyáltalán nem hívja meg a metódust (illetve csak a [bootstrapban|application:bootstrapping] a `Nette\Application\Application` megszerzéséhez), teljesen kikapcsolhatja az exportálást `false`-szal. - - -Kiterjesztések -============== - -További DI kiterjesztések regisztrálása. Ezzel a módszerrel hozzáadjuk például a `Dibi\Bridges\Nette\DibiExtension22` DI kiterjesztést `dibi` néven. - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Ezután a `dibi` szekcióban konfiguráljuk: - -```neon -dibi: - host: localhost -``` - -Kiterjesztésként hozzá lehet adni egy osztályt is, amelynek paraméterei vannak: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Fájlok beillesztése -=================== - -További konfigurációs fájlokat illeszthetünk be az `includes` szekcióban: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -A `parameters.php` név nem elírás, a konfiguráció PHP fájlban is leírható, amely tömbként adja vissza: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Ha a konfigurációs fájlokban azonos kulcsokkal rendelkező elemek jelennek meg, felülíródnak, vagy [tömbök esetén egyesítve |#Összefésülés] lesznek. A később beillesztett fájl magasabb prioritású, mint az előző. Az a fájl, amelyben az `includes` szekció szerepel, magasabb prioritású, mint a benne beillesztett fájlok. - - -Search -====== - -A szolgáltatások automatikus hozzáadása a DI konténerhez rendkívül megkönnyíti a munkát. A Nette automatikusan hozzáadja a presentereket a konténerhez, de könnyen hozzáadhat bármilyen más osztályt is. - -Csak meg kell adni, mely könyvtárakban (és alkönyvtárakban) keresse az osztályokat: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Általában azonban nem akarjuk hozzáadni az összes osztályt és interfészt, ezért szűrhetjük őket: - -```neon -search: - - in: %appDir%/Forms - - # szűrés fájlnév alapján (string|string[]) - files: - - *Factory.php - - # szűrés osztálynév alapján (string|string[]) - classes: - - *Factory -``` - -Vagy kiválaszthatunk olyan osztályokat, amelyek legalább egyet örökölnek vagy implementálnak a megadott osztályok közül: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Definiálhatunk kizáró szabályokat is, azaz osztálynév maszkokat vagy örökölt ősöket, amelyek ha megfelelnek, a szolgáltatás nem kerül hozzáadásra a DI konténerhez: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Minden szolgáltatáshoz be lehet állítani tageket: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Összefésülés -============ - -Ha több konfigurációs fájlban azonos kulcsokkal rendelkező elemek jelennek meg, felülíródnak, vagy tömbök esetén összefésülődnek. A később beillesztett fájl magasabb prioritású, mint az előző. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>eredmény</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Tömbök esetén megakadályozható az összefésülés egy felkiáltójel hozzáadásával a kulcs neve után: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>eredmény</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Dependency Injection Konfiguráció}} diff --git a/dependency-injection/hu/container.texy b/dependency-injection/hu/container.texy deleted file mode 100644 index d20eb30da4..0000000000 --- a/dependency-injection/hu/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Mi az a DI konténer? -******************** - -.[perex] -A Dependency injection konténer (DIC) egy olyan osztály, amely képes objektumokat példányosítani és konfigurálni. - -Talán meglepő, de sok esetben nincs szüksége dependency injection konténerre ahhoz, hogy kihasználja a dependency injection (röviden DI) előnyeit. Hiszen már a [bevezető fejezetben|introduction] is konkrét példákon keresztül mutattuk be a DI-t, és nem volt szükség semmilyen konténerre. - -Ha azonban nagyszámú, sok függőséggel rendelkező különböző objektumot kell kezelnie, a dependency injection konténer valóban hasznos lesz. Ez például a keretrendszerre épülő webalkalmazások esetében igaz. - -Az előző fejezetben bemutattuk az `Article` és `UserController` osztályokat. Mindkettőnek vannak bizonyos függőségei, nevezetesen az adatbázis és az `ArticleFactory` factory. És ezekhez az osztályokhoz most létrehozunk egy konténert. Természetesen egy ilyen egyszerű példához nincs értelme konténert használni. De létrehozzuk, hogy megmutassuk, hogyan néz ki és működik. - -Itt van egy egyszerű hardcoded konténer a megadott példához: - -```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()); - } -} -``` - -A használat így nézne ki: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Csak megkérdezzük a konténert az objektumról, és már nem kell tudnunk semmit arról, hogyan kell létrehozni, és milyen függőségei vannak; mindezt a konténer tudja. A függőségeket a konténer automatikusan injektálja. Ebben rejlik az ereje. - -A konténernek eddig minden adata fixen be van írva. Tegyünk tehát egy újabb lépést, és adjunk hozzá paramétereket, hogy a konténer valóban hasznos legyen: - -```php -class Container -{ - public function __construct( - private array $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' => '***', -]); -``` - -Az éles szemű olvasók talán észrevettek egy problémát. Minden alkalommal, amikor lekérünk egy `UserController` objektumot, új `ArticleFactory` példány és adatbázis is létrejön. Ezt biztosan nem akarjuk. - -Ezért hozzáadunk egy `getService()` metódust, amely mindig ugyanazokat a példányokat adja vissza: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // a getService('Database') a createDatabase()-t fogja hívni - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Az első híváskor, pl. `$container->getService('Database')`, a `createDatabase()` metódussal létrehozza az adatbázis objektumot, amelyet a `$services` tömbbe ment, és a következő híváskor egyenesen visszaadja. - -Módosítjuk a konténer többi részét is, hogy a `getService()`-t használja: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Mellesleg, a szolgáltatás kifejezés bármely, a konténer által kezelt objektumot jelöl. Ezért is a metódus neve `getService()`. - -Kész. Van egy teljesen működőképes DI konténerünk! És használhatjuk: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Ahogy láthatja, egy DIC megírása nem bonyolult dolog. Érdemes megjegyezni, hogy maguk az objektumok nem tudják, hogy valamilyen konténer hozza őket létre. Így bármilyen PHP objektumot létre lehet hozni anélkül, hogy a forráskódjába bele kellene nyúlni. - -A konténer osztály manuális létrehozása és karbantartása meglehetősen gyorsan rémálommá válhat. Ezért a következő fejezetben a [Nette DI Container-ről|nette-container] beszélünk, amely szinte önmagát tudja generálni és frissíteni. - - -{{maintitle: Mi az a dependency injection konténer?}} diff --git a/dependency-injection/hu/extensions.texy b/dependency-injection/hu/extensions.texy deleted file mode 100644 index b29a120db5..0000000000 --- a/dependency-injection/hu/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Kiterjesztések készítése a Nette DI-hez -*************************************** - -.[perex] -A DI konténer generálását a konfigurációs fájlokon kívül az úgynevezett *kiterjesztések* is befolyásolják. Ezeket a konfigurációs fájl `extensions` szekciójában aktiváljuk. - -Így adjuk hozzá a `BlogExtension` osztály által reprezentált kiterjesztést `blog` néven: - -```neon -extensions: - blog: BlogExtension -``` - -Minden compiler kiterjesztés a [api:Nette\DI\CompilerExtension]-ből öröklődik, és implementálhatja a következő metódusokat, amelyeket a DI konténer összeállítása során sorban hívnak meg: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Ez a metódus hívódik meg először. Definiálja a sémát a konfigurációs paraméterek validálásához. - -A kiterjesztést abban a szekcióban konfiguráljuk, amelynek neve megegyezik azzal, amely alatt a kiterjesztést hozzáadták, tehát `blog`: - -```neon -# ugyanaz a név, mint a kiterjesztésé -blog: - postsPerPage: 10 - allowComments: false -``` - -Létrehozunk egy sémát, amely leírja az összes konfigurációs opciót, beleértve azok típusait, megengedett értékeit és esetleg alapértelmezett értékeit is: - -```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), - ]); - } -} -``` - -A dokumentációt a [Schema |schema:] oldalon találja. Ezenkívül meg lehet határozni, mely opciók lehetnek [dinamikusak |application:bootstrapping#Dinamikus paraméterek] a `dynamic()` segítségével, pl. `Expect::int()->dynamic()`. - -A konfigurációhoz a `$this->config` változón keresztül férünk hozzá, amely egy `stdClass` objektum: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Szolgáltatások hozzáadására szolgál a konténerhez. Erre a [api:Nette\DI\ContainerBuilder] szolgál: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - $builder->addDefinition($this->prefix('articles')) - ->setFactory(App\Model\HomepageArticles::class, ['@connection']) // vagy setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -A konvenció az, hogy a kiterjesztés által hozzáadott szolgáltatásokat annak nevével prefixeljük, hogy ne keletkezzenek névütközések. Ezt a `prefix()` metódus teszi, tehát ha a kiterjesztés neve `blog`, a szolgáltatás neve `blog.articles` lesz. - -Ha át kell neveznünk egy szolgáltatást, a visszamenőleges kompatibilitás megőrzése érdekében létrehozhatunk egy aliast az eredeti névvel. Hasonlóan teszi ezt a Nette például a `routing.router` szolgáltatásnál, amely a korábbi `router` néven is elérhető. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Szolgáltatások betöltése fájlból --------------------------------- - -A szolgáltatásokat nem csak a ContainerBuilder osztály API-ján keresztül hozhatjuk létre, hanem a konfigurációs fájlban a services szekcióban használt ismert írásmóddal is. Az `@extension` prefix az aktuális kiterjesztést jelenti. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -A szolgáltatásokat betöltjük: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // a kiterjesztés konfigurációs fájljának betöltése - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -A metódus akkor hívódik meg, amikor a konténer tartalmazza az összes, az egyes kiterjesztések által a `loadConfiguration` metódusokban hozzáadott szolgáltatást, valamint a felhasználói konfigurációs fájlokból származókat is. Az összeállítás ezen szakaszában tehát módosíthatjuk a szolgáltatásdefiníciókat, vagy kiegészíthetjük a köztük lévő kapcsolatokat. A szolgáltatások konténerben való kereséséhez tagek alapján a `findByTag()` metódust, osztály vagy interfész alapján pedig a `findByType()` metódust használhatjuk. - -```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] -======================== - -Ebben a fázisban a konténer osztálya már [ClassType |php-generator:#Osztályok] objektum formájában van generálva, tartalmazza az összes metódust, amely szolgáltatásokat hoz létre, és készen áll a cache-be írásra. Az eredményül kapott osztálykódot ebben a pillanatban még módosíthatjuk. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -A Configurator osztály a [konténer létrehozása |application:bootstrapping#index.php] után meghívja az inicializációs kódot, amely a `$this->initialization` objektumba való írással jön létre a [addBody() metódusával |php-generator:#Metódus és függvény törzsek] segítségével. - -Mutatunk egy példát, hogyan indíthatjuk el például a sessiont inicializációs kóddal, vagy futtathatunk olyan szolgáltatásokat, amelyeknek `run` tagjük van: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // session automatikus indítása - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // a run taggel rendelkező szolgáltatásokat a konténer példányosítása után kell létrehozni - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/hu/factory.texy b/dependency-injection/hu/factory.texy deleted file mode 100644 index 07f077420e..0000000000 --- a/dependency-injection/hu/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Generált factory-k -****************** - -.[perex] -A Nette DI képes automatikusan generálni factory kódot interfészek alapján, ami megkíméli Önt a kódírástól. - -A factory egy olyan osztály, amely objektumokat gyárt és konfigurál. Tehát átadja nekik a függőségeiket is. Kérjük, ne keverje össze a *factory method* tervezési mintával, amely a factory-k specifikus felhasználási módját írja le, és nem kapcsolódik ehhez a témához. - -Hogy néz ki egy ilyen factory, azt a [bevezető fejezetben |introduction#Factory] mutattuk be: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -A Nette DI képes automatikusan generálni a factory kódot. Mindössze annyit kell tennie, hogy létrehoz egy interfészt, és a Nette DI legenerálja az implementációt. Az interfésznek pontosan egy `create` nevű metódussal kell rendelkeznie, és deklarálnia kell a visszatérési típust: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Tehát az `ArticleFactory` factorynak van egy `create` metódusa, amely `Article` objektumokat hoz létre. Az `Article` osztály például így nézhet ki: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -A factoryt hozzáadjuk a konfigurációs fájlhoz: - -```neon -services: - - ArticleFactory -``` - -A Nette DI legenerálja a factory megfelelő implementációját. - -A kódban, amely a factoryt használja, így kérünk egy objektumot az interfész alapján, és a Nette DI a generált implementációt használja: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // hagyjuk, hogy a factory létrehozza az objektumot - $article = $this->articleFactory->create(); - } -} -``` - - -Paraméterezett factory -====================== - -A `create` factory metódus elfogadhat paramétereket, amelyeket aztán átad a konstruktornak. Egészítsük ki például az `Article` osztályt a cikk szerzőjének ID-jával: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -A paramétert hozzáadjuk a factoryhoz is: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Annak köszönhetően, hogy a konstruktorban és a factoryban lévő paraméter neve ugyanaz, a Nette DI teljesen automatikusan átadja őket. - - -Haladó definíció -================ - -A definíciót többsoros formában is le lehet írni az `implement` kulcs használatával: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Ezzel a hosszabb írásmóddal további argumentumokat lehet megadni a konstruktorhoz az `arguments` kulcsban, és kiegészítő konfigurációt a `setup` segítségével, ugyanúgy, mint a [normál szolgáltatásoknál|services]. - -Példa: ha a `create()` metódus nem fogadná el a `$authorId` paramétert, megadhatnánk egy fix értéket a konfigurációban, amelyet átadnánk az `Article` konstruktorának: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Vagy fordítva, ha a `create()` elfogadná a `$authorId` paramétert, de az nem lenne része a konstruktornak, és a `Article::setAuthorId()` metódussal adnánk át, akkor a `setup` szekcióban hivatkoznánk rá: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -A Nette a factory-k mellett ún. accessorokat is tud generálni. Ezek olyan objektumok, amelyeknek van egy `get()` metódusa, amely egy bizonyos szolgáltatást ad vissza a DI konténerből. A `get()` ismételt hívása mindig ugyanazt a példányt adja vissza. - -Az accessorok lazy-loadingot biztosítanak a függőségeknek. Tegyük fel, hogy van egy osztályunk, amely hibákat ír egy speciális adatbázisba. Ha ez az osztály konstruktorfüggőségként kapná meg az adatbázis-kapcsolatot, a kapcsolatot mindig létre kellene hozni, bár a gyakorlatban hiba csak kivételesen fordul elő, és így a kapcsolat legtöbbször kihasználatlan maradna. Ehelyett az osztály átad egy accessort, és csak akkor jön létre az adatbázis objektum, amikor annak `get()` metódusát meghívják: - -Hogyan hozzunk létre accessort? Csak írjunk egy interfészt, és a Nette DI legenerálja az implementációt. Az interfésznek pontosan egy `get` nevű metódussal kell rendelkeznie, és deklarálnia kell a visszatérési típust: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Az accessort hozzáadjuk a konfigurációs fájlhoz, ahol a szolgáltatás definíciója is található, amelyet vissza fog adni: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Mivel az accessor `PDO` típusú szolgáltatást ad vissza, és a konfigurációban csak egy ilyen szolgáltatás van, pontosan azt fogja visszaadni. Ha több ilyen típusú szolgáltatás lenne, a visszaadott szolgáltatást név szerint határoznánk meg, pl. `- PDOAccessor(@db1)`. - - -Többszörös factory/accessor -=========================== -Eddig a factory-ink és accessoraink mindig csak egy objektumot tudtak gyártani vagy visszaadni. De nagyon könnyen létrehozhatunk többszörös factory-kat is accessorokkal kombinálva. Egy ilyen osztály interfésze tetszőleges számú `create<name>()` és `get<name>()` nevű metódust tartalmazhat, pl.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Tehát ahelyett, hogy több generált factoryt és accessort adnánk át, egy komplexebb factoryt adunk át, amely többet tud. - -Alternatívaként több metódus helyett használhatjuk a `get()`-et paraméterrel: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Ekkor igaz, hogy a `MultiFactory::getArticle()` ugyanazt csinálja, mint a `MultiFactoryAlt::get('article')`. Az alternatív írásmódnak azonban az a hátránya, hogy nem egyértelmű, milyen `$name` értékek támogatottak, és logikailag nem lehet megkülönböztetni a különböző visszatérési értékeket a különböző `$name`-ekhez az interfészben. - - -Definíció listával ------------------- -Ezzel a módszerrel definiálhatunk többszörös factoryt a konfigurációban: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # definiálja a createArticle()-t - db: PDO(%dsn%, %user%, %password%) # definiálja a getDb()-t - ) -``` - -Vagy a factory definíciójában hivatkozhatunk létező szolgáltatásokra referenciával: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # definiálja a createArticle()-t - db: @\PDO # definiálja a getDb()-t - ) -``` - - -Definíció tagekkel ------------------- - -A második lehetőség a [tageket |services#Tagek] használni a definícióhoz: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/hu/faq.texy b/dependency-injection/hu/faq.texy deleted file mode 100644 index 3ac34906d6..0000000000 --- a/dependency-injection/hu/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Gyakran Ismételt Kérdések a DI-ről (GYIK) -***************************************** - - -A DI egy másik név az IoC-re? ------------------------------ - -Az *Inversion of Control* (IoC) egy elv, amely arra összpontosít, hogyan fut a kód - hogy a kódja futtat-e egy idegen kódot, vagy a kódja integrálva van egy idegen kódba, amely aztán meghívja. Az IoC egy tág fogalom, amely magában foglalja az [eseményeket |nette:glossary#Eventek események], az úgynevezett [Hollywood-elvet |application:components#Hollywood style] és más szempontokat is. Ennek a koncepciónak a része a factory is, amelyről a [3. szabály: hagyd a factory-ra |introduction#3. szabály: Hagyd a factory-ra] szól, és amely az `new` operátor inverzióját jelenti. - -A *Dependency Injection* (DI) arra összpontosít, hogyan tud meg egy objektum egy másik objektumról, azaz annak függőségeiről. Ez egy tervezési minta, amely megköveteli a függőségek explicit átadását az objektumok között. - -Tehát mondhatjuk, hogy a DI az IoC egy specifikus formája. Azonban nem minden IoC forma megfelelő a kód tisztasága szempontjából. Például az antipattern-ek közé tartoznak azok a technikák, amelyek [globális állapottal |global-state] dolgoznak, vagy az úgynevezett [Service Locator |#Mi az a Service Locator]. - - -Mi az a Service Locator? ------------------------- - -Ez egy alternatíva a Dependency Injection-re. Úgy működik, hogy létrehoz egy központi tárolót, ahol minden elérhető szolgáltatás vagy függőség regisztrálva van. Amikor egy objektumnak szüksége van egy függőségre, a Service Locatortól kéri azt. - -A Dependency Injection-nel szemben azonban elveszíti az átláthatóságot: a függőségek nem közvetlenül kerülnek átadásra az objektumoknak, és így nem könnyen azonosíthatók, ami megköveteli a kód átvizsgálását, hogy minden kapcsolatot feltárjunk és megértsünk. A tesztelés is bonyolultabb, mert nem tudunk egyszerűen mock objektumokat átadni a tesztelt objektumoknak, hanem a Service Locatoron keresztül kell ezt megtennünk. Ráadásul a Service Locator megzavarja a kód tervezését, mivel az egyes objektumoknak tudniuk kell a létezéséről, ami eltér a Dependency Injection-től, ahol az objektumoknak nincs tudomásuk a DI konténerről. - - -Mikor jobb nem használni a DI-t? --------------------------------- - -Nincsenek ismert nehézségek a Dependency Injection tervezési minta használatával kapcsolatban. Ellenkezőleg, a függőségek globálisan elérhető helyekről való beszerzése [számos komplikációhoz |global-state] vezet, ahogy a Service Locator használata is. Ezért célszerű mindig DI-t használni. Ez nem dogmatikus megközelítés, egyszerűen nem találtak jobb alternatívát. - -Ennek ellenére léteznek bizonyos helyzetek, amikor nem adunk át objektumokat, és a globális térből szerezzük be őket. Például a kód debuggolásakor, amikor egy adott ponton ki kell íratni egy változó értékét, meg kell mérni egy programrész futási idejét, vagy naplózni kell egy üzenetet. Ilyen esetekben, amikor ideiglenes műveletekről van szó, amelyeket később eltávolítanak a kódból, legitim egy globálisan elérhető dumper, stopperóra vagy logger használata. Ezek az eszközök ugyanis nem tartoznak a kód tervezéséhez. - - -Vannak árnyoldalai a DI használatának? --------------------------------------- - -Jár-e a Dependency Injection használata valamilyen hátránnyal, például megnövekedett kódírási igénybevétellel vagy rosszabb teljesítménnyel? Mit veszítünk, ha elkezdünk DI-kompatibilis kódot írni? - -A DI nincs hatással az alkalmazás teljesítményére vagy memóriaigényére. A DI Container teljesítménye játszhat némi szerepet, azonban a [Nette DI |nette-container] esetében a konténer tiszta PHP-ba van fordítva, így a futásidejű overhead lényegében nulla. - -A kódírás során szükség lehet konstruktorok létrehozására, amelyek függőségeket fogadnak el. Korábban ez időigényes lehetett, de a modern IDE-knek és a [constructor property promotion |https://blog.nette.org/hu/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]-nek köszönhetően ez most másodpercek kérdése. A factory-kat könnyen lehet generálni a Nette DI és a PhpStorm plugin segítségével egy egérkattintással. Másrészt nincs szükség singletonok és statikus hozzáférési pontok írására. - -Megállapítható, hogy egy helyesen megtervezett, DI-t használó alkalmazás sem rövidebb, sem hosszabb nem lesz egy singletonokat használó alkalmazáshoz képest. A függőségekkel dolgozó kódrészek csupán ki vannak emelve az egyes osztályokból, és új helyekre kerülnek, azaz a DI konténerbe és a factory-kba. - - -Hogyan írjunk át egy legacy alkalmazást DI-re? ----------------------------------------------- - -Egy legacy alkalmazás átállítása Dependency Injection-re kihívást jelentő folyamat lehet, különösen nagy és komplex alkalmazások esetén. Fontos, hogy ezt a folyamatot szisztematikusan közelítsük meg. - -- A Dependency Injection-re való áttéréskor fontos, hogy a csapat minden tagja megértse az alkalmazott elveket és eljárásokat. -- Először végezzen elemzést a meglévő alkalmazásról, és azonosítsa a kulcsfontosságú komponenseket és azok függőségeit. Készítsen tervet arról, mely részeket kell refaktorálni és milyen sorrendben. -- Implementáljon egy DI konténert, vagy még jobb, ha egy létező könyvtárat használ, például a Nette DI-t. -- Fokozatosan refaktorálja az alkalmazás egyes részeit, hogy Dependency Injection-t használjanak. Ez magában foglalhatja a konstruktorok vagy metódusok módosítását úgy, hogy paraméterként fogadják el a függőségeket. -- Módosítsa azokat a kódrészeket, ahol függőségekkel rendelkező objektumok jönnek létre, hogy ehelyett a függőségeket a konténer injektálja. Ez magában foglalhatja a factory-k használatát. - -Ne feledje, hogy a Dependency Injection-re való áttérés befektetés a kód minőségébe és az alkalmazás hosszú távú fenntarthatóságába. Bár kihívást jelenthet ezeknek a változtatásoknak a végrehajtása, az eredmény egy tisztább, modulárisabb és könnyen tesztelhető kód kell, hogy legyen, amely készen áll a jövőbeli bővítésre és karbantartásra. - - -Miért részesítjük előnyben a kompozíciót az öröklődéssel szemben? ------------------------------------------------------------------ -Célszerűbb a [kompozíciót |nette:introduction-to-object-oriented-programming#Kompozíció] használni az [öröklődés |nette:introduction-to-object-oriented-programming#Öröklődés] helyett, mert a kód újrafelhasználására szolgál anélkül, hogy aggódnunk kellene a változtatások következményei miatt. Tehát lazább kötést biztosít, ahol nem kell attól tartanunk, hogy egy kód módosítása szükségessé teszi egy másik függő kód módosítását. Tipikus példa erre a [constructor hell |passing-dependencies#Constructor hell] néven ismert helyzet. - - -Használható a Nette DI Container a Nette-n kívül? -------------------------------------------------- - -Határozottan. A Nette DI Container a Nette része, de önálló könyvtárként lett tervezve, amely a keretrendszer többi részétől függetlenül használható. Csak telepíteni kell a Composer segítségével, létre kell hozni egy konfigurációs fájlt a szolgáltatások definíciójával, majd néhány sor PHP kóddal létre kell hozni a DI konténert. És azonnal elkezdheti kihasználni a Dependency Injection előnyeit a projektjeiben. - -A konkrét használatot, beleértve a kódokat is, a [Nette DI Container |nette-container] fejezet írja le. - - -Miért van a konfiguráció NEON fájlokban? ----------------------------------------- - -A NEON egy egyszerű és könnyen olvasható konfigurációs nyelv, amelyet a Nette keretében fejlesztettek ki alkalmazások, szolgáltatások és azok függőségeinek beállítására. A JSON-nal vagy YAML-lel összehasonlítva sokkal intuitívabb és rugalmasabb lehetőségeket kínál erre a célra. A NEON-ban természetesen leírhatók olyan kapcsolatok, amelyeket a Symfony & YAML-ben vagy egyáltalán nem lehetne leírni, vagy csak bonyolult leírással. - - -Nem lassítja le az alkalmazást a NEON fájlok feldolgozása? ----------------------------------------------------------- - -Bár a NEON fájlok nagyon gyorsan feldolgozódnak, ez a szempont egyáltalán nem számít. Az ok az, hogy a fájlok feldolgozása csak egyszer történik meg az alkalmazás első indításakor. Ezután legenerálódik a DI konténer kódja, elmentődik a lemezre, és minden további kérésnél elindul anélkül, hogy további feldolgozásra lenne szükség. - -Ez így működik a produkciós környezetben. A fejlesztés során a NEON fájlok minden alkalommal feldolgozódnak, amikor a tartalmuk megváltozik, hogy a fejlesztő mindig naprakész DI konténerrel rendelkezzen. Maga a feldolgozás, ahogy említettük, pillanatok kérdése. - - -Hogyan férek hozzá az osztályomból a konfigurációs fájl paramétereihez? ------------------------------------------------------------------------ - -Tartsuk szem előtt az [1. szabályt: kérd el, hogy átadják |introduction#1. szabály: Kérd el]. Ha egy osztálynak információra van szüksége a konfigurációs fájlból, nem kell azon gondolkodnunk, hogyan jussunk hozzá ehhez az információhoz, ehelyett egyszerűen kérjük el - például az osztály konstruktorán keresztül. Az átadást pedig a konfigurációs fájlban valósítjuk meg. - -Ebben a példában a `%myParameter%` a `myParameter` paraméter értékének helyettesítője, amelyet átadunk a `MyClass` osztály konstruktorának: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Ha több paramétert szeretne átadni, vagy autowiringot szeretne használni, célszerű [a paramétereket objektumba csomagolni |best-practices:passing-settings-to-presenters]. - - -Támogatja a Nette a PSR-11: Container interface-t? --------------------------------------------------- - -A Nette DI Container nem támogatja közvetlenül a PSR-11-et. Azonban, ha interoperabilitásra van szüksége a Nette DI Container és olyan könyvtárak vagy keretrendszerek között, amelyek PSR-11 Container Interface-t várnak, létrehozhat egy [egyszerű adaptert |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], amely hídként szolgál a Nette DI Container és a PSR-11 között. diff --git a/dependency-injection/hu/global-state.texy b/dependency-injection/hu/global-state.texy deleted file mode 100644 index 1cef481b30..0000000000 --- a/dependency-injection/hu/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Globális állapot és singletonok -******************************* - -.[perex] -Figyelmeztetés: A következő konstrukciók rosszul megtervezett kód jelei: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` vagy `static::$var` - -Előfordulnak ezek a konstrukciók a kódjában? Akkor itt a lehetőség a javításra. Talán azt gondolja, hogy ezek általános konstrukciók, amelyeket akár különböző könyvtárak és keretrendszerek példamegoldásaiban is lát. Ha ez így van, akkor a kódjuk tervezése nem jó. - -Most biztosan nem valamilyen akadémiai tisztaságról beszélünk. Minden ilyen konstrukciónak egy közös vonása van: globális állapotot használnak. És ennek romboló hatása van a kód minőségére. Az osztályok hazudnak a függőségeikről. A kód kiszámíthatatlanná válik. Megzavarja a programozókat és csökkenti hatékonyságukat. - -Ebben a fejezetben elmagyarázzuk, miért van ez így, és hogyan kerüljük el a globális állapotot. - - -Globális összekapcsolás ------------------------ - -Egy ideális világban egy objektumnak csak azokkal az objektumokkal kellene tudnia kommunikálni, amelyeket [közvetlenül átadva |passing-dependencies] kapott. Ha létrehozok két `A` és `B` objektumot, és soha nem adok át referenciát közöttük, akkor sem `A`, sem `B` nem férhet hozzá a másik objektumhoz, vagy nem változtathatja meg annak állapotát. Ez a kód egy nagyon kívánatos tulajdonsága. Hasonló ahhoz, mint amikor van egy elem és egy izzó; az izzó nem fog világítani, amíg nem köti össze az elemmel egy dróttal. - -Ez azonban nem igaz a globális (statikus) változókra vagy singletonokra. Az `A` objektum *vezeték nélkül* hozzáférhetne a `C` objektumhoz, és módosíthatná azt anélkül, hogy bármilyen referenciát átadna, azáltal, hogy meghívja a `C::changeSomething()`-t. Ha a `B` objektum is megragadja a globális `C`-t, akkor `A` és `B` kölcsönösen befolyásolhatják egymást a `C`-n keresztül. - -A globális változók használata a *vezeték nélküli* összekapcsolás új formáját vezeti be a rendszerbe, amely kívülről nem látható. Füstfüggönyt hoz létre, amely bonyolítja a kód megértését és használatát. Ahhoz, hogy a fejlesztők valóban megértsék a függőségeket, el kell olvasniuk a forráskód minden sorát. Ahelyett, hogy egyszerűen megismerkednének az osztályok interfészével. Ráadásul ez egy teljesen felesleges összekapcsolás. A globális állapotot azért használják, mert könnyen hozzáférhető bárhonnan, és lehetővé teszi például az adatbázisba írást a globális (statikus) `DB::insert()` metóduson keresztül. De ahogy megmutatjuk, az ebből származó előny elenyésző, míg a okozott komplikációk végzetesek. - -.[note] -Viselkedés szempontjából nincs különbség a globális és a statikus változó között. Ugyanolyan károsak. - - -Kísérteties távolhatás ----------------------- - -"Kísérteties távolhatás" - így nevezte el híresen 1935-ben Albert Einstein a kvantumfizika egy jelenségét, amelytől libabőrös lett. -Ez egy kvantum-összefonódás, amelynek különlegessége, hogy ha megmérjük az információt az egyik részecskéről, azonnal befolyásoljuk a másik részecskét is, még akkor is, ha millió fényév távolságra vannak egymástól. Ami látszólag megsérti az univerzum alapvető törvényét, hogy semmi sem terjedhet gyorsabban a fénynél. - -A szoftver világában "kísérteties távolhatásnak" nevezhetjük azt a helyzetet, amikor elindítunk egy folyamatot, amelyről azt gondoljuk, hogy izolált (mert nem adtunk át neki semmilyen referenciát), de a rendszer távoli pontjain váratlan interakciók és állapotváltozások következnek be, amelyekről nem volt tudomásunk. Ez csak globális állapoton keresztül történhet meg. - -Képzelje el, hogy csatlakozik egy projekt fejlesztői csapatához, amelynek kiterjedt, kiforrott kódbázisa van. Az új vezetője megkéri Önt egy új funkció implementálására, és Ön, mint jó fejlesztő, a teszt írásával kezdi. Mivel azonban új a projektben, sok feltáró tesztet végez, mint például "mi történik, ha meghívom ezt a metódust". És megpróbálja megírni a következő tesztet: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // az Ön kártyaszáma - $cc->charge(100); -} -``` - -Futtatja a kódot, talán többször is, és egy idő után észreveszi a mobilján a banki értesítéseket, hogy minden futtatáskor 100 dollárt vontak le a bankkártyájáról 🤦‍♂️ - -Hogy a fenébe okozhatta a teszt a valódi pénzlevonást? A bankkártyával való művelet nem egyszerű. Kommunikálnia kell egy harmadik fél webszolgáltatásával, ismernie kell ennek a webszolgáltatásnak az URL-jét, be kell jelentkeznie és így tovább. Ezek közül az információk közül egyik sem szerepel a tesztben. Sőt, még azt sem tudja, hol vannak ezek az információk, és így azt sem, hogyan mockolja az externális függőségeket, hogy minden futtatás ne vezessen újabb 100 dollár levonásához. És honnan kellett volna tudnia új fejlesztőként, hogy amit tenni készül, az 100 dollárral szegényebbé teszi? - -Ez a kísérteties távolhatás! - -Nem marad más hátra, mint hosszan turkálni a rengeteg forráskódban, kérdezgetni az idősebb és tapasztaltabb kollégákat, amíg meg nem érti, hogyan működnek a kapcsolatok a projektben. Ez azért van, mert a `CreditCard` osztály interfészének megtekintésekor nem lehet megállapítani a globális állapotot, amelyet inicializálni kell. Még az osztály forráskódjának megtekintése sem árulja el, melyik inicializációs metódust kell meghívnia. Legjobb esetben találhat egy globális változót, amelyhez hozzáférnek, és abból megpróbálhatja kitalálni, hogyan inicializálja. - -Az ilyen projekt osztályai patologikus hazudozók. A bankkártya úgy tesz, mintha elég lenne példányosítani és meghívni a `charge()` metódust. Titokban azonban együttműködik egy másik `PaymentGateway` osztállyal, amely a fizetési kaput képviseli. Annak interfésze is azt mondja, hogy önállóan inicializálható, de valójában kihúzza a hitelesítő adatokat valamilyen konfigurációs fájlból és így tovább. A fejlesztőknek, akik ezt a kódot írták, világos, hogy a `CreditCard`-nak szüksége van a `PaymentGateway`-re. Így írták a kódot. De bárki számára, aki új a projektben, ez teljes rejtély, és akadályozza a tanulást. - -Hogyan javítsuk a helyzetet? Könnyen. **Hagyja, hogy az API deklarálja a függőségeket.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Figyelje meg, hogyan válnak hirtelen nyilvánvalóvá a kódon belüli kapcsolatok. Azzal, hogy a `charge()` metódus deklarálja, hogy szüksége van a `PaymentGateway`-re, nem kell senkitől megkérdeznie, hogyan van összekapcsolva a kód. Tudja, hogy létre kell hoznia annak példányát, és amikor megpróbálja, rájön, hogy meg kell adnia a hozzáférési paramétereket. Nélkülük a kód el sem indulna. - -És ami a legfontosabb, most már mockolhatja a fizetési kaput, így nem vonnak le 100 dollárt minden tesztfuttatáskor. - -A globális állapot miatt az objektumai titokban hozzáférhetnek olyan dolgokhoz, amelyek nincsenek deklarálva az API-jukban, és ennek következtében az API-jai patologikus hazudozókká válnak. - -Talán korábban nem gondolt rá így, de minden alkalommal, amikor globális állapotot használ, titkos vezeték nélküli kommunikációs csatornákat hoz létre. A kísérteties távolhatás arra kényszeríti a fejlesztőket, hogy minden kódsort elolvassanak a potenciális interakciók megértéséhez, csökkenti a fejlesztők termelékenységét és megzavarja az új csapattagokat. Ha Ön hozta létre a kódot, ismeri a valódi függőségeket, de bárki, aki Ön után jön, tanácstalan. - -Ne írjon olyan kódot, amely globális állapotot használ, részesítse előnyben a függőségek átadását. Tehát a dependency injection-t. - - -Globális állapot törékenysége ------------------------------ - -A globális állapotot és singletonokat használó kódban soha nem biztos, hogy mikor és ki változtatta meg ezt az állapotot. Ez a kockázat már az inicializáláskor megjelenik. A következő kódnak adatbázis-kapcsolatot kellene létrehoznia és inicializálnia a fizetési kaput, azonban folyamatosan kivételt dob, és az ok keresése rendkívül hosszadalmas: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Részletesen át kell néznie a kódot, hogy rájöjjön, a `PaymentGateway` objektum vezeték nélkül hozzáfér más objektumokhoz, amelyek közül néhány adatbázis-kapcsolatot igényel. Tehát az adatbázist korábban kell inicializálni, mint a `PaymentGateway`-t. Azonban a globális állapot füstfüggönye ezt elrejti Ön elől. Mennyi időt takaríthatna meg, ha az egyes osztályok API-ja nem hazudna, és deklarálná a függőségeit? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Hasonló probléma merül fel az adatbázis-kapcsolat globális elérésének használatakor is: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -A `save()` metódus hívásakor nem biztos, hogy már létrejött-e az adatbázis-kapcsolat, és ki felelős annak létrehozásáért. Ha például futás közben szeretnénk megváltoztatni az adatbázis-kapcsolatot, például tesztek miatt, valószínűleg további metódusokat kellene létrehoznunk, mint például `DB::reconnect(...)` vagy `DB::reconnectForTest()`. - -Vegyünk egy példát: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Hol van a biztosíték arra, hogy a `$article->save()` hívásakor valóban a tesztadatbázist használjuk? Mi van, ha a `Foo::doSomething()` metódus megváltoztatta a globális adatbázis-kapcsolatot? Ennek kiderítéséhez meg kellene vizsgálnunk a `Foo` osztály forráskódját, és valószínűleg sok más osztályét is. Ez a megközelítés azonban csak rövid távú választ adna, mivel a helyzet a jövőben megváltozhat. - -És mi van, ha az adatbázis-kapcsolatot egy statikus változóba helyezzük az `Article` osztályon belül? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Ezzel egyáltalán semmi sem változott. A probléma a globális állapot, és teljesen mindegy, melyik osztályban rejtőzik. Ebben az esetben, akárcsak az előzőben, a `$article->save()` metódus hívásakor nincs semmilyen támpontunk arra vonatkozóan, hogy melyik adatbázisba íródik. Bárki az alkalmazás másik végén bármikor megváltoztathatta az adatbázist az `Article::setDb()` segítségével. A kezünk alatt. - -A globális állapot **rendkívül törékennyé** teszi az alkalmazásunkat. - -Van azonban egy egyszerű módja ennek a problémának a kezelésére. Csak hagyni kell, hogy az API deklarálja a függőségeket, ami biztosítja a helyes működést. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Ennek a megközelítésnek köszönhetően megszűnik az aggodalom a rejtett és váratlan adatbázis-kapcsolat változások miatt. Most már biztosak lehetünk benne, hova mentődik a cikk, és semmilyen kódmódosítás egy másik, nem kapcsolódó osztályon belül már nem változtathat a helyzeten. A kód már nem törékeny, hanem stabil. - -Ne írjon olyan kódot, amely globális állapotot használ, részesítse előnyben a függőségek átadását. Tehát a dependency injection-t. - - -Singleton ---------- - -A Singleton egy tervezési minta, amely a híres Gang of Four kiadvány "definíciója":https://en.wikipedia.org/wiki/Singleton_pattern szerint egy osztályt egyetlen példányra korlátoz, és globális hozzáférést kínál hozzá. Ennek a mintának az implementációja általában a következő kódhoz hasonlít: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // és további metódusok, amelyek az adott osztály funkcióit töltik be -} -``` - -Sajnos a singleton globális állapotot vezet be az alkalmazásba. És ahogy fentebb megmutattuk, a globális állapot nemkívánatos. Ezért a singletont antipattern-nek tekintik. - -Ne használjon singletonokat a kódjában, és helyettesítse őket más mechanizmusokkal. Valóban nincs szüksége singletonokra. Ha azonban garantálnia kell egy osztály egyetlen példányának létezését az egész alkalmazás számára, bízza azt a [DI konténerre |container]. Hozzon létre így egy alkalmazás szintű singletont, azaz egy szolgáltatást. Ezzel az osztály megszűnik foglalkozni saját egyediségének biztosításával (azaz nem lesz `getInstance()` metódusa és statikus változója), és csak a funkcióit fogja ellátni. Így megszűnik megsérteni az egyetlen felelősség elvét. - - -Globális állapot versus tesztek -------------------------------- - -Tesztek írásakor feltételezzük, hogy minden teszt egy izolált egység, és hogy semmilyen külső állapot nem lép be. És semmilyen állapot nem hagyja el a teszteket. A teszt befejezése után minden, a teszthez kapcsolódó állapotot automatikusan el kell távolítania a garbage collectornak. Ennek köszönhetően a tesztek izoláltak. Ezért futtathatjuk a teszteket tetszőleges sorrendben. - -Ha azonban globális állapotok/singletonok vannak jelen, mindezek a kellemes feltételezések összeomlanak. Az állapot beléphet a tesztbe és kiléphet belőle. Hirtelen számíthat a tesztek sorrendje. - -Ahhoz, hogy egyáltalán tesztelni tudjuk a singletonokat, a fejlesztők gyakran kénytelenek lazítani a tulajdonságaikat, például azáltal, hogy megengedik a példány cseréjét egy másikkal. Az ilyen megoldások legjobb esetben is hackek, amelyek nehezen karbantartható és érthető kódot hoznak létre. Minden tesztnek vagy `tearDown()` metódusnak, amely bármilyen globális állapotot befolyásol, vissza kell állítania ezeket a változtatásokat. - -A globális állapot a legnagyobb fejfájás az unit tesztelés során! - -Hogyan javítsuk a helyzetet? Könnyen. Ne írjon olyan kódot, amely singletonokat használ, részesítse előnyben a függőségek átadását. Tehát a dependency injection-t. - - -Globális konstansok -------------------- - -A globális állapot nem korlátozódik csak a singletonok és statikus változók használatára, hanem globális konstansokra is vonatkozhat. - -Azok a konstansok, amelyek értéke nem hoz számunkra semmilyen új (`M_PI`) vagy hasznos (`PREG_BACKTRACK_LIMIT_ERROR`) információt, egyértelműen rendben vannak. Ellenben azok a konstansok, amelyek arra szolgálnak, hogy *vezeték nélkül* információt adjanak át a kódba, nem mások, mint rejtett függőségek. Mint például a `LOG_FILE` a következő példában. A `FILE_APPEND` konstans használata teljesen korrekt. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Ebben az esetben deklarálnunk kellene egy paramétert a `Foo` osztály konstruktorában, hogy az API részévé váljon: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Most már átadhatjuk az információt a naplófájl elérési útjáról, és szükség szerint könnyen megváltoztathatjuk, ami megkönnyíti a kód tesztelését és karbantartását. - - -Globális függvények és statikus metódusok ------------------------------------------ - -Szeretnénk hangsúlyozni, hogy maguk a statikus metódusok és globális függvények használata nem problematikus. Elmagyaráztuk, miért nem megfelelő a `DB::insert()` és hasonló metódusok használata, de ez mindig csak a globális állapot kérdése volt, amely valamilyen statikus változóban van tárolva. A `DB::insert()` metódus megköveteli egy statikus változó létezését, mert abban van tárolva az adatbázis-kapcsolat. E változó nélkül lehetetlen lenne a metódust implementálni. - -Determinisztikus statikus metódusok és függvények használata, mint például a `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` és sok más, teljes mértékben összhangban van a dependency injection-nel. Ezek a függvények mindig ugyanazokat az eredményeket adják vissza ugyanazokból a bemeneti paraméterekből, és ezért előrejelezhetők. Nem használnak semmilyen globális állapotot. - -Léteznek azonban olyan függvények is PHP-ban, amelyek nem determinisztikusak. Ezek közé tartozik például a `htmlspecialchars()` függvény. Annak harmadik paramétere, a `$encoding`, ha nincs megadva, alapértelmezett értékként a `ini_get('default_charset')` konfigurációs opció értékét veszi fel. Ezért ajánlott ezt a paramétert mindig megadni, hogy elkerüljük a függvény esetleges kiszámíthatatlan viselkedését. A Nette ezt következetesen megteszi. - -Néhány függvény, mint például a `strtolower()`, `strtoupper()` és hasonlók, a közelmúltban nem determinisztikusan viselkedtek, és a `setlocale()` beállítástól függtek. Ez sok komplikációt okozott, leggyakrabban a török nyelvvel való munka során. Az ugyanis megkülönbözteti a kis- és nagybetűs `I`-t ponttal és pont nélkül is. Így a `strtolower('I')` az `ı` karaktert adta vissza, a `strtoupper('i')` pedig az `İ` karaktert, ami ahhoz vezetett, hogy az alkalmazások számos rejtélyes hibát kezdtek okozni. Ezt a problémát azonban a PHP 8.2-es verziójában orvosolták, és a függvények már nem függnek a locale-tól. - -Ez egy szép példa arra, hogyan okozott fejfájást a globális állapot több ezer fejlesztőnek világszerte. A megoldás az volt, hogy dependency injection-nel helyettesítették. - - -Mikor lehet globális állapotot használni? ------------------------------------------ - -Léteznek bizonyos specifikus helyzetek, amikor lehet globális állapotot használni. Például a kód debuggolásakor, amikor ki kell íratni egy változó értékét, vagy meg kell mérni egy programrész futási idejét. Ilyen esetekben, amelyek ideiglenes műveletekre vonatkoznak, amelyeket később eltávolítanak a kódból, legitim egy globálisan elérhető dumper vagy stopperóra használata. Ezek az eszközök ugyanis nem részei a kód tervezésének. - -Egy másik példa a reguláris kifejezésekkel dolgozó `preg_*` függvények, amelyek belsőleg statikus cache-ben tárolják a lefordított reguláris kifejezéseket a memóriában. Tehát ha ugyanazt a reguláris kifejezést többször hívja meg a kód különböző pontjain, csak egyszer fordítódik le. A cache teljesítményt takarít meg, és ugyanakkor a felhasználó számára teljesen láthatatlan, ezért az ilyen használat legitimnek tekinthető. - - -Összegzés ---------- - -Megbeszéltük, miért van értelme: - -1) Eltávolítani minden statikus változót a kódból -2) Deklarálni a függőségeket -3) És használni a dependency injection-t - -Amikor a kód tervezésén gondolkodik, gondoljon arra, hogy minden `static $foo` problémát jelent. Ahhoz, hogy a kódja DI-t tiszteletben tartó környezet legyen, elengedhetetlen a globális állapot teljes kiirtása és dependency injection-nel való helyettesítése. - -E folyamat során talán rájön, hogy egy osztályt fel kell osztani, mert több felelőssége van. Ne féljen ettől; törekedjen az egyetlen felelősség elvére. - -*Szeretnék köszönetet mondani Miško Hevery-nek, akinek cikkei, mint például a [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], képezik ennek a fejezetnek az alapját.* diff --git a/dependency-injection/hu/introduction.texy b/dependency-injection/hu/introduction.texy deleted file mode 100644 index 8cd8b4fe69..0000000000 --- a/dependency-injection/hu/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Mi az a Dependency Injection? -***************************** - -.[perex] -Ez a fejezet bemutatja azokat az alapvető programozási gyakorlatokat, amelyeket minden alkalmazás írásakor követnie kell. Ezek az alapok szükségesek a tiszta, érthető és karbantartható kód írásához. - -Ha elsajátítja és követi ezeket a szabályokat, a Nette minden lépésben segíteni fog Önnek. Kezelni fogja a rutinfeladatokat, és maximális kényelmet biztosít Önnek, hogy a tényleges logikára koncentrálhasson. - -Az itt bemutatott elvek meglehetősen egyszerűek. Nincs mitől félnie. - - -Emlékszel az első programodra? ------------------------------- - -Nem tudjuk, milyen nyelven írta, de ha PHP lett volna, valószínűleg így nézett volna ki: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // kiírja a 24-et -``` - -Néhány triviális kódsor, de annyi kulcsfontosságú koncepciót rejtenek magukban. Hogy vannak változók. Hogy a kód kisebb egységekre van osztva, mint például a függvények. Hogy bemeneti argumentumokat adunk át nekik, és eredményeket adnak vissza. Már csak a feltételek és a ciklusok hiányoznak. - -Az, hogy bemeneti adatokat adunk át egy függvénynek, és az eredményt ad vissza, egy tökéletesen érthető koncepció, amelyet más területeken is használnak, például a matematikában. - -Egy függvénynek van szignatúrája, amely a nevéből, a paraméterek és típusaik listájából, valamint végül a visszatérési érték típusából áll. Felhasználóként minket a szignatúra érdekel, a belső megvalósításról általában nem kell tudnunk semmit. - -Most képzelje el, hogy a függvény szignatúrája így néz ki: - -```php -function soucet(float $x): float -``` - -Összeadás egy paraméterrel? Ez furcsa… És mi van ezzel? - -```php -function soucet(): float -``` - -Ez már tényleg nagyon furcsa, nem? Hogyan használják a függvényt? - -```php -echo soucet(); // vajon mit ír ki? -``` - -Egy ilyen kódot látva összezavarodnánk. Nemcsak egy kezdő nem értené, de egy tapasztalt programozó sem. - -Gondolkodik azon, hogyan nézne ki egy ilyen függvény belülről? Honnan veszi az összeadandókat? Valószínűleg *valahogy* maga szerezné be őket, például így: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -A függvény törzsében rejtett függőségeket fedeztünk fel más globális függvényekre vagy statikus metódusokra. Ahhoz, hogy megtudjuk, honnan származnak valójában az összeadandók, tovább kell kutatnunk. - - -Nem erre! ---------- - -Az imént bemutatott tervezés számos negatív tulajdonság esszenciája: - -- A függvény szignatúrája úgy tett, mintha nem lenne szüksége összeadandókra, ami félrevezetett minket. -- Fogalmunk sincs, hogyan vegyük rá a függvényt, hogy két másik számot adjon össze. -- Bele kellett néznünk a kódba, hogy megtudjuk, honnan veszi az összeadandókat. -- Rejtett függőségeket fedeztünk fel. -- A teljes megértéshez ezeket a függőségeket is meg kell vizsgálni. - -És egyáltalán az összeadó függvény feladata a bemenetek beszerzése? Természetesen nem. Az ő felelőssége csak maga az összeadás. - - -Ilyen kóddal nem akarunk találkozni, és határozottan nem akarunk ilyet írni. A javítás egyszerű: térjünk vissza az alapokhoz, és egyszerűen használjunk paramétereket: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -1. szabály: Kérd el -------------------- - -A legfontosabb szabály: **minden adatot, amire egy függvénynek vagy osztálynak szüksége van, át kell adni neki**. - -Ahelyett, hogy rejtett módokat találnál ki, amelyekkel maguk is hozzáférhetnének, egyszerűen add át a paramétereket. Időt takarítasz meg a rejtett utak kitalálásával, amelyek biztosan nem javítják a kódodat. - -Ha ezt a szabályt mindig és mindenhol betartod, úton vagy a rejtett függőségek nélküli kód felé. Egy olyan kód felé, amely nemcsak a szerző számára érthető, hanem bárki számára is, aki utána olvassa. Ahol minden érthető a függvények és osztályok szignatúráiból, és nem kell rejtett titkok után kutatni a megvalósításban. - -Ezt a technikát szakmailag **dependency injection**-nek (függőséginjektálás) nevezik. És ezeket az adatokat **függőségeknek** (dependencies). Valójában ez csak egyszerű paraméterátadás, semmi több. - -.[note] -Kérjük, ne keverje össze a dependency injection-t, ami egy tervezési minta, a „dependency injection container”-rel, ami egy eszköz, tehát valami gyökeresen más. A konténerekkel később foglalkozunk. - - -Függvényektől az osztályokig ----------------------------- - -És hogyan kapcsolódik ez az osztályokhoz? Az osztály egy összetettebb egység, mint egy egyszerű függvény, de az 1. szabály itt is maradéktalanul érvényes. Csak [több lehetőség van az argumentumok átadására|passing-dependencies]. Például egészen hasonlóan, mint egy függvénynél: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Vagy más metódusokkal, vagy közvetlenül a konstruktorral: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Mindkét példa teljes mértékben összhangban van a dependency injection elvével. - - -Valós példák ------------- - -A való világban nem fogsz osztályokat írni számok összeadására. Térjünk át a gyakorlati példákra. - -Legyen egy `Article` osztályunk, amely egy blogbejegyzést reprezentál: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // elmentjük a cikket az adatbázisba - } -} -``` - -és a használat a következő lesz: - -```php -$article = new Article; -$article->title = '10 dolog, amit tudnod kell a fogyásról'; -$article->content = 'Minden évben emberek milliói ...'; -$article->save(); -``` - -A `save()` metódus elmenti a cikket egy adatbázis táblába. A [Nette Database |database:] segítségével megvalósítani gyerekjáték lenne, ha nem lenne egy bökkenő: honnan veszi az `Article` az adatbázis-kapcsolatot, azaz a `Nette\Database\Connection` osztály objektumát? - -Úgy tűnik, sok lehetőségünk van. Veheti valahonnan egy statikus változóból. Vagy örökölhet egy olyan osztálytól, amely biztosítja az adatbázis-kapcsolatot. Vagy használhatja az úgynevezett [singleton |global-state#Singleton] mintát. Vagy az úgynevezett facades-okat, amelyeket a Laravelben használnak: - -```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], - ); - } -} -``` - -Nagyszerű, megoldottuk a problémát. - -Vagy mégsem? - -Idézzük fel az [##1. szabály: Kérd el]: minden függőséget, amire az osztálynak szüksége van, át kell adni neki. Mert ha megszegjük a szabályt, a piszkos kód útjára léptünk, tele rejtett függőségekkel, érthetetlenséggel, és az eredmény egy olyan alkalmazás lesz, amelyet fájdalmas lesz karbantartani és fejleszteni. - -Az `Article` osztály felhasználója nem tudja, hova menti a `save()` metódus a cikket. Adatbázis táblába? Melyikbe, az élesbe vagy a tesztbe? És hogyan lehet ezt megváltoztatni? - -A felhasználónak meg kell néznie, hogyan van implementálva a `save()` metódus, és megtalálja a `DB::insert()` metódus használatát. Tehát tovább kell kutatnia, hogyan szerzi be ez a metódus az adatbázis-kapcsolatot. És a rejtett függőségek elég hosszú láncot alkothatnak. - -A tiszta és jól megtervezett kódban soha nincsenek rejtett függőségek, Laravel facade-ok vagy statikus változók. A tiszta és jól megtervezett kódban argumentumokat adnak át: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Még praktikusabb lesz, ahogy később látni fogjuk, a konstruktorral: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Ha tapasztalt programozó vagy, talán azt gondolod, hogy az `Article`-nek egyáltalán nem kellene `save()` metódussal rendelkeznie, tisztán adatkomponensnek kellene lennie, és a mentésről egy különálló repositorynak kellene gondoskodnia. Ennek van értelme. De ezzel messze túllépnénk a témán, ami a dependency injection, és az egyszerű példák bemutatására tett erőfeszítésen. - -Ha olyan osztályt írsz, amelynek a működéséhez például adatbázisra van szüksége, ne azon gondolkodj, honnan szerezd be, hanem kérd el. Például a konstruktor vagy egy másik metódus paramétereként. Ismerd el a függőségeket. Ismerd el őket az osztályod API-jában. Érthető és kiszámítható kódot kapsz. - -És mi van ezzel az osztállyal, amely hibaüzeneteket naplóz: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Mit gondolsz, betartottuk az [##1. szabály: Kérd el]? - -Nem tartottuk be. - -A kulcsinformációt, azaz a naplófájlt tartalmazó könyvtárat, az osztály *maga szerzi be* egy konstansból. - -Nézd meg a használati példát: - -```php -$logger = new Logger; -$logger->log('A hőmérséklet 23 °C'); -$logger->log('A hőmérséklet 10 °C'); -``` - -Az implementáció ismerete nélkül tudnál válaszolni arra a kérdésre, hogy hova íródnak az üzenetek? Eszedbe jutna, hogy a működéshez szükség van a `LOG_DIR` konstans létezésére? És tudnál létrehozni egy második példányt, amely máshova ír? Biztosan nem. - -Javítsuk ki az osztályt: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Az osztály most sokkal érthetőbb, konfigurálhatóbb és ezáltal hasznosabb. - -```php -$logger = new Logger('/útvonal/a/naplóhoz.txt'); -$logger->log('A hőmérséklet 15 °C'); -``` - - -De ez engem nem érdekel! ------------------------- - -*"Amikor létrehozok egy Article objektumot és meghívom a save()-t, nem akarok az adatbázissal foglalkozni, egyszerűen azt akarom, hogy abba mentse el, amit a konfigurációban beállítottam."* - -*"Amikor a Logger-t használom, egyszerűen azt akarom, hogy az üzenet íródjon ki, és nem akarom megoldani, hogy hova. Használja a globális beállítást."* - -Ezek helyes észrevételek. - -Példaként egy hírleveleket küldő osztályt mutatunk be, amely naplózza, hogyan sikerült: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Az e-mailek elküldve'); - - } catch (Exception $e) { - $logger->log('Hiba történt a küldés során'); - throw $e; - } - } -} -``` - -A továbbfejlesztett `Logger`, amely már nem használja a `LOG_DIR` konstansot, a konstruktorban megköveteli a fájl elérési útjának megadását. Hogyan oldjuk ezt meg? A `NewsletterDistributor` osztályt egyáltalán nem érdekli, hova íródnak az üzenetek, csak ki akarja írni őket. - -A megoldás ismét az [##1. szabály: Kérd el]: minden adatot, amire az osztálynak szüksége van, átadunk neki. - -Tehát ez azt jelenti, hogy a konstruktoron keresztül átadjuk a napló elérési útját, amelyet aztán a `Logger` objektum létrehozásakor használunk? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ NEM ÍGY! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Nem így! Az elérési út ugyanis **nem tartozik** azok közé az adatok közé, amelyekre a `NewsletterDistributor` osztálynak szüksége van; azokra ugyanis a `Logger`-nek van szüksége. Érzed a különbséget? A `NewsletterDistributor` osztálynak magára a loggerre van szüksége. Tehát azt adjuk át: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Az e-mailek elküldve'); - - } catch (Exception $e) { - $this->logger->log('Hiba történt a küldés során'); - throw $e; - } - } -} -``` - -Most már a `NewsletterDistributor` osztály szignatúráiból világos, hogy a funkcionalitásának része a naplózás is. És a logger cseréjének feladata egy másikra, például tesztelés céljából, teljesen triviális. Ráadásul, ha a `Logger` osztály konstruktora megváltozna, az nem lenne hatással az osztályunkra. - - -2. szabály: Vedd el, ami a tiéd -------------------------------- - -Ne hagyd magad megtéveszteni, és ne kérd a függőségeid függőségeinek átadását. Csak a saját függőségeidet kérd el. - -Ennek köszönhetően a más objektumokat használó kód teljesen független lesz a konstruktoraik változásaitól. Az API-ja igazabb lesz. És főleg triviális lesz ezeket a függőségeket másokra cserélni. - - -Új családtag ------------- - -A fejlesztői csapat úgy döntött, hogy létrehoz egy második loggert, amely adatbázisba ír. Tehát létrehozunk egy `DatabaseLogger` osztályt. Így van két osztályunk, a `Logger` és a `DatabaseLogger`, az egyik fájlba ír, a másik adatbázisba… nem tűnik valami furcsának az elnevezés? Nem lenne jobb átnevezni a `Logger`-t `FileLogger`-re? Biztosan igen. - -De okosan csináljuk. Az eredeti név alatt létrehozunk egy interfészt: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… amelyet mindkét logger implementálni fog: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -Ennek köszönhetően nem kell semmit sem változtatni a kód többi részében, ahol a loggert használják. Például a `NewsletterDistributor` osztály konstruktora továbbra is elégedett lesz azzal, hogy paraméterként `Logger`-t igényel. És csak rajtunk múlik, melyik példányt adjuk át neki. - -**Ezért soha nem adunk az interfészek nevéhez `Interface` utótagot vagy `I` előtagot.** Különben nem lehetne a kódot ilyen szépen fejleszteni. - - -Houston, van egy problémánk ---------------------------- - -Míg az egész alkalmazásban megelégedhetünk egyetlen logger példánnyal, legyen az fájl- vagy adatbázis-alapú, és egyszerűen átadjuk mindenhol, ahol valami naplózásra kerül, egészen más a helyzet az `Article` osztály esetében. Ennek példányait ugyanis szükség szerint hozzuk létre, akár többször is. Hogyan kezeljük az adatbázis-függőséget a konstruktorában? - -Példaként szolgálhat egy kontroller, amelynek egy űrlap elküldése után el kell mentenie a cikket az adatbázisba: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Egy lehetséges megoldás közvetlenül adódik: átadjuk az adatbázis objektumot a konstruktoron keresztül az `EditController`-nek, és használjuk a `$article = new Article($this->db)` kódot. - -Ahogy az előző esetben a `Logger`-rel és a fájl elérési útjával, ez sem a helyes megközelítés. Az adatbázis nem az `EditController` függősége, hanem az `Article`-é. Az adatbázis átadása tehát ellentétes a [#2. szabály: Vedd el, ami a tiéd] szabállyal. Ha az `Article` osztály konstruktora megváltozik (új paraméter kerül hozzáadásra), akkor a kódot is módosítani kell mindenhol, ahol példányt hoznak létre. Pfff. - -Houston, mit javasolsz? - - -3. szabály: Hagyd a factory-ra ------------------------------- - -Azzal, hogy megszüntettük a rejtett függőségeket, és minden függőséget argumentumként adunk át, konfigurálhatóbb és rugalmasabb osztályokat kaptunk. És ezért szükségünk van még valamire, ami létrehozza és konfigurálja nekünk ezeket a rugalmasabb osztályokat. Ezt factory-nak (gyárnak) fogjuk nevezni. - -A szabály így szól: ha egy osztálynak függőségei vannak, hagyd a példányok létrehozását a factory-ra. - -A factory-k az `new` operátor okosabb helyettesítői a dependency injection világában. - -.[note] -Kérjük, ne keverje össze a *factory method* tervezési mintával, amely a factory-k specifikus felhasználási módját írja le, és nem kapcsolódik ehhez a témához. - - -Factory -------- - -A factory egy metódus vagy osztály, amely objektumokat gyárt és konfigurál. Az `Article`-t gyártó osztályt `ArticleFactory`-nak nevezzük, és például így nézhet ki: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Használata a kontrollerben a következő lesz: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // hagyjuk, hogy a factory hozza létre az objektumot - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Ha ebben a pillanatban megváltozik az `Article` osztály konstruktorának szignatúrája, az egyetlen kódrészlet, amelynek reagálnia kell rá, maga a `ArticleFactory`. Minden más kód, amely `Article` objektumokkal dolgozik, mint például az `EditController`, ettől érintetlen marad. - -Talán most a homlokodra csapsz, hogy egyáltalán segítettünk-e magunkon. A kód mennyisége megnőtt, és az egész kezd gyanúsan bonyolultnak tűnni. - -Ne aggódj, hamarosan eljutunk a Nette DI konténerhez. És annak számos aduásza van a tarsolyában, amelyek rendkívül leegyszerűsítik a dependency injectiont használó alkalmazások építését. Például az `ArticleFactory` osztály helyett elég lesz [csak egy interfészt írni |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -De ezzel előreszaladunk, még tarts ki :-) - - -Összegzés ---------- - -Ennek a fejezetnek az elején azt ígértük, hogy bemutatunk egy módszert a tiszta kód tervezésére. Elég az osztályoknak - -1) [átadni a szükséges függőségeket |#1. szabály: Kérd el] -2) [és fordítva, nem átadni azt, amire közvetlenül nincs szükségük |#2. szabály: Vedd el ami a tiéd] -3) [és hogy a függőségekkel rendelkező objektumokat a legjobban factory-kban lehet létrehozni |#3. szabály: Hagyd a factory-ra] - -Első pillantásra talán nem tűnik úgy, de ennek a három szabálynak messzemenő következményei vannak. Radikálisan más nézőponthoz vezetnek a kódtervezésben. Megéri? Azok a programozók, akik elhagyták régi szokásaikat és következetesen elkezdték használni a dependency injectiont, ezt a lépést szakmai életük kulcsfontosságú pillanatának tartják. Megnyílt előttük az áttekinthető és karbantartható alkalmazások világa. - -De mi van, ha a kód nem használja következetesen a dependency injectiont? Mi van, ha statikus metódusokra vagy singletonokra épül? Ez okoz valamilyen problémát? [Igen, és nagyon alapvetőeket |global-state]. diff --git a/dependency-injection/hu/nette-container.texy b/dependency-injection/hu/nette-container.texy deleted file mode 100644 index ffcccc429a..0000000000 --- a/dependency-injection/hu/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI Container -****************** - -.[perex] -A Nette DI a Nette egyik legérdekesebb könyvtára. Képes generálni és automatikusan frissíteni a lefordított DI konténereket, amelyek rendkívül gyorsak és elképesztően könnyen konfigurálhatók. - -A DI konténer által létrehozandó szolgáltatások formáját általában konfigurációs fájlokban definiáljuk [NEON formátumban|neon:format]. A konténer, amelyet manuálisan hoztunk létre az [előző fejezetben|container], így íródna le: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -A leírás valóban tömör. - -Az `ArticleFactory` és `UserController` osztályok konstruktoraiban deklarált összes függőséget a Nette DI maga kideríti és átadja az úgynevezett [autowiring|autowiring] segítségével, ezért a konfigurációs fájlban semmit sem kell megadni. Tehát még ha a paraméterek megváltoznak is, a konfigurációban semmit sem kell módosítani. A Nette konténer automatikusan újragenerálódik. Ön így tisztán az alkalmazás fejlesztésére koncentrálhat. - -Ha a függőségeket setterek segítségével szeretnénk átadni, használjuk a [setup |services#Setup] szekciót. - -A Nette DI közvetlenül PHP kódot generál a konténerhez. Az eredmény tehát egy `.php` fájl, amelyet megnyithat és tanulmányozhat. Ennek köszönhetően pontosan láthatja, hogyan működik a konténer. Debuggolhatja is az IDE-ben és lépésenként végigkövetheti. És ami a legfontosabb: a generált PHP rendkívül gyors. - -A Nette DI képes [factory|factory] kódot is generálni egy megadott interfész alapján. Ezért az `ArticleFactory` osztály helyett elég lesz csak egy interfészt létrehozni az alkalmazásban: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -A teljes példát megtalálja [GitHubon|https://github.com/nette-examples/di-example-doc]. - - -Önálló használat ----------------- - -A Nette DI könyvtár bevezetése egy alkalmazásba nagyon egyszerű. Először telepítjük a Composerrel (mert a zip fájlok letöltése annyira elavult): - -```shell -composer require nette/di -``` - -A következő kód létrehoz egy DI konténer példányt a `config.neon` fájlban tárolt konfiguráció alapján: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -A konténer csak egyszer generálódik le, a kódja a cache-be íródik (a `__DIR__ . '/temp'` könyvtárba), és a további kéréseknél már csak innen töltődik be. - -A szolgáltatások létrehozására és lekérésére a `getService()` vagy a `getByType()` metódusok szolgálnak. Így hozunk létre egy `UserController` objektumot: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -Fejlesztés közben hasznos aktiválni az auto-refresh módot, amelyben a konténer automatikusan újragenerálódik, ha bármelyik osztály vagy konfigurációs fájl megváltozik. Ehhez elég a `ContainerLoader` konstruktorában második argumentumként `true`-t megadni. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Használat a Nette keretrendszerrel ----------------------------------- - -Ahogy bemutattuk, a Nette DI használata nem korlátozódik a Nette Frameworkben írt alkalmazásokra, mindössze 3 sor kóddal bárhol bevethető. Ha azonban alkalmazásokat fejleszt a Nette Frameworkben, a konténer konfigurálását és létrehozását a [Bootstrap |application:bootstrapping#DI konténer konfigurálása] végzi. diff --git a/dependency-injection/hu/passing-dependencies.texy b/dependency-injection/hu/passing-dependencies.texy deleted file mode 100644 index 6d33c4c5f1..0000000000 --- a/dependency-injection/hu/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Függőségek átadása -****************** - -<div class=perex> - -Az argumentumokat, vagy a DI terminológiájában „függőségeket”, a következő fő módokon lehet átadni az osztályoknak: - -* konstruktoron keresztüli átadás -* metóduson (úgynevezett setteren) keresztüli átadás -* property beállításával -* *inject* metódussal, annotációval vagy attribútummal - -</div> - -Most az egyes változatokat konkrét példákon mutatjuk be. - - -Konstruktoron keresztüli átadás -=============================== - -A függőségek az objektum létrehozásának pillanatában kerülnek átadásra a konstruktor argumentumaiként: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Ez a forma alkalmas a kötelező függőségekre, amelyekre az osztálynak feltétlenül szüksége van a működéséhez, mivel nélkülük nem lehet példányt létrehozni. - -PHP 8.0 óta használhatunk rövidebb írásmódot ([constructor property promotion |https://blog.nette.org/hu/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), amely funkcionálisan ekvivalens: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -PHP 8.1 óta a property-t `readonly` jelzővel lehet ellátni, amely deklarálja, hogy a property tartalma már nem fog megváltozni: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -A DI konténer automatikusan átadja a függőségeket a konstruktornak az [autowiring |autowiring] segítségével. Azokat az argumentumokat, amelyeket így nem lehet átadni (pl. stringek, számok, booleanek), [a konfigurációban írjuk le |services#Argumentumok]. - - -Constructor hell ----------------- - -A *constructor hell* kifejezés azt a helyzetet jelöli, amikor egy leszármazott egy szülő osztálytól örököl, amelynek konstruktora függőségeket igényel, és ugyanakkor a leszármazott is függőségeket igényel. Eközben át kell vennie és át kell adnia a szülő függőségeit is: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -A probléma akkor merül fel, amikor meg akarjuk változtatni a `BaseClass` osztály konstruktorát, például ha új függőség kerül hozzáadásra. Ekkor ugyanis módosítani kell az összes leszármazott konstruktorát is. Ami egy ilyen módosítást pokollá tesz. - -Hogyan előzzük ezt meg? A megoldás az, hogy **előnyben részesítjük a [kompozíciót az öröklődéssel szemben |faq#Miért részesítjük előnyben a kompozíciót az öröklődéssel szemben]**. - -Tehát másképp tervezzük meg a kódot. Kerülni fogjuk az [absztrakt |nette:introduction-to-object-oriented-programming#Absztrakt osztályok] `Base*` osztályokat. Ahelyett, hogy a `MyClass` bizonyos funkcionalitást úgy szerezne meg, hogy a `BaseClass`-tól örököl, ezt a funkcionalitást függőségként kapja meg: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Setteren keresztüli átadás -========================== - -A függőségek egy metódus hívásával kerülnek átadásra, amely egy privát property-be menti őket. Ezeknek a metódusoknak a szokásos elnevezési konvenciója a `set*()` forma, ezért settereknek nevezik őket, de természetesen bármilyen más néven is nevezhetők. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Ez a módszer alkalmas a nem kötelező függőségekre, amelyek nem szükségesek az osztály működéséhez, mivel nincs garantálva, hogy az objektum ténylegesen megkapja a függőséget (azaz hogy a felhasználó meghívja a metódust). - -Ugyanakkor ez a módszer lehetővé teszi a setter ismételt meghívását és a függőség megváltoztatását. Ha ez nem kívánatos, adjunk hozzá egy ellenőrzést a metódushoz, vagy PHP 8.1 óta jelöljük a `$cache` property-t `readonly` jelzővel. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('The dependency has already been set'); - } - $this->cache = $cache; - } -} -``` - -A setter hívását a DI konténer konfigurációjában a [setup kulcsban |services#Setup] definiáljuk. Itt is automatikus függőségátadás történik az autowiring segítségével: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Property beállításával -====================== - -A függőségek közvetlenül a tagváltozóba (property-be) írással kerülnek átadásra: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Ez a módszer nem megfelelőnek tekinthető, mivel a property-t `public`-ként kell deklarálni. Így nincs ellenőrzésünk afölött, hogy az átadott függőség valóban a megadott típusú-e (ez a PHP 7.4 előtt volt érvényes), és elveszítjük a lehetőséget, hogy saját kóddal reagáljunk az újonnan hozzárendelt függőségre, például megakadályozzuk a későbbi módosítást. Ugyanakkor a property az osztály nyilvános interfészének részévé válik, ami nem feltétlenül kívánatos. - -A property beállítását a DI konténer konfigurációjában a [setup szekcióban |services#Setup] definiáljuk: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Míg az előző három módszer általánosan érvényes minden objektumorientált nyelvben, a metódussal, annotációval vagy *inject* attribútummal történő injektálás kizárólag a Nette presenterjeire jellemző. Ezekről egy [külön fejezet |best-practices:inject-method-attribute] szól. - - -Melyik módszert válasszuk? -========================== - -- A konstruktor alkalmas a kötelező függőségekre, amelyekre az osztálynak feltétlenül szüksége van a működéséhez. -- A setter viszont alkalmas a nem kötelező függőségekre, vagy olyan függőségekre, amelyeket lehetőség szerint tovább lehet módosítani. -- A public property-k nem megfelelőek. diff --git a/dependency-injection/hu/services.texy b/dependency-injection/hu/services.texy deleted file mode 100644 index d1891fed65..0000000000 --- a/dependency-injection/hu/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Szolgáltatások definiálása -************************** - -.[perex] -A konfiguráció az a hely, ahol megtanítjuk a DI konténernek, hogyan állítsa össze az egyes szolgáltatásokat, és hogyan kapcsolja össze őket más függőségekkel. A Nette nagyon áttekinthető és elegáns módot kínál ennek elérésére. - -A `services` szekció a NEON formátumú konfigurációs fájlban az a hely, ahol saját szolgáltatásainkat és azok konfigurációját definiáljuk. Nézzünk egy egyszerű példát egy `database` nevű szolgáltatás definíciójára, amely egy `PDO` osztály példányát reprezentálja: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -A megadott konfiguráció a következő factory metódust eredményezi a [DI konténerben|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -A szolgáltatásnevek lehetővé teszik, hogy a konfigurációs fájl más részeiben hivatkozzunk rájuk, `@szolgaltatasNev` formátumban. Ha nincs szükség a szolgáltatás elnevezésére, egyszerűen használhatunk csak egy kötőjelet: - -```neon -services: - - PDO('sqlite::memory:') -``` - -A szolgáltatás lekéréséhez a DI konténerből használhatjuk a `getService()` metódust a szolgáltatás nevével paraméterként, vagy a `getByType()` metódust a szolgáltatás típusával: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Szolgáltatás létrehozása -======================== - -Legtöbbször egyszerűen úgy hozunk létre egy szolgáltatást, hogy létrehozunk egy példányt egy adott osztályból. Például: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Ha a konfigurációt további kulcsokkal kell bővítenünk, a definíciót több sorba is szétírhatjuk: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -A `create` kulcsnak van egy `factory` aliasa, mindkét változat gyakori a gyakorlatban. Azonban javasoljuk a `create` használatát. - -A konstruktor vagy a létrehozó metódus argumentumai alternatívaként az `arguments` kulcsban is megadhatók: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -A szolgáltatásokat nemcsak egyszerű osztálypéldányosítással lehet létrehozni, hanem statikus metódusok vagy más szolgáltatások metódusainak hívásának eredményeként is: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Vegyük észre, hogy az egyszerűség kedvéért `->` helyett `::` használatos, lásd [#kifejező eszközök]. Ezek a factory metódusok generálódnak: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -A DI konténernek ismernie kell a létrehozott szolgáltatás típusát. Ha egy olyan metódussal hozunk létre szolgáltatást, amelynek nincs megadva visszatérési típusa, akkor ezt a típust explicit módon meg kell adnunk a konfigurációban: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumentumok -============ - -A konstruktoroknak és metódusoknak argumentumokat adunk át, nagyon hasonlóan magához a PHP-hez: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -A jobb olvashatóság érdekében az argumentumokat külön sorokba írhatjuk. Ebben az esetben a vesszők használata opcionális: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Az argumentumokat el is nevezheti, és akkor nem kell törődnie a sorrendjükkel: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Ha ki szeretne hagyni néhány argumentumot, és azok alapértelmezett értékét szeretné használni, vagy egy szolgáltatást szeretne beilleszteni az [autowiring|autowiring] segítségével, használjon aláhúzást: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Argumentumként átadhatók szolgáltatások, használhatók paraméterek és még sok más, lásd [#kifejező eszközök]. - - -Setup -===== - -A `setup` szekcióban definiáljuk azokat a metódusokat, amelyeket a szolgáltatás létrehozásakor kell meghívni. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Ez PHP-ban így nézne ki: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -A metódushívásokon kívül értékeket is átadhatunk a property-knek. Támogatott az elem hozzáadása egy tömbhöz is, amelyet idézőjelek közé kell írni, hogy ne ütközzön a NEON szintaxisával: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Ami a PHP kódban a következőképpen nézne ki: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -A setupban azonban hívhatunk statikus metódusokat vagy más szolgáltatások metódusait is. Ha az aktuális szolgáltatást argumentumként kell átadni, adja meg `@self`-ként: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Vegyük észre, hogy az egyszerűség kedvéért `->` helyett `::` használatos, lásd [#kifejező eszközök]. Ilyen factory metódus generálódik: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Kifejező eszközök -================= - -A Nette DI rendkívül gazdag kifejező eszközöket ad nekünk, amelyekkel szinte bármit leírhatunk. A konfigurációs fájlokban így használhatunk [paramétereket |configuration#Paraméterek]: - -```neon -# paraméter -%wwwDir% - -# paraméter értéke kulcs alatt -%mailer.user% - -# paraméter egy stringen belül -'%wwwDir%/images' -``` - -Továbbá objektumokat hozhatunk létre, metódusokat és függvényeket hívhatunk: - -```neon -# objektum létrehozása -DateTime() - -# statikus metódus hívása -Collator::create(%locale%) - -# PHP függvény hívása -::getenv(DB_USER) -``` - -Hivatkozhatunk szolgáltatásokra akár a nevükkel, akár a típusukkal: - -```neon -# szolgáltatás név szerint -@database - -# szolgáltatás típus szerint -@Nette\Database\Connection -``` - -Használhatunk first-class callable szintaxist: .{data-version:3.2.0} - -```neon -# callback létrehozása, hasonlóan a [@user, logout]-hoz -@user::logout(...) -``` - -Használhatunk konstansokat: - -```neon -# osztály konstans -FilesystemIterator::SKIP_DOTS - -# globális konstansot a constant() PHP függvénnyel kapunk -::constant(PHP_VERSION) -``` - -A metódushívásokat ugyanúgy lehet láncolni, mint PHP-ban. Csak az egyszerűség kedvéért `->` helyett `::` használatos: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Ezeket a kifejezéseket bárhol használhatja, a [szolgáltatások létrehozásakor |#Szolgáltatás létrehozása], az [argumentumokban |#Argumentumok], a [#setup] szekcióban vagy a [paraméterekben |configuration#Paraméterek]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Speciális függvények --------------------- - -A konfigurációs fájlokban használhatja ezeket a speciális függvényeket: - -- `not()` érték negálása -- `bool()`, `int()`, `float()`, `string()` veszteségmentes típuskonverzió a megadott típusra -- `typed()` létrehozza a megadott típusú összes szolgáltatás tömbjét -- `tagged()` létrehozza a megadott taggel rendelkező összes szolgáltatás tömbjét - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -A klasszikus PHP típuskonverzióval ellentétben, mint pl. az `(int)`, a veszteségmentes típuskonverzió kivételt dob nem numerikus értékek esetén. - -A `typed()` függvény létrehozza a megadott típusú (osztály vagy interfész) összes szolgáltatás tömbjét. Kihagyja azokat a szolgáltatásokat, amelyeknek ki van kapcsolva az autowiringja. Több típust is meg lehet adni vesszővel elválasztva. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Egy adott típusú szolgáltatások tömbjét argumentumként is átadhatja automatikusan az [autowiring |autowiring#Szolgáltatások tömbje] segítségével. - -A `tagged()` függvény pedig létrehozza az összes, adott taggel rendelkező szolgáltatás tömbjét. Itt is megadhat több taget vesszővel elválasztva. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Az `autowired` kulcs lehetővé teszi az autowiring viselkedésének befolyásolását egy adott szolgáltatásra. Részletekért lásd az [autowiringról szóló fejezetet|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # a foo szolgáltatás ki van zárva az autowiringból -``` - - -Lazy szolgáltatások .{data-version:3.2.4} -========================================= - -A lazy loading egy technika, amely elhalasztja a szolgáltatás létrehozását egészen addig a pillanatig, amíg valóban szükség van rá. A globális konfigurációban [engedélyezhető a lazy létrehozás |configuration#Lazy szolgáltatások] minden szolgáltatásra egyszerre. Az egyes szolgáltatások esetében ezt a viselkedést felülbírálhatja: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Ha egy szolgáltatás lazy-ként van definiálva, annak a DI konténerből való lekérésekor egy speciális helyettesítő objektumot kapunk. Ez ugyanúgy néz ki és viselkedik, mint a valódi szolgáltatás, de a tényleges inicializálás (konstruktor és setup hívása) csak bármely metódusának vagy property-jének első hívásakor történik meg. - -.[note] -A lazy loading csak felhasználói osztályokra használható, belső PHP osztályokra nem. PHP 8.4 vagy újabb verziót igényel. - - -Tagek -===== - -A tagek további információk hozzáadására szolgálnak a szolgáltatásokhoz. Egy szolgáltatáshoz egy vagy több taget adhat hozzá: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -A tagek értékeket is hordozhatnak: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Ahhoz, hogy megkapja az összes, adott tagekkel rendelkező szolgáltatást, használhatja a `tagged()` függvényt: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -A DI konténerben lekérheti az összes, adott taggel rendelkező szolgáltatás nevét a `findByTag()` metódussal: - -```php -$names = $container->findByTag('logger'); -// $names egy tömb, amely tartalmazza a szolgáltatás nevét és a tag értékét -// pl. ['foo' => 'monolog.logger.event', ...] -``` - - -Inject mód -========== - -Az `inject: true` jelzővel aktiválódik a függőségek átadása a public property-ken keresztül [inject |best-practices:inject-method-attribute#Inject attribútumok] annotációval és az [inject*() |best-practices:inject-method-attribute#inject metódusok] metódusokkal. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Alapértelmezés szerint az `inject` csak a presenterekre van aktiválva. - - -Szolgáltatások módosítása -========================= - -A DI konténer számos szolgáltatást tartalmaz, amelyeket beépített vagy [felhasználói kiterjesztés|extensions] révén adtak hozzá. Módosíthatja ezeknek a szolgáltatásoknak a definícióit közvetlenül a konfigurációban. Például megváltoztathatja az `application.application` szolgáltatás osztályát, amely alapértelmezés szerint `Nette\Application\Application`, egy másikra: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Az `alteration` jelző informatív jellegű, és azt jelzi, hogy csak egy meglévő szolgáltatást módosítunk. - -Kiegészíthetjük a setupot is: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Egy szolgáltatás felülírásakor előfordulhat, hogy el akarjuk távolítani az eredeti argumentumokat, setup elemeket vagy tageket, erre szolgál a `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Ha el szeretne távolítani egy kiterjesztés által hozzáadott szolgáltatást, azt így teheti meg: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/pt/@home.texy b/dependency-injection/pt/@home.texy deleted file mode 100644 index 513481f0e3..0000000000 --- a/dependency-injection/pt/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -A Injeção de Dependência é um padrão de projeto que mudará fundamentalmente sua perspectiva sobre código e desenvolvimento. Abrirá o caminho para o mundo de aplicações bem projetadas e sustentáveis. - -- [O que é Injeção de Dependência? |introduction] -- [Estado global e singletons |global-state] -- [Passando dependências |passing-dependencies] -- [O que é um Contêiner DI? |container] -- [Perguntas frequentes|faq] - - -O pacote `nette/di` fornece um contêiner de DI compilado extremamente avançado para PHP. - -- [Contêiner Nette DI |nette-container] -- [Configuração |configuration] -- [Definindo serviços |services] -- [Autowiring |autowiring] -- [Fábricas geradas |factory] -- [Criando extensões para Nette DI|extensions] diff --git a/dependency-injection/pt/@left-menu.texy b/dependency-injection/pt/@left-menu.texy deleted file mode 100644 index 2b9273bd2a..0000000000 --- a/dependency-injection/pt/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Injeção de Dependência -********************** -- [O que é DI? |introduction] -- [Estado global e singletons |global-state] -- [Passando dependências |passing-dependencies] -- [O que é um Contêiner DI? |container] -- [Perguntas frequentes|faq] - - -Nette DI --------- -- [Contêiner Nette DI |nette-container] -- [Configuração |configuration] -- [Definindo serviços |services] -- [Autowiring |autowiring] -- [Fábricas geradas |factory] -- [Criando extensões para Nette DI|extensions] diff --git a/dependency-injection/pt/@meta.texy b/dependency-injection/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/dependency-injection/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/dependency-injection/pt/autowiring.texy b/dependency-injection/pt/autowiring.texy deleted file mode 100644 index bcf85fc932..0000000000 --- a/dependency-injection/pt/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring é um ótimo recurso que pode passar automaticamente os serviços necessários para o construtor e outros métodos, para que não precisemos escrevê-los. Isso economiza muito tempo. - -Graças a isso, podemos omitir a grande maioria dos argumentos ao escrever definições de serviço. Em vez de: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Basta escrever: - -```neon -services: - articles: Model\ArticleRepository -``` - -O Autowiring é orientado por tipos, então para funcionar, a classe `ArticleRepository` deve ser definida aproximadamente assim: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Para poder usar o autowiring, deve haver **exatamente um serviço** para cada tipo no contêiner. Se houver mais, o autowiring não saberá qual passar e lançará uma exceção: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # LANÇARÁ EXCEÇÃO, tanto mainDb quanto tempDb correspondem -``` - -A solução seria contornar o autowiring e especificar explicitamente o nome do serviço (ou seja, `articles: Model\ArticleRepository(@mainDb)`). Mas é mais inteligente [desativar |#Desativação do autowiring] o autowiring para um dos serviços, ou [dar preferência |#Preferência de autowiring] ao primeiro serviço. - - -Desativação do autowiring -------------------------- - -Podemos desativar o autowiring de um serviço usando a opção `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # o serviço tempDb é excluído do autowiring - - articles: Model\ArticleRepository # portanto, passa mainDb para o construtor -``` - -O serviço `articles` não lançará uma exceção dizendo que existem dois serviços do tipo `PDO` correspondentes (ou seja, `mainDb` e `tempDb`) que podem ser passados para o construtor, porque ele vê apenas o serviço `mainDb`. - -.[note] -A configuração do autowiring no Nette funciona de forma diferente do Symfony, onde a opção `autowire: false` diz que o autowiring não deve ser usado para os argumentos do construtor do serviço fornecido. No Nette, o autowiring é sempre usado, seja para argumentos do construtor ou para quaisquer outros métodos. A opção `autowired: false` diz que a instância do serviço fornecido não deve ser passada para lugar nenhum usando autowiring. - - -Preferência de autowiring -------------------------- - -Se tivermos vários serviços do mesmo tipo e especificarmos a opção `autowired` para um deles, esse serviço se torna o preferido: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # torna-se preferido - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -O serviço `articles` não lançará uma exceção dizendo que existem dois serviços do tipo `PDO` correspondentes (ou seja, `mainDb` e `tempDb`), mas usará o serviço preferido, ou seja, `mainDb`. - - -Array de serviços ------------------ - -O Autowiring também pode passar arrays de serviços de um determinado tipo. Como não é possível escrever nativamente o tipo dos itens do array em PHP, é necessário, além do tipo `array`, adicionar um comentário phpDoc com o tipo do item no formato `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -O contêiner DI então passa automaticamente um array de serviços correspondentes ao tipo fornecido. Ele omite serviços que têm o autowiring desativado. - -O tipo no comentário também pode estar no formato `array<int, Class>` ou `list<Class>`. Se você não pode influenciar a forma do comentário phpDoc, pode passar o array de serviços diretamente na configuração usando [`typed()` |services#Funções especiais]. - - -Argumentos escalares --------------------- - -O Autowiring só pode injetar objetos e arrays de objetos. Argumentos escalares (por exemplo, strings, números, booleanos) [são escritos na configuração |services#Argumentos]. Uma alternativa é criar um [objeto de configurações |best-practices:passing-settings-to-presenters], que encapsula o valor escalar (ou múltiplos valores) em um objeto, que pode então ser passado novamente usando autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly pode ser usado a partir do PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Você cria um serviço a partir dele adicionando-o à configuração: - -```neon -services: - - MySettings('any value') -``` - -Todas as classes então o solicitarão usando autowiring. - - -Restringindo o autowiring -------------------------- - -Para serviços individuais, o autowiring pode ser restrito a certas classes ou interfaces. - -Normalmente, o autowiring passa o serviço para cada parâmetro de método cujo tipo o serviço corresponde. Restringir significa que estabelecemos condições que os tipos especificados nos parâmetros do método devem satisfazer para que o serviço seja passado para eles. - -Vamos ilustrar com um exemplo: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Se registrássemos todos eles como serviços, o autowiring falharia: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # LANÇARÁ EXCEÇÃO, os serviços parent e child correspondem - childDep: ChildDependent # autowiring passa o serviço child para o construtor -``` - -O serviço `parentDep` lançará a exceção `Multiple services of type ParentClass found: parent, child`, porque ambos os serviços `parent` e `child` se encaixam em seu construtor, e o autowiring não pode decidir qual escolher. - -Para o serviço `child`, podemos, portanto, restringir seu autowiring ao tipo `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # também pode escrever 'autowired: self' - - parentDep: ParentDependent # autowiring passa o serviço parent para o construtor - childDep: ChildDependent # autowiring passa o serviço child para o construtor -``` - -Agora, o serviço `parent` é passado para o construtor do serviço `parentDep`, porque agora é o único objeto correspondente. O autowiring não passa mais o serviço `child` para lá. Sim, o serviço `child` ainda é do tipo `ParentClass`, mas a condição restritiva dada para o tipo do parâmetro não é mais válida, ou seja, não é verdade que `ParentClass` *é um supertipo de* `ChildClass`. - -Para o serviço `child`, `autowired: ChildClass` também poderia ser escrito como `autowired: self`, já que `self` é um placeholder para a classe do serviço atual. - -Na chave `autowired`, também é possível especificar várias classes ou interfaces como um array: - -```neon -autowired: [BarClass, FooInterface] -``` - -Vamos tentar complementar o exemplo com interfaces: - -```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) - {} -} -``` - -Se não restringirmos o serviço `child` de forma alguma, ele se encaixará nos construtores de todas as classes `FooDependent`, `BarDependent`, `ParentDependent` e `ChildDependent`, e o autowiring o passará para lá. - -No entanto, se restringirmos seu autowiring a `ChildClass` usando `autowired: ChildClass` (ou `self`), o autowiring o passará apenas para o construtor de `ChildDependent`, porque ele requer um argumento do tipo `ChildClass` e é verdade que `ChildClass` *é do tipo* `ChildClass`. Nenhum outro tipo especificado nos outros parâmetros é um supertipo de `ChildClass`, então o serviço não é passado. - -Se o restringirmos a `ParentClass` usando `autowired: ParentClass`, ele será novamente passado para o construtor de `ChildDependent` (porque o `ChildClass` exigido é um supertipo de `ParentClass`) e, agora também para o construtor de `ParentDependent`, porque o tipo `ParentClass` exigido também é adequado. - -Se o restringirmos a `FooInterface`, ele ainda será autowired para `ParentDependent` (o `ParentClass` exigido é um supertipo de `FooInterface`) e `ChildDependent`, mas adicionalmente também para o construtor de `FooDependent`, mas não para `BarDependent`, porque `BarInterface` não é um supertipo de `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring passa child para o construtor - barDep: BarDependent # LANÇARÁ EXCEÇÃO, nenhum serviço corresponde - parentDep: ParentDependent # autowiring passa child para o construtor - childDep: ChildDependent # autowiring passa child para o construtor -``` diff --git a/dependency-injection/pt/configuration.texy b/dependency-injection/pt/configuration.texy deleted file mode 100644 index e22c943046..0000000000 --- a/dependency-injection/pt/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Configuração do Contêiner DI -**************************** - -.[perex] -Visão geral das opções de configuração para o contêiner Nette DI. - - -Arquivo de Configuração -======================= - -O contêiner Nette DI é facilmente controlado por meio de arquivos de configuração. Eles geralmente são escritos no [formato NEON|neon:format]. Para edição, recomendamos [editores com suporte |best-practices:editors-and-tools#Editor IDE] para este formato. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Decorador .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "Contêiner DI .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensões]: "Instalação de extensões DI adicionais .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Inclusão de arquivos]: "Inclusão de arquivos .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parâmetros]: "Parâmetros .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Registro automático de serviços .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Serviços .[prism-token prism-comment]" -</pre> - -.[note] -Para escrever uma string contendo o caractere `%`, você deve escapá-lo duplicando-o para `%%`. - - -Parâmetros -========== - -Na configuração, você pode definir parâmetros que podem ser usados como parte das definições de serviço. Isso pode tornar a configuração mais clara ou unificar e extrair valores que serão alterados. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Referimo-nos ao parâmetro `dsn` em qualquer lugar na configuração escrevendo `%dsn%`. Os parâmetros também podem ser usados dentro de strings como `'%wwwDir%/images'`. - -Os parâmetros não precisam ser apenas strings ou números, eles também podem conter arrays: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Referimo-nos a uma chave específica como `%mailer.user%`. - -Se você precisar descobrir o valor de qualquer parâmetro em seu código, por exemplo, em uma classe, passe-o para essa classe. Por exemplo, no construtor. Não existe um objeto global representando a configuração que as classes consultariam para obter valores de parâmetros. Isso violaria o princípio da injeção de dependência. - - -Serviços -======== - -Veja [capítulo separado|services]. - - -Decorator -========= - -Como modificar em massa todos os serviços de um determinado tipo? Por exemplo, chamar um determinado método em todos os presenters que herdam de um ancestral comum específico? É para isso que serve o decorator. - -```neon -decorator: - # para todos os serviços que são instâncias desta classe ou interface - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # chame este método - - $absoluteUrls = true # e defina a variável -``` - -O decorator também pode ser usado para definir [tags |services#Tags] ou ativar o modo [inject |services#Modo Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Configurações técnicas do contêiner DI. - -```neon -di: - # exibir DIC na Tracy Bar? - debugger: ... # (bool) padrão é true - - # tipos de parâmetros que nunca devem ser autowired - excluded: ... # (string[]) - - # permitir criação lazy de serviços? - lazy: ... # (bool) padrão é false - - # classe da qual o contêiner DI herda - parentClass: ... # (string) padrão é Nette\DI\Container -``` - - -Serviços Lazy .{data-version:3.2.4} ------------------------------------ - -A configuração `lazy: true` ativa a criação lazy (adiada) de serviços. Isso significa que os serviços não são realmente criados no momento em que os solicitamos do contêiner DI, mas apenas no momento de seu primeiro uso. Isso pode acelerar o início da aplicação e reduzir o consumo de memória, pois apenas os serviços que são realmente necessários na requisição atual são criados. - -Para um serviço específico, a criação lazy pode ser [alterada |services#Serviços Lazy]. - -.[note] -Objetos lazy só podem ser usados para classes de usuário, não para classes internas do PHP. Requer PHP 8.4 ou posterior. - - -Exportação de metadados ------------------------ - -A classe do contêiner DI também contém muitos metadados. Você pode reduzi-la reduzindo a exportação de metadados. - -```neon -di: - export: - # exportar parâmetros? - parameters: false # (bool) padrão é true - - # exportar tags e quais? - tags: # (string[]|bool) padrão são todas - - event.subscriber - - # exportar dados para autowiring e quais? - types: # (string[]|bool) padrão são todas - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Se você não usa o array `$container->getParameters()`, pode desativar a exportação de parâmetros. Além disso, você pode exportar apenas as tags pelas quais obtém serviços usando o método `$container->findByTag(...)`. Se você não chamar o método, pode desativar completamente a exportação de tags usando `false`. - -Você pode reduzir significativamente os metadados para [autowiring] especificando as classes que você usa como parâmetro do método `$container->getByType()`. E novamente, se você não chamar o método (respectivamente, apenas no [bootstrap|application:bootstrapping] para obter `Nette\Application\Application`), pode desativar completamente a exportação usando `false`. - - -Extensões -========= - -Registro de extensões DI adicionais. Desta forma, adicionamos, por exemplo, a extensão DI `Dibi\Bridges\Nette\DibiExtension22` sob o nome `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Posteriormente, a configuramos na seção `dibi`: - -```neon -dibi: - host: localhost -``` - -Também é possível adicionar uma classe que tem parâmetros como extensão: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Inclusão de arquivos -==================== - -Podemos incluir outros arquivos de configuração na seção `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -O nome `parameters.php` não é um erro de digitação, a configuração também pode ser escrita em um arquivo PHP, que a retorna como um array: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Se elementos com as mesmas chaves aparecerem em vários arquivos de configuração, eles serão sobrescritos ou, no caso de [arrays, mesclados |#Mesclagem]. O arquivo incluído posteriormente tem prioridade maior que o anterior. O arquivo no qual a seção `includes` está listada tem prioridade maior que os arquivos incluídos nele. - - -Search -====== - -A adição automática de serviços ao contêiner DI torna o trabalho extremamente agradável. Nette adiciona automaticamente presenters ao contêiner, mas também é fácil adicionar quaisquer outras classes. - -Basta especificar em quais diretórios (e subdiretórios) as classes devem ser procuradas: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -No entanto, geralmente não queremos adicionar absolutamente todas as classes e interfaces, por isso podemos filtrá-las: - -```neon -search: - - in: %appDir%/Forms - - # filtragem por nome de arquivo (string|string[]) - files: - - *Factory.php - - # filtragem por nome de classe (string|string[]) - classes: - - *Factory -``` - -Ou podemos selecionar classes que herdam ou implementam pelo menos uma das classes listadas: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Também é possível definir regras de exclusão, ou seja, máscaras de nome de classe ou ancestrais herdados, que, se corresponderem, o serviço não será adicionado ao contêiner DI: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Tags podem ser definidas para todos os serviços: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Mesclagem -========= - -Se elementos com as mesmas chaves aparecerem em vários arquivos de configuração, eles serão sobrescritos ou, no caso de arrays, mesclados. O arquivo incluído posteriormente tem prioridade maior que o anterior. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>resultado</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Para arrays, a mesclagem pode ser evitada adicionando um ponto de exclamação após o nome da chave: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>resultado</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Configuração de Injeção de Dependência}} diff --git a/dependency-injection/pt/container.texy b/dependency-injection/pt/container.texy deleted file mode 100644 index fff5f871d2..0000000000 --- a/dependency-injection/pt/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -O que é um Contêiner DI? -************************ - -.[perex] -Um contêiner de injeção de dependência (DIC) é uma classe que pode instanciar e configurar objetos. - -Pode surpreendê-lo, mas em muitos casos, você não precisa de um contêiner de injeção de dependência para aproveitar os benefícios da injeção de dependência (DI para abreviar). Afinal, mesmo no [capítulo introdutório|introduction], mostramos DI com exemplos concretos e nenhum contêiner foi necessário. - -No entanto, se você precisar gerenciar um grande número de objetos diferentes com muitas dependências, um contêiner de injeção de dependência será realmente útil. É o caso, por exemplo, de aplicações web construídas sobre um framework. - -No capítulo anterior, apresentamos as classes `Article` e `UserController`. Ambas têm algumas dependências, nomeadamente o banco de dados e a fábrica `ArticleFactory`. E para essas classes, agora criaremos um contêiner. Claro, para um exemplo tão simples, não faz sentido ter um contêiner. Mas vamos criá-lo para mostrar como ele se parece e funciona. - -Aqui está um contêiner simples hardcoded para o exemplo dado: - -```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()); - } -} -``` - -O uso seria o seguinte: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Apenas pedimos ao contêiner o objeto e não precisamos mais saber nada sobre como criá-lo ou quais são suas dependências; o contêiner sabe tudo isso. As dependências são injetadas automaticamente pelo contêiner. Essa é a sua força. - -Por enquanto, o contêiner tem todos os dados codificados. Daremos o próximo passo e adicionaremos parâmetros para tornar o contêiner realmente útil: - -```php -class Container -{ - public function __construct( - private array $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' => '***', -]); -``` - -Leitores atentos podem ter notado um certo problema. Toda vez que obtenho um objeto `UserController`, uma nova instância de `ArticleFactory` e do banco de dados também é criada. Definitivamente não queremos isso. - -Portanto, adicionaremos um método `getService()` que sempre retornará as mesmas instâncias: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') chamará createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Na primeira chamada, por exemplo, `$container->getService('Database')`, ele fará com que `createDatabase()` crie o objeto do banco de dados, que ele armazena no array `$services`, e na próxima chamada, ele o retorna diretamente. - -Também modificaremos o restante do contêiner para usar `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -A propósito, o termo serviço refere-se a qualquer objeto gerenciado pelo contêiner. É por isso que o método se chama `getService()`. - -Feito. Temos um contêiner DI totalmente funcional! E podemos usá-lo: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Como você pode ver, escrever um DIC não é complicado. Vale a pena notar que os próprios objetos não sabem que estão sendo criados por algum contêiner. Assim, é possível criar qualquer objeto em PHP dessa forma sem interferir em seu código-fonte. - -Criar e manter manualmente a classe do contêiner pode se tornar rapidamente um pesadelo. Portanto, no próximo capítulo, falaremos sobre o [Nette DI Container|nette-container], que pode se gerar e atualizar quase sozinho. - - -{{maintitle: O que é um contêiner de injeção de dependência?}} diff --git a/dependency-injection/pt/extensions.texy b/dependency-injection/pt/extensions.texy deleted file mode 100644 index a0fa7b614e..0000000000 --- a/dependency-injection/pt/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Criação de extensões para Nette DI -********************************** - -.[perex] -A geração do contêiner DI, além dos arquivos de configuração, também é influenciada pelas chamadas *extensões*. Nós as ativamos no arquivo de configuração na seção `extensions`. - -Desta forma, adicionamos a extensão representada pela classe `BlogExtension` sob o nome `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Cada extensão do compilador herda de [api:Nette\DI\CompilerExtension] e pode implementar os seguintes métodos, que são chamados sequencialmente durante a construção do contêiner DI: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Este método é chamado primeiro. Ele define o schema para validação dos parâmetros de configuração. - -Configuramos a extensão na seção cujo nome é o mesmo sob o qual a extensão foi adicionada, ou seja, `blog`: - -```neon -# mesmo nome da extensão -blog: - postsPerPage: 10 - allowComments: false -``` - -Criamos um schema descrevendo todas as opções de configuração, incluindo seus tipos, valores permitidos e, opcionalmente, valores padrão: - -```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), - ]); - } -} -``` - -A documentação pode ser encontrada na página [Schema |schema:]. Além disso, pode-se especificar quais opções podem ser [dinâmicas |application:bootstrapping#Parâmetros dinâmicos] usando `dynamic()`, např. `Expect::int()->dynamic()`. - -Acessamos a configuração através da variável `$this->config`, que é um objeto `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postsPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Usado para adicionar serviços ao contêiner. Para isso, serve a [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']) // ou setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -A convenção é prefixar os serviços adicionados pela extensão com seu nome, para que não ocorram conflitos de nomes. O método `prefix()` faz isso, então se a extensão se chama `blog`, o serviço será nomeado `blog.articles`. - -Se precisarmos renomear um serviço, podemos, para manter a compatibilidade retroativa, criar um alias com o nome original. A Nette faz algo semelhante, por exemplo, com o serviço `routing.router`, que também está disponível sob o nome anterior `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Carregamento de serviços de um arquivo --------------------------------------- - -Não precisamos criar serviços apenas usando a API da classe ContainerBuilder, mas também com a notação familiar usada no arquivo de configuração NEON na seção services. O prefixo `@extension` representa a extensão atual. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Carregamos os serviços: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // carregamento do arquivo de configuração para a extensão - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -O método é chamado no momento em que o contêiner contém todos os serviços adicionados pelas extensões individuais nos métodos `loadConfiguration` e também pelos arquivos de configuração do usuário. Nesta fase de construção, podemos, portanto, modificar as definições de serviço ou adicionar ligações entre eles. Para pesquisar serviços no contêiner por tags, pode-se usar o método `findByTag()`, por classe ou interface, o método `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] -======================== - -Nesta fase, a classe do contêiner já está gerada na forma de um objeto [ClassType |php-generator:#Classes], contém todos os métodos que criam serviços e está pronta para ser escrita no cache. O código resultante da classe ainda pode ser modificado neste momento. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -A classe Configurator, após [criar o contêiner |application:bootstrapping#index.php], chama o código de inicialização, que é criado escrevendo no objeto `$this->initialization` usando o [método addBody() |php-generator:#Corpos de métodos e funções]. - -Mostraremos um exemplo de como, por exemplo, iniciar a sessão com o código de inicialização ou iniciar serviços que têm a tag `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // início automático da sessão - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // serviços com a tag run devem ser criados após a instanciação do contêiner - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/pt/factory.texy b/dependency-injection/pt/factory.texy deleted file mode 100644 index 7d64f484c7..0000000000 --- a/dependency-injection/pt/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Fábricas Geradas -**************** - -.[perex] -A Nette DI pode gerar automaticamente código de fábricas com base em interfaces, o que economiza a escrita de código. - -Uma fábrica é uma classe que produz e configura objetos. Portanto, ela também passa suas dependências para eles. Por favor, não confunda com o padrão de projeto *factory method*, que descreve uma maneira específica de usar fábricas e não está relacionado a este tópico. - -Mostramos como é uma fábrica no [capítulo introdutório |introduction#Fábrica]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -A Nette DI pode gerar automaticamente o código das fábricas. Tudo o que você precisa fazer é criar uma interface e a Nette DI gerará a implementação. A interface deve ter exatamente um método chamado `create` e declarar o tipo de retorno: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ou seja, a fábrica `ArticleFactory` tem um método `create` que cria objetos `Article`. A classe `Article` pode se parecer com o seguinte: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Adicionamos a fábrica ao arquivo de configuração: - -```neon -services: - - ArticleFactory -``` - -A Nette DI gerará a implementação correspondente da fábrica. - -No código que usa a fábrica, solicitamos o objeto pela interface e a Nette DI usará a implementação gerada: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // deixamos a fábrica criar o objeto - $article = $this->articleFactory->create(); - } -} -``` - - -Fábrica Parametrizada -===================== - -O método da fábrica `create` pode aceitar parâmetros, que são então passados para o construtor. Vamos adicionar, por exemplo, o ID do autor do artigo à classe `Article`: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Também adicionamos o parâmetro à fábrica: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Graças ao fato de que o parâmetro no construtor e o parâmetro na fábrica têm o mesmo nome, a Nette DI os passa de forma totalmente automática. - - -Definição Avançada -================== - -A definição também pode ser escrita em formato de múltiplas linhas usando a chave `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Ao escrever desta forma mais longa, é possível especificar argumentos adicionais para o construtor na chave `arguments` e configuração adicional usando `setup`, assim como nos serviços comuns. - -Exemplo: se o método `create()` não aceitasse o parâmetro `$authorId`, poderíamos especificar um valor fixo na configuração, que seria passado para o construtor de `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Ou, inversamente, se `create()` aceitasse o parâmetro `$authorId`, mas ele não fizesse parte do construtor e fosse passado pelo método `Article::setAuthorId()`, faríamos referência a ele na seção `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Além das fábricas, a Nette também pode gerar os chamados accessors. São objetos com um método `get()`, que retorna um determinado serviço do contêiner DI. Chamadas repetidas de `get()` retornam sempre a mesma instância. - -Os accessors fornecem carregamento preguiçoso (lazy-loading) para dependências. Considere uma classe que registra erros em um banco de dados especial. Se essa classe recebesse a conexão com o banco de dados como dependência via construtor, a conexão sempre teria que ser criada, embora na prática um erro ocorra apenas excepcionalmente e, portanto, na maioria das vezes a conexão permaneceria inutilizada. Em vez disso, a classe recebe um accessor e somente quando seu `get()` é chamado, o objeto do banco de dados é criado: - -Como criar um accessor? Basta escrever uma interface e a Nette DI gerará a implementação. A interface deve ter exatamente um método chamado `get` e declarar o tipo de retorno: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Adicionamos o accessor ao arquivo de configuração, onde também está a definição do serviço que ele retornará: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Como o accessor retorna um serviço do tipo `PDO` e há apenas um serviço desse tipo na configuração, ele retornará exatamente esse. Se houvesse mais serviços do tipo fornecido, especificaríamos o serviço retornado usando o nome, por exemplo, `- PDOAccessor(@db1)`. - - -Fábrica/Accessor Múltiplo -========================= -Nossas fábricas e accessors até agora só podiam produzir ou retornar um objeto. No entanto, é muito fácil criar também fábricas múltiplas combinadas сom accessors. A interface de tal classe conterá qualquer número de métodos com os nomes `create<name>()` e `get<name>()`, por exemplo: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Então, em vez de passar várias fábricas e accessors gerados, passamos uma fábrica mais complexa que pode fazer mais. - -Alternativamente, em vez de vários métodos, pode-se usar `get()` сom um parâmetro: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Então, vale que `MultiFactory::createArticle()` faz o mesmo que `MultiFactoryAlt::get('article')`. No entanto, a notação alternativa tem a desvantagem de não ficar claro quais valores de `$name` são suportados e, logicamente, também não é possível na interface distinguir diferentes valores de retorno para diferentes `$name`. - - -Definição por lista -------------------- -Desta forma, é possível definir uma fábrica múltipla na configuração: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # define createArticle() - db: PDO(%dsn%, %user%, %password%) # define getDb() - ) -``` - -Ou podemos, na definição da fábrica, referir-nos a serviços existentes usando uma referência: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # define createArticle() - db: @\PDO # define getDb() - ) -``` - - -Definição usando tags ---------------------- - -A segunda opção é usar [tags |services#Tags] para a definição: - -```neon -services: - - App\Core\RouterFactory::createRouter # Assumindo que isso é um serviço ou factory - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer # Assumindo que existe um serviço com este nome - ) -``` diff --git a/dependency-injection/pt/faq.texy b/dependency-injection/pt/faq.texy deleted file mode 100644 index d93dd8c94a..0000000000 --- a/dependency-injection/pt/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Perguntas Frequentes sobre DI (FAQ) -*********************************** - - -DI é outro nome para IoC? -------------------------- - -*Inversion of Control* (IoC) é um princípio focado na maneira como o código é executado - se o seu código executa código de terceiros ou se o seu código é integrado a código de terceiros que o chama posteriormente. IoC é um termo amplo que inclui [eventos |nette:glossary#Eventos], o chamado [Princípio de Hollywood |application:components#Estilo Hollywood] e outros aspectos. Parte deste conceito também são as fábricas, sobre as quais fala a [Regra nº 3: deixe para a fábrica |introduction#Regra nº 3: deixe para a fábrica], e que representam uma inversão para o operador `new`. - -*Dependency Injection* (DI) foca na maneira como um objeto aprende sobre outro objeto, ou seja, sobre suas dependências. É um padrão de projeto que exige a passagem explícita de dependências entre objetos. - -Pode-se dizer, portanto, que DI é uma forma específica de IoC. No entanto, nem todas as formas de IoC são adequadas do ponto de vista da pureza do código. Por exemplo, entre os antipadrões estão técnicas que trabalham com [estado global |global-state] ou o chamado [Service Locator |#O que é Service Locator]. - - -O que é Service Locator? ------------------------- - -É uma alternativa à Injeção de Dependência. Funciona criando um repositório central onde todos os serviços ou dependências disponíveis são registrados. Quando um objeto precisa de uma dependência, ele a solicita ao Service Locator. - -No entanto, em comparação com a Injeção de Dependência, perde em transparência: as dependências não são passadas diretamente aos objetos e não são tão facilmente identificáveis, o que exige examinar o código para revelar e entender todas as ligações. O teste também é mais complicado, pois não podemos simplesmente passar objetos mock para os objetos testados, mas temos que passar pelo Service Locator. Além disso, o Service Locator perturba o design do código, pois objetos individuais precisam saber de sua existência, o que difere da Injeção de Dependência, onde os objetos não têm conhecimento do contêiner DI. - - -Quando é melhor não usar DI? ----------------------------- - -Não são conhecidas dificuldades associadas ao uso do padrão de projeto Injeção de Dependência. Pelo contrário, obter dependências de locais globalmente disponíveis leva a [uma série de complicações |global-state], assim como o uso do Service Locator. Portanto, é aconselhável usar DI sempre. Isso não é uma abordagem dogmática, mas simplesmente não foi encontrada uma alternativa melhor. - -No entanto, existem certas situações em que não passamos objetos e os obtemos do espaço global. Por exemplo, ao depurar código, quando você precisa imprimir o valor de uma variável em um ponto específico do programa, medir a duração de uma determinada parte do programa ou registrar uma mensagem. Nesses casos, quando se trata de tarefas temporárias que serão posteriormente removidas do código, é legítimo usar um dumper, cronômetro ou logger globalmente disponível. Essas ferramentas não pertencem ao design do código. - - -O uso de DI tem desvantagens? ------------------------------ - -O uso da Injeção de Dependência traz alguma desvantagem, como aumento da complexidade na escrita do código ou piora no desempenho? O que perdemos quando começamos a escrever código de acordo com DI? - -DI não tem impacto no desempenho ou nos requisitos de memória da aplicação. O desempenho do Contêiner DI pode desempenhar algum papel, mas no caso do [Nette DI |nette-container], o contêiner é compilado em PHP puro, então sua sobrecarga durante a execução da aplicação é essencialmente zero. - -Ao escrever código, geralmente é necessário criar construtores que aceitam dependências. Antigamente, isso podia ser demorado, mas graças aos IDEs modernos e à [promoção de propriedades do construtor |https://blog.nette.org/pt/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], agora é uma questão de segundos. As fábricas podem ser facilmente geradas usando Nette DI e o plugin para PhpStorm com um clique do mouse. Por outro lado, elimina-se a necessidade de escrever singletons e pontos de acesso estáticos. - -Pode-se afirmar que uma aplicação corretamente projetada usando DI não é nem mais curta nem mais longa em comparação com uma aplicação usando singletons. As partes do código que trabalham com dependências são apenas extraídas das classes individuais e movidas para novos locais, ou seja, para o contêiner DI e fábricas. - - -Como reescrever uma aplicação legada para DI? ---------------------------------------------- - -A transição de uma aplicação legada para Injeção de Dependência pode ser um processo desafiador, especialmente para aplicações grandes e complexas. É importante abordar este processo sistematicamente. - -- Ao fazer a transição para Injeção de Dependência, é importante que todos os membros da equipe entendam os princípios e procedimentos que estão sendo usados. -- Primeiro, realize uma análise da aplicação existente e identifique os componentes chave e suas dependências. Crie um plano de quais partes serão refatoradas e em que ordem. -- Implemente um contêiner DI ou, melhor ainda, use uma biblioteca existente, como Nette DI. -- Refatore gradualmente partes individuais da aplicação para usar Injeção de Dependência. Isso pode incluir a modificação de construtores ou métodos para aceitar dependências como parâmetros. -- Modifique os locais no código onde objetos com dependências são criados para que, em vez disso, as dependências sejam injetadas pelo contêiner. Isso pode incluir o uso de fábricas. - -Lembre-se que a transição para Injeção de Dependência é um investimento na qualidade do código e na sustentabilidade a longo prazo da aplicação. Embora possa ser desafiador fazer essas mudanças, o resultado deve ser um código mais limpo, modular e facilmente testável, pronto para futuras extensões e manutenção. - - -Por que a composição é preferida em relação à herança? ------------------------------------------------------- -É preferível usar [composição |nette:introduction-to-object-oriented-programming#Composição] em vez de [herança |nette:introduction-to-object-oriented-programming#Herança], porque ela serve para reutilizar código sem ter que nos preocupar com as consequências das mudanças. Ela fornece, portanto, um acoplamento mais fraco, onde não precisamos nos preocupar que a mudança em algum código cause a necessidade de mudar outro código dependente. Um exemplo típico é a situação conhecida como [inferno de construtores |passing-dependencies#Constructor hell]. - - -É possível usar o Nette DI Container fora do Nette? ---------------------------------------------------- - -Com certeza. O Nette DI Container faz parte do Nette, mas foi projetado como uma biblioteca independente que pode ser usada independentemente de outras partes do framework. Basta instalá-lo usando o Composer, criar um arquivo de configuração com a definição de seus serviços e, em seguida, usar algumas linhas de código PHP para criar o contêiner DI. E você pode começar imediatamente a aproveitar os benefícios da Injeção de Dependência em seus projetos. - -O uso específico, incluindo códigos, é descrito no capítulo [Nette DI Container |nette-container]. - - -Por que a configuração está em arquivos NEON? ---------------------------------------------- - -NEON é uma linguagem de configuração simples e fácil de ler, desenvolvida no Nette para configurar aplicações, serviços e suas dependências. Em comparação com JSON ou YAML, oferece opções muito mais intuitivas e flexíveis para este propósito. Em NEON, é possível descrever naturalmente ligações que em Symfony & YAMLu não seria possível escrever, ou apenas por meio de uma descrição complexa. - - -A análise de arquivos NEON não torna a aplicação mais lenta? ------------------------------------------------------------- - -Embora os arquivos NEON sejam analisados muito rapidamente, este aspecto não importa. A razão é que a análise dos arquivos ocorre apenas uma vez na primeira execução da aplicação. Depois disso, o código do contêiner DI é gerado, salvo em disco e executado em cada requisição subsequente, sem a necessidade de realizar análises adicionais. - -É assim que funciona em um ambiente de produção. Durante o desenvolvimento, os arquivos NEON são analisados toda vez que seu conteúdo é alterado, para que o desenvolvedor sempre tenha um contêiner DI atualizado. A análise em si é, como mencionado, uma questão de momento. - - -Como acesso os parâmetros do arquivo de configuração a partir da minha classe? ------------------------------------------------------------------------------- - -Lembre-se da [Regra nº 1: peça para receber |introduction#Regra nº 1: peça para ser passado]. Se uma classe requer informações do arquivo de configuração, não precisamos pensar em como obter essas informações, em vez disso, simplesmente as solicitamos - por exemplo, através do construtor da classe. E realizamos a passagem no arquivo de configuração. - -Neste exemplo, `%myParameter%` é um placeholder para o valor do parâmetro `myParameter`, que é passado para o construtor da classe `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Se você deseja passar vários parâmetros ou usar autowiring, é aconselhável [envolver os parâmetros em um objeto |best-practices:passing-settings-to-presenters]. - - -Nette suporta a interface PSR-11: Container? --------------------------------------------- - -O Nette DI Container não suporta PSR-11 diretamente. No entanto, se você precisar de interoperabilidade entre o Nette DI Container e bibliotecas ou frameworks que esperam a Interface de Contêiner PSR-11, você pode criar um [adaptador simples |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f] que servirá como uma ponte entre o Nette DI Container e o PSR-11. diff --git a/dependency-injection/pt/global-state.texy b/dependency-injection/pt/global-state.texy deleted file mode 100644 index 65375c7661..0000000000 --- a/dependency-injection/pt/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Estado Global e Singletons -************************** - -.[perex] -Aviso: As seguintes construções são um sinal de código mal projetado: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` ou `static::$var` - -Alguma dessas construções ocorre em seu código? Então você tem a oportunidade de melhorá-lo. Você pode pensar que são construções comuns que você vê até mesmo em soluções de exemplo de várias bibliotecas e frameworks. Se for esse o caso, então o design do código deles não é bom. - -Agora, definitivamente não estamos falando de alguma pureza acadêmica. Todas essas construções têm uma coisa em comum: elas usam estado global. E isso tem um impacto destrutivo na qualidade do código. As classes mentem sobre suas dependências. O código se torna imprevisível. Confunde os programadores e reduz sua eficiência. - -Neste capítulo, explicaremos por que isso acontece e como evitar o estado global. - - -Acoplamento Global ------------------- - -Em um mundo ideal, um objeto só deveria ser capaz de se comunicar com objetos que lhe foram [passados diretamente |passing-dependencies]. Se eu criar dois objetos `A` e `B` e nunca passar uma referência entre eles, então nem `A` nem `B` podem acessar o outro objeto ou alterar seu estado. Esta é uma propriedade muito desejável do código. É semelhante a ter uma bateria e uma lâmpada; a lâmpada não acenderá até que você a conecte à bateria com um fio. - -Mas isso não se aplica a variáveis globais (estáticas) ou singletons. O objeto `A` poderia acessar *sem fio* o objeto `C` e modificá-lo sem qualquer passagem de referência, chamando `C::changeSomething()`. Se o objeto `B` também pegar o `C` global, então `A` e `B` podem se influenciar mutuamente através de `C`. - -O uso de variáveis globais introduz no sistema uma nova forma de acoplamento *sem fio*, que não é visível de fora. Cria uma cortina de fumaça complicando a compreensão e o uso do código. Para que os desenvolvedores realmente entendam as dependências, eles precisam ler cada linha do código-fonte. Em vez de apenas se familiarizarem com as interfaces das classes. Além disso, é um acoplamento completamente desnecessário. O estado global é usado porque é facilmente acessível de qualquer lugar e permite, por exemplo, escrever no banco de dados através do método global (estático) `DB::insert()`. Mas, como mostraremos, a vantagem que isso traz é insignificante, enquanto as complicações que causa são fatais. - -.[note] -Do ponto de vista do comportamento, não há diferença entre uma variável global e estática. Elas são igualmente prejudiciais. - - -Ação fantasmagórica à distância -------------------------------- - -"Ação fantasmagórica à distância" - foi assim que Albert Einstein famosamente chamou, em 1935, um fenômeno na física quântica que lhe causava arrepios. -Trata-se do emaranhamento quântico, cuja peculiaridade é que, quando você mede a informação sobre uma partícula, influencia instantaneamente a outra partícula, mesmo que estejam a milhões de anos-luz de distância. Isso aparentemente viola a lei fundamental do universo de que nada pode se propagar mais rápido que a luz. - -No mundo do software, podemos chamar de "ação fantasmagórica à distância" a situação em que iniciamos um processo que acreditamos ser isolado (porque não passamos nenhuma referência a ele), mas em locais remotos do sistema ocorrem interações inesperadas e mudanças de estado das quais não tínhamos conhecimento. Isso só pode acontecer através do estado global. - -Imagine que você se junta a uma equipe de desenvolvedores de um projeto que tem uma base de código extensa e madura. Seu novo líder pede que você implemente uma nova funcionalidade e você, como um bom desenvolvedor, começa escrevendo um teste. Mas como você é novo no projeto, faz muitos testes exploratórios do tipo "o que acontece se eu chamar este método". E tenta escrever o seguinte teste: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // número do seu cartão - $cc->charge(100); -} -``` - -Você executa o código, talvez várias vezes, e depois de um tempo percebe notificações do banco no seu celular informando que a cada execução foram debitados 100 dólares do seu cartão de crédito 🤦‍♂️ - -Como diabos o teste pôde causar um débito real de dinheiro? Operar com um cartão de crédito não é fácil. Você precisa se comunicar com um serviço web de terceiros, precisa saber a URL desse serviço web, precisa fazer login e assim por diante. Nenhuma dessas informações está contida no teste. Pior ainda, você nem sabe onde essas informações estão presentes e, portanto, nem como mockar as dependências externas para que cada execução não leve a um novo débito de 100 dólares. E como você, como novo desenvolvedor, deveria saber que o que estava prestes a fazer resultaria em ficar 100 dólares mais pobre? - -Isso é ação fantasmagórica à distância! - -Você não tem escolha a não ser vasculhar longamente um monte de código-fonte, perguntar aos colegas mais velhos e experientes, até entender como as ligações no projeto funcionam. Isso ocorre porque, ao olhar para a interface da classe `CreditCard`, não é possível identificar o estado global que precisa ser inicializado. Mesmo olhar para o código-fonte da classe não revela qual método de inicialização você deve chamar. Na melhor das hipóteses, você pode encontrar uma variável global que está sendo acessada e, a partir dela, tentar adivinhar como inicializá-la. - -As classes em tal projeto são mentirosas patológicas. O cartão de crédito finge que basta instanciá-lo e chamar o método `charge()`. Secretamente, porém, ele colabora com outra classe `PaymentGateway`, que representa o gateway de pagamento. Sua interface também diz que pode ser inicializada separadamente, mas na realidade ela extrai credenciais de algum arquivo de configuração e assim por diante. Para os desenvolvedores que escreveram este código, está claro que `CreditCard` precisa de `PaymentGateway`. Eles escreveram o código desta forma. Mas para qualquer pessoa nova no projeto, é um mistério absoluto e impede o aprendizado. - -Como consertar a situação? Facilmente. **Deixe a API declarar as dependências.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Observe como as interconexões dentro do código se tornam repentinamente óbvias. Como o método `charge()` declara que precisa de `PaymentGateway`, você não precisa perguntar a ninguém como o código está interconectado. Você sabe que precisa criar sua instância e, ao tentar fazê-lo, descobrirá que precisa fornecer parâmetros de acesso. Sem eles, o código nem sequer seria executado. - -E, o mais importante, agora você pode mockar o gateway de pagamento, para não ser cobrado 100 dólares toda vez que executar o teste. - -O estado global faz com que seus objetos possam acessar secretamente coisas que não são declaradas em sua API e, como resultado, tornam suas APIs mentirosas patológicas. - -Talvez você não tenha pensado nisso antes, mas sempre que usa estado global, está criando canais de comunicação secretos sem fio. A ação fantasmagórica à distância força os desenvolvedores a ler cada linha de código para entender as interações potenciais, reduz a produtividade dos desenvolvedores e confunde os novos membros da equipe. Se você foi quem criou o código, conhece as dependências reais, mas qualquer pessoa que vier depois de você ficará perdida. - -Não escreva código que utilize estado global, prefira passar dependências. Ou seja, injeção de dependência. - - -Fragilidade do estado global ----------------------------- - -No código que usa estado global e singletons, nunca é certo quando e quem alterou esse estado. Esse risco surge já na inicialização. O código a seguir deve criar uma conexão com o banco de dados e inicializar o gateway de pagamento, mas lança constantemente uma exceção e encontrar a causa é extremamente demorado: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Você precisa percorrer detalhadamente o código para descobrir que o objeto `PaymentGateway` acessa sem fio outros objetos, alguns dos quais requerem uma conexão com o banco de dados. Portanto, é necessário inicializar o banco de dados antes de `PaymentGateway`. No entanto, a cortina de fumaça do estado global esconde isso de você. Quanto tempo você economizaria se a API das classes individuais não mentisse e declarasse suas dependências? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Um problema semelhante surge também ao usar acesso global à conexão do banco de dados: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Ao chamar o método `save()`, não é certo se a conexão com o banco de dados já foi criada e quem é responsável por sua criação. Se quisermos, por exemplo, alterar a conexão com o banco de dados em tempo de execução, talvez para testes, provavelmente teríamos que criar outros métodos como `DB::reconnect(...)` ou `DB::reconnectForTest()`. - -Considere o exemplo: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Onde temos certeza de que ao chamar `$article->save()` o banco de dados de teste está realmente sendo usado? E se o método `Foo::doSomething()` alterou a conexão global do banco de dados? Para descobrir, teríamos que examinar o código-fonte da classe `Foo` e provavelmente de muitas outras classes. Essa abordagem, no entanto, traria apenas uma resposta de curto prazo, pois a situação pode mudar no futuro. - -E se movermos a conexão com o banco de dados para uma variável estática dentro da classe `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Isso não mudou nada. O problema é o estado global e é completamente irrelevante em qual classe ele está escondido. Neste caso, assim como no anterior, ao chamar o método `$article->save()`, não temos nenhuma pista sobre em qual banco de dados ele será escrito. Qualquer pessoa do outro lado da aplicação poderia ter alterado o banco de dados a qualquer momento usando `Article::setDb()`. Sob nossos narizes. - -O estado global torna nossa aplicação **extremamente frágil**. - -No entanto, existe uma maneira simples de lidar com esse problema. Basta deixar a API declarar as dependências, garantindo assim a funcionalidade correta. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Graças a essa abordagem, elimina-se a preocupação com alterações ocultas e inesperadas na conexão do banco de dados. Agora temos certeza de onde o artigo está sendo salvo e nenhuma modificação no código dentro de outra classe não relacionada pode mais alterar a situação. O código não é mais frágil, mas estável. - -Não escreva código que utilize estado global, prefira passar dependências. Ou seja, injeção de dependência. - - -Singleton ---------- - -Singleton é um padrão de projeto que, de acordo com a "definição":https://en.wikipedia.org/wiki/Singleton_pattern da conhecida publicação Gang of Four, restringe uma classe a uma única instância e oferece acesso global a ela. A implementação desse padrão geralmente se assemelha ao seguinte código: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // e outros métodos que cumprem as funções da classe dada -} -``` - -Infelizmente, o singleton introduz estado global na aplicação. E como mostramos acima, o estado global é indesejável. Portanto, o singleton é considerado um antipadrão. - -Não use singletons em seu código e substitua-os por outros mecanismos. Você realmente não precisa de singletons. No entanto, se precisar garantir a existência de uma única instância de uma classe para toda a aplicação, deixe isso para o [contêiner DI |container]. Crie assim um singleton de aplicação, ou seja, um serviço. Com isso, a classe deixa de se preocupar em garantir sua própria unicidade (ou seja, não terá o método `getInstance()` e a variável estática) e cumprirá apenas suas funções. Assim, deixará de violar o princípio da responsabilidade única. - - -Estado global versus testes ---------------------------- - -Ao escrever testes, assumimos que cada teste é uma unidade isolada e que nenhum estado externo entra nele. E nenhum estado sai dos testes. Após a conclusão do teste, todo o estado relacionado ao teste deve ser removido automaticamente pelo coletor de lixo. Graças a isso, os testes são isolados. Portanto, podemos executar os testes em qualquer ordem. - -No entanto, se houver estados globais/singletons, todas essas suposições agradáveis desmoronam. O estado pode entrar e sair do teste. De repente, a ordem dos testes pode importar. - -Para poder testar singletons, os desenvolvedores muitas vezes precisam afrouxar suas propriedades, talvez permitindo que a instância seja substituída por outra. Tais soluções são, na melhor das hipóteses, um hack que cria código difícil de manter e entender. Cada teste ou método `tearDown()`, que afeta qualquer estado global, deve reverter essas alterações. - -O estado global é a maior dor de cabeça nos testes unitários! - -Como consertar a situação? Facilmente. Não escreva código que utilize singletons, prefira passar dependências. Ou seja, injeção de dependência. - - -Constantes Globais ------------------- - -O estado global não se limita apenas ao uso de singletons e variáveis estáticas, mas também pode se referir a constantes globais. - -Constantes cujo valor não nos traz nenhuma informação nova (`M_PI`) ou útil (`PREG_BACKTRACK_LIMIT_ERROR`) são claramente aceitáveis. Por outro lado, constantes que servem como uma forma de passar informações *sem fio* para dentro do código não são nada mais do que uma dependência oculta. Como `LOG_FILE` no exemplo a seguir. O uso da constante `FILE_APPEND` é totalmente correto. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Neste caso, deveríamos declarar um parâmetro no construtor da classe `Foo`, para que ele se torne parte da API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Agora podemos passar a informação sobre o caminho do arquivo para log e alterá-la facilmente conforme necessário, o que facilita o teste e a manutenção do código. - - -Funções Globais e Métodos Estáticos ------------------------------------ - -Queremos enfatizar que o uso de métodos estáticos e funções globais em si não é problemático. Explicamos por que o uso de `DB::insert()` e métodos semelhantes é inadequado, mas sempre foi apenas uma questão de estado global armazenado em alguma variável estática. O método `DB::insert()` requer a existência de uma variável estática porque a conexão com o banco de dados está armazenada nela. Sem essa variável, seria impossível implementar o método. - -O uso de métodos estáticos e funções determinísticas, como `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` e muitas outras, está em total conformidade com a injeção de dependência. Essas funções sempre retornam os mesmos resultados para os mesmos parâmetros de entrada e são, portanto, previsíveis. Elas não usam nenhum estado global. - -Existem, porém, também funções no PHP que não são determinísticas. Entre elas está, por exemplo, a função `htmlspecialchars()`. Seu terceiro parâmetro `$encoding`, se não for especificado, tem como valor padrão o valor da opção de configuração `ini_get('default_charset')`. Portanto, recomenda-se sempre especificar este parâmetro para evitar possíveis comportamentos imprevisíveis da função. A Nette faz isso consistentemente. - -Algumas funções, como `strtolower()`, `strtoupper()` e semelhantes, comportaram-se de forma não determinística no passado recente e dependiam da configuração `setlocale()`. Isso causou muitas complicações, mais frequentemente ao trabalhar com a língua turca. Isso porque o turco distingue entre letras `I` maiúsculas e minúsculas com e sem ponto. Assim, `strtolower('I')` retornava o caractere `ı` e `strtoupper('i')` o caractere `İ`, o que levou as aplicações a causar uma série de erros misteriosos. No entanto, esse problema foi corrigido na versão 8.2 do PHP e as funções já não dependem do locale. - -Este é um bom exemplo de como o estado global atormentou milhares de desenvolvedores em todo o mundo. A solução foi substituí-lo por injeção de dependência. - - -Quando é possível usar estado global? -------------------------------------- - -Existem certas situações específicas em que é possível utilizar o estado global. Por exemplo, ao depurar código, quando você precisa imprimir o valor de uma variável ou medir a duração de uma determinada parte do programa. Nesses casos, que dizem respeito a ações temporárias que serão posteriormente removidas do código, é legítimo usar um dumper ou cronômetro globalmente disponível. Essas ferramentas não fazem parte do design do código. - -Outro exemplo são as funções para trabalhar com expressões regulares `preg_*`, que internamente armazenam expressões regulares compiladas em um cache estático na memória. Assim, quando você chama a mesma expressão regular várias vezes em diferentes partes do código, ela é compilada apenas uma vez. O cache economiza desempenho e, ao mesmo tempo, é completamente invisível para o usuário, portanto, tal uso pode ser considerado legítimo. - - -Resumo ------- - -Discutimos por que faz sentido: - -1) Remover todas as variáveis estáticas do código -2) Declarar dependências -3) E usar injeção de dependência - -Ao pensar no design do código, lembre-se de que cada `static $foo` representa um problema. Para que seu código seja um ambiente que respeite DI, é essencial erradicar completamente o estado global e substituí-lo por injeção de dependência. - -Durante esse processo, você pode descobrir que é necessário dividir a classe porque ela tem mais de uma responsabilidade. Não tenha medo disso; busque o princípio da responsabilidade única. - -*Gostaria de agradecer a Miško Hevery, cujos artigos, como [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], são a base deste capítulo.* diff --git a/dependency-injection/pt/introduction.texy b/dependency-injection/pt/introduction.texy deleted file mode 100644 index cfa0126a29..0000000000 --- a/dependency-injection/pt/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -O que é Injeção de Dependência? -******************************* - -.[perex] -Este capítulo apresentará os procedimentos básicos de programação que você deve seguir ao escrever todas as aplicações. São os fundamentos necessários para escrever código limpo, compreensível e sustentável. - -Se você dominar e seguir estas regras, o Nette o apoiará em cada passo. Ele cuidará das tarefas rotineiras para você e fornecerá o máximo de conforto, para que você possa se concentrar na lógica em si. - -Os princípios que mostraremos aqui são bastante simples. Você não precisa se preocupar com nada. - - -Lembra do seu primeiro programa? --------------------------------- - -Não sabemos em que linguagem você o escreveu, mas se fosse PHP, provavelmente seria algo assim: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // imprime 24 -``` - -Algumas linhas triviais de código, mas nelas se escondem tantos conceitos-chave. Que existem variáveis. Que o código é dividido em unidades menores, como funções. Que passamos argumentos de entrada para elas e elas retornam resultados. Faltam apenas condições e loops. - -O fato de passarmos dados de entrada para uma função e ela retornar um resultado é um conceito perfeitamente compreensível, usado também em outras áreas, como na matemática. - -Uma função tem sua assinatura, que consiste em seu nome, uma lista de parâmetros e seus tipos, e finalmente o tipo do valor de retorno. Como usuários, estamos interessados na assinatura; geralmente não precisamos saber nada sobre a implementação interna. - -Agora imagine que a assinatura da função fosse assim: - -```php -function soucet(float $x): float -``` - -Soma com um parâmetro? Isso é estranho... E que tal assim? - -```php -function soucet(): float -``` - -Isso já é muito estranho, não é? Como a função seria usada? - -```php -echo soucet(); // o que será que imprime? -``` - -Ao olhar para tal código, ficaríamos confusos. Não apenas um iniciante não entenderia, mas nem mesmo um programador experiente entenderia tal código. - -Você está pensando como essa função seria por dentro? Onde ela obteria os operandos? Provavelmente, ela os obteria *de alguma forma* por conta própria, talvez assim: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -No corpo da função, descobrimos ligações ocultas a outras funções globais ou métodos estáticos. Para descobrir de onde os operandos realmente vêm, precisamos investigar mais. - - -Não por aqui! -------------- - -O design que acabamos de mostrar é a essência de muitas características negativas: - -- a assinatura da função fingia não precisar de operandos, o que nos confundiu -- não sabemos como fazer a função somar outros dois números -- tivemos que olhar o código para descobrir onde ela obtém os operandos -- descobrimos ligações ocultas -- para entender completamente, é necessário examinar também essas ligações - -E é tarefa da função de soma obter as entradas? Claro que não. Sua responsabilidade é apenas a soma em si. - - -Não queremos encontrar tal código, e definitivamente não queremos escrevê-lo. A correção é simples: voltar ao básico e simplesmente usar parâmetros: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Regra nº 1: peça para ser passado ---------------------------------- - -A regra mais importante é: **todos os dados que uma função ou classe precisa devem ser passados para ela**. - -Em vez de inventar maneiras ocultas pelas quais eles poderiam obtê-los sozinhos, simplesmente passe os parâmetros. Você economizará o tempo necessário para inventar caminhos ocultos, que definitivamente não melhorarão seu código. - -Se você seguir esta regra sempre e em toda parte, estará no caminho para um código sem ligações ocultas. Para um código que é compreensível não apenas para o autor, mas também para qualquer pessoa que o leia depois dele. Onde tudo é compreensível a partir das assinaturas das funções e classes e não há necessidade de procurar segredos ocultos na implementação. - -Essa técnica é tecnicamente chamada de **injeção de dependência**. E esses dados são chamados de **dependências.** Na verdade, é apenas a passagem comum de parâmetros, nada mais. - -.[note] -Por favor, não confunda injeção de dependência, que é um padrão de projeto, com "contêiner de injeção de dependência", que é uma ferramenta, ou seja, algo diametralmente diferente. Falaremos sobre contêineres mais tarde. - - -De funções para classes ------------------------ - -E como as classes se relacionam com isso? Uma classe é uma unidade mais complexa do que uma função simples, mas a regra nº 1 se aplica integralmente aqui também. Apenas existem [mais opções para passar argumentos|passing-dependencies]. Por exemplo, de forma bastante semelhante ao caso de uma função: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Ou usando outros métodos, ou diretamente o construtor: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Ambos os exemplos estão totalmente de acordo com a injeção de dependência. - - -Exemplos reais --------------- - -No mundo real, você não escreverá classes para somar números. Vamos passar para exemplos práticos. - -Temos uma classe `Article` representando um artigo de blog: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // salvamos o artigo no banco de dados - } -} -``` - -e o uso será o seguinte: - -```php -$article = new Article; -$article->title = '10 coisas que você precisa saber sobre perder peso'; -$article->content = 'Todo ano milhões de pessoas em ...'; -$article->save(); -``` - -O método `save()` salva o artigo em uma tabela do banco de dados. Implementá-lo usando [Nette Database |database:] seria moleza, se não fosse por um obstáculo: onde `Article` obtém a conexão com o banco de dados, ou seja, o objeto da classe `Nette\Database\Connection`? - -Parece que temos muitas opções. Pode obtê-lo de algum lugar em uma variável estática. Ou herdar de uma classe que fornece a conexão com o banco de dados. Ou usar o chamado [singleton |global-state#Singleton]. Ou as chamadas facades, que são usadas no 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], - ); - } -} -``` - -Ótimo, resolvemos o problema. - -Ou não? - -Lembre-se da [##Regra nº 1: peça para ser passado]: todas as dependências que a classe precisa devem ser passadas para ela. Porque se quebrarmos a regra, entramos no caminho do código sujo cheio de ligações ocultas, incompreensibilidade, e o resultado será uma aplicação que será dolorosa de manter e desenvolver. - -O usuário da classe `Article` não tem ideia de onde o método `save()` salva o artigo. Em uma tabela do banco de dados? Em qual, produção ou teste? E como isso pode ser alterado? - -O usuário precisa olhar como o método `save()` é implementado e encontra o uso do método `DB::insert()`. Então, ele precisa investigar mais, como esse método obtém a conexão com o banco de dados. E as ligações ocultas podem formar uma cadeia bastante longa. - -Em código limpo e bem projetado, nunca existem ligações ocultas, facades do Laravel ou variáveis estáticas. Em código limpo e bem projetado, os argumentos são passados: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Ainda mais prático, como veremos mais adiante, será pelo construtor: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Se você é um programador experiente, pode estar pensando que `Article` não deveria ter um método `save()`, deveria representar puramente um componente de dados e o armazenamento deveria ser responsabilidade de um repositório separado. Isso faz sentido. Mas isso nos levaria muito além do escopo do tópico, que é a injeção de dependência, e do esforço para fornecer exemplos simples. - -Se você for escrever uma classe que requer, por exemplo, um banco de dados para sua operação, não invente de onde obtê-lo, mas peça para que seja passado. Talvez como um parâmetro do construtor ou de outro método. Admita as dependências. Admita-as na API da sua classe. Você obterá um código compreensível e previsível. - -E que tal esta classe, que registra mensagens de erro: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -O que você acha, seguimos a [##Regra nº 1: peça para ser passado]? - -Não seguimos. - -A informação chave, ou seja, o diretório com o arquivo de log, a classe *obtém por si mesma* a partir de uma constante. - -Veja o exemplo de uso: - -```php -$logger = new Logger; -$logger->log('A temperatura é 23 °C'); -$logger->log('A temperatura é 10 °C'); -``` - -Sem conhecer a implementação, você conseguiria responder à pergunta de onde as mensagens são escritas? Você pensaria que para funcionar é necessária a existência da constante `LOG_DIR`? E você conseguiria criar uma segunda instância que escreveria em outro lugar? Certamente não. - -Vamos corrigir a classe: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -A classe agora é muito mais compreensível, configurável e, portanto, mais útil. - -```php -$logger = new Logger('/caminho/para/log.txt'); -$logger->log('A temperatura é 15 °C'); -``` - - -Mas isso não me interessa! --------------------------- - -*"Quando crio um objeto Article e chamo save(), não quero lidar com o banco de dados, só quero que ele seja salvo naquele que configurei."* - -*"Quando uso o Logger, só quero que a mensagem seja escrita, e não quero me preocupar onde. Que use a configuração global."* - -Essas são observações válidas. - -Como exemplo, mostraremos uma classe que envia newsletters e registra o resultado: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('E-mails foram enviados'); - - } catch (Exception $e) { - $logger->log('Ocorreu um erro ao enviar'); - throw $e; - } - } -} -``` - -O `Logger` aprimorado, que não usa mais a constante `LOG_DIR`, requer que o caminho do arquivo seja especificado no construtor. Como resolver isso? A classe `NewsletterDistributor` não se importa onde as mensagens são escritas, ela só quer escrevê-las. - -A solução é novamente a [##Regra nº 1: peça para ser passado]: todos os dados que a classe precisa, nós passamos para ela. - -Então isso significa que passamos o caminho do log através do construtor, que então usamos ao criar o objeto `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ ASSIM NÃO! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Assim não! O caminho, de fato, **não pertence** aos dados que a classe `NewsletterDistributor` precisa; esses são necessários pelo `Logger`. Você percebe a diferença? A classe `NewsletterDistributor` precisa do logger como tal. Então, passamos ele: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('E-mails foram enviados'); - - } catch (Exception $e) { - $this->logger->log('Ocorreu um erro ao enviar'); - throw $e; - } - } -} -``` - -Agora está claro pelas assinaturas da classe `NewsletterDistributor` que o log faz parte de sua funcionalidade. E a tarefa de trocar o logger por outro, talvez para testes, é completamente trivial. Além disso, se o construtor da classe `Logger` mudar, isso não terá nenhum efeito em nossa classe. - - -Regra nº 2: pegue o que é seu ------------------------------ - -Não se deixe enganar e não peça para passar as dependências de suas dependências. Peça para passar apenas suas dependências. - -Graças a isso, o código que utiliza outros objetos será completamente independente das mudanças em seus construtores. Sua API será mais verdadeira. E, principalmente, será trivial trocar essas dependências por outras. - - -Novo membro da família ----------------------- - -Na equipe de desenvolvimento, foi decidido criar um segundo logger, que escreve no banco de dados. Criaremos então a classe `DatabaseLogger`. Então temos duas classes, `Logger` e `DatabaseLogger`, uma escreve em arquivo, a outra no banco de dados... não parece algo estranho nessa nomenclatura? Não seria melhor renomear `Logger` para `FileLogger`? Certamente sim. - -Mas faremos isso de forma inteligente. Sob o nome original, criaremos uma interface: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -... que ambos os loggers implementarão: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -E graças a isso, não será necessário alterar nada no restante do código onde o logger é utilizado. Por exemplo, o construtor da classe `NewsletterDistributor` continuará satisfeito em exigir `Logger` como parâmetro. E caberá a nós qual instância passar para ele. - -**Por isso, nunca damos aos nomes das interfaces o sufixo `Interface` ou o prefixo `I`.** Caso contrário, não seria possível desenvolver o código de forma tão elegante. - - -Houston, temos um problema --------------------------- - -Enquanto em toda a aplicação podemos nos contentar com uma única instância de logger, seja de arquivo ou de banco de dados, e simplesmente passá-la para todos os lugares onde algo é registrado, a situação é bem diferente no caso da classe `Article`. Suas instâncias são criadas conforme necessário, até mesmo várias vezes. Como lidar com a dependência do banco de dados em seu construtor? - -Como exemplo, pode servir um controller que, após o envio de um formulário, deve salvar o artigo no banco de dados: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Uma solução possível se oferece diretamente: passamos o objeto do banco de dados pelo construtor para `EditController` e usamos `$article = new Article($this->db)`. - -Assim como no caso anterior com `Logger` e o caminho do arquivo, este não é o procedimento correto. O banco de dados não é uma dependência de `EditController`, mas de `Article`. Passar o banco de dados, portanto, vai contra a [#regra nº 2: pegue o que é seu]. Quando o construtor da classe `Article` mudar (um novo parâmetro for adicionado), será necessário modificar também o código em todos os lugares onde instâncias são criadas. Ufa. - -Houston, o que você sugere? - - -Regra nº 3: deixe para a fábrica --------------------------------- - -Ao eliminar as ligações ocultas e passar todas as dependências como argumentos, obtivemos classes mais configuráveis e flexíveis. E, portanto, precisamos de algo mais, que crie e configure essas classes mais flexíveis para nós. Chamaremos isso de fábricas. - -A regra é: se uma classe tem dependências, deixe a criação de suas instâncias para a fábrica. - -As fábricas são substitutos mais inteligentes do operador `new` no mundo da injeção de dependência. - -.[note] -Por favor, não confunda com o padrão de projeto *factory method*, que descreve um uso específico de fábricas e não está relacionado a este tópico. - - -Fábrica -------- - -Uma fábrica é um método ou classe que produz e configura objetos. A classe que produz `Article` chamaremos de `ArticleFactory` e poderia parecer, por exemplo, assim: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Seu uso no controller será o seguinte: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // deixamos a fábrica criar o objeto - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Neste momento, se a assinatura do construtor da classe `Article` mudar, a única parte do código que precisa reagir é a própria fábrica `ArticleFactory`. Todo o restante do código que trabalha com objetos `Article`, como `EditController`, não será afetado de forma alguma. - -Talvez você esteja batendo na testa agora, se realmente nos ajudamos. A quantidade de código aumentou e tudo começa a parecer suspeitosamente complicado. - -Não se preocupe, em breve chegaremos ao Contêiner de DI do Nette. E ele tem vários ases na manga que simplificarão imensamente a construção de aplicações usando injeção de dependência. Por exemplo, em vez da classe `ArticleFactory`, será suficiente [escrever apenas uma interface |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Mas estamos nos adiantando, aguarde mais um pouco :-) - - -Resumo ------- - -No início deste capítulo, prometemos mostrar um procedimento para projetar código limpo. Basta para as classes - -1) [passar as dependências que precisam |#Regra nº 1: peça para ser passado |#pravidlo č. 1: nech si to předat] -2) [e, inversamente, não passar o que não precisam diretamente |#Regra nº 2: pegue o que é seu |#Pravidlo č. 2: ber, co tvé jest] -3) [e que objetos com dependências são melhor criados em fábricas |#Regra nº 3: deixe para a fábrica |#Pravidlo č. 3: nech to na továrně] - -Pode não parecer à primeira vista, mas essas três regras têm consequências de longo alcance. Elas levam a uma visão radicalmente diferente do design de código. Vale a pena? Programadores que abandonaram velhos hábitos e começaram a usar consistentemente a injeção de dependência consideram este passo um momento crucial em suas vidas profissionais. Abriu-se para eles o mundo de aplicações claras e sustentáveis. - -Mas e se o código não usar consistentemente a injeção de dependência? E se for construído sobre métodos estáticos ou singletons? Isso traz algum problema? [Traz e muito fundamentais |global-state]. diff --git a/dependency-injection/pt/nette-container.texy b/dependency-injection/pt/nette-container.texy deleted file mode 100644 index d7373bd24d..0000000000 --- a/dependency-injection/pt/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Contêiner de DI do Nette -************************ - -.[perex] -Nette DI é uma das bibliotecas mais interessantes do Nette. Ela pode gerar e atualizar automaticamente contêineres de DI compilados, que são extremamente rápidos e incrivelmente fáceis de configurar. - -A forma dos serviços que o Contêiner de DI deve criar é geralmente definida usando arquivos de configuração no [formato NEON|neon:format]. O contêiner que criamos manualmente no [capítulo anterior|container] seria escrito assim: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -A notação é realmente concisa. - -Todas as dependências declaradas nos construtores das classes `ArticleFactory` e `UserController` são descobertas e passadas automaticamente pelo Nette DI graças ao chamado [autowiring|autowiring], portanto, não é necessário especificar nada no arquivo de configuração. Assim, mesmo que os parâmetros mudem, você não precisa alterar nada na configuração. O contêiner Nette é regenerado automaticamente. Você pode se concentrar puramente no desenvolvimento da aplicação. - -Se quisermos passar dependências usando setters, usamos a seção [setup |services#Setup] para isso. - -Nette DI gera diretamente o código PHP do contêiner. O resultado é, portanto, um arquivo `.php` que você pode abrir e estudar. Graças a isso, você vê exatamente como o contêiner funciona. Você também pode depurá-lo no IDE e percorrer passo a passo. E o mais importante: o PHP gerado é extremamente rápido. - -Nette DI também pode gerar código para [fábricas|factory] com base na interface fornecida. Portanto, em vez da classe `ArticleFactory`, basta criar apenas uma interface na aplicação: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Você pode encontrar o exemplo completo [no GitHub|https://github.com/nette-examples/di-example-doc]. - - -Uso independente ----------------- - -Implantar a biblioteca Nette DI em uma aplicação é muito fácil. Primeiro, instalamos com o Composer (porque baixar zips é tããão ultrapassado): - -```shell -composer require nette/di -``` - -O código a seguir cria uma instância do Contêiner de DI de acordo com a configuração armazenada no arquivo `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -O contêiner é gerado apenas uma vez, seu código é escrito no cache (diretório `__DIR__ . '/temp'`) e nas requisições subsequentes ele é apenas carregado de lá. - -Para criar e obter serviços, são usados os métodos `getService()` ou `getByType()`. Assim criamos o objeto `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -Durante o desenvolvimento, é útil ativar o modo de atualização automática, onde o contêiner é automaticamente regenerado se qualquer classe ou arquivo de configuração for alterado. Basta especificar `true` como segundo argumento no construtor `ContainerLoader`. - -```php -$loader = new ContainerLoader(__DIR__ . '/temp', autoRebuild: true); -``` - - -Uso com o framework Nette -------------------------- - -Como mostramos, o uso do Nette DI não se limita a aplicações escritas no Nette Framework, você pode implantá-lo em qualquer lugar com apenas 3 linhas de código. No entanto, se você desenvolve aplicações no Nette Framework, a configuração e criação do contêiner são de responsabilidade do [Bootstrap |application:bootstrapping#Configuração do contêiner de DI]. diff --git a/dependency-injection/pt/passing-dependencies.texy b/dependency-injection/pt/passing-dependencies.texy deleted file mode 100644 index 303ddb09c0..0000000000 --- a/dependency-injection/pt/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Passando Dependências -********************* - -<div class=perex> - -Argumentos, ou na terminologia de DI "dependências", podem ser passados para classes das seguintes maneiras principais: - -* passagem pelo construtor -* passagem por método (chamado setter) -* configuração de propriedade -* método, anotação ou atributo *inject* - -</div> - -Agora mostraremos cada variante com exemplos concretos. - - -Passagem pelo construtor -======================== - -As dependências são passadas no momento da criação do objeto como argumentos do construtor: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Esta forma é adequada para dependências obrigatórias que a classe necessita essencialmente para sua função, pois sem elas a instância não poderá ser criada. - -A partir do PHP 8.0, podemos usar uma forma mais curta de notação ([constructor property promotion |https://blog.nette.org/pt/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), que é funcionalmente equivalente: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -A partir do PHP 8.1, a propriedade pode ser marcada com o sinalizador `readonly`, que declara que o conteúdo da propriedade não mudará mais: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -O contêiner de DI passa as dependências para o construtor automaticamente usando [autowiring |autowiring]. Argumentos que não podem ser passados dessa forma (por exemplo, strings, números, booleanos) [escrevemos na configuração |services#Argumentos]. - - -Constructor hell ----------------- - -O termo *constructor hell* descreve a situação em que um descendente herda de uma classe pai cujo construtor requer dependências, e ao mesmo tempo o descendente requer dependências. Ele também deve receber e passar as dependências do pai: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -O problema surge no momento em que queremos alterar o construtor da classe `BaseClass`, por exemplo, quando uma nova dependência é adicionada. Então, é necessário modificar também todos os construtores dos descendentes. O que torna tal modificação um inferno. - -Como evitar isso? A solução é **dar preferência à [composição em vez de herança |faq#Por que a composição é preferida em relação à herança]**. - -Ou seja, projetaremos o código de forma diferente. Evitaremos classes [abstratas |nette:introduction-to-object-oriented-programming#Classes Abstratas] `Base*`. Em vez de `MyClass` obter certas funcionalidades herdando de `BaseClass`, essa funcionalidade será passada como dependência: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Passagem por setter -=================== - -As dependências são passadas chamando um método que as armazena em uma propriedade privada. A convenção usual de nomenclatura para esses métodos é a forma `set*()`, por isso são chamados de setters, mas podem, é claro, ter qualquer outro nome. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Este método é adequado para dependências opcionais que não são essenciais para a função da classe, pois não há garantia de que o objeto realmente receberá a dependência (ou seja, que o usuário chamará o método). - -Ao mesmo tempo, este método permite chamar o setter repetidamente e, assim, alterar a dependência. Se isso não for desejado, adicionamos uma verificação ao método ou, a partir do PHP 8.1, marcamos a propriedade `$cache` com o sinalizador `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new \RuntimeException('A dependência já foi definida'); - } - $this->cache = $cache; - } -} -``` - -A chamada do setter é definida na configuração do contêiner de DI na [chave setup |services#Setup]. Aqui também se utiliza a passagem automática de dependências por autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Configuração de propriedade -=========================== - -As dependências são passadas escrevendo diretamente na propriedade de membro: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Este método é considerado inadequado porque a propriedade de membro deve ser declarada como `public`. E, portanto, não temos controle sobre se a dependência passada será realmente do tipo especificado (válido antes do PHP 7.4) e perdemos a capacidade de reagir à dependência recém-atribuída com código próprio, por exemplo, para impedir alterações subsequentes. Ao mesmo tempo, a propriedade se torna parte da interface pública da classe, o que pode não ser desejável. - -A configuração da propriedade é definida na configuração do contêiner de DI na [seção setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Enquanto os três métodos anteriores se aplicam geralmente em todas as linguagens orientadas a objetos, a injeção por método, anotação ou atributo *inject* é específica puramente para presenters no Nette. Eles são discutidos em um [capítulo separado |best-practices:inject-method-attribute]. - - -Qual método escolher? -===================== - -- o construtor é adequado para dependências obrigatórias que a classe necessita essencialmente para sua função -- o setter, por outro lado, é adequado para dependências opcionais, ou dependências que podem ser alteradas posteriormente -- propriedades públicas não são adequadas diff --git a/dependency-injection/pt/services.texy b/dependency-injection/pt/services.texy deleted file mode 100644 index 916ac4050d..0000000000 --- a/dependency-injection/pt/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Definindo Serviços -****************** - -.[perex] -A configuração é o local onde ensinamos ao contêiner de DI como construir serviços individuais e como conectá-los a outras dependências. O Nette fornece uma maneira muito clara e elegante de conseguir isso. - -A seção `services` no arquivo de configuração no formato NEON é onde definimos nossos próprios serviços e suas configurações. Vejamos um exemplo simples de definição de um serviço chamado `database`, que representa uma instância da classe `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -A configuração fornecida resultará no seguinte método de fábrica no [Contêiner de DI|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Os nomes dos serviços nos permitem referenciá-los em outras partes do arquivo de configuração, no formato `@nomeDoServico`. Se não for necessário nomear o serviço, podemos simplesmente usar um marcador: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Para obter um serviço do contêiner de DI, podemos usar o método `getService()` com o nome do serviço como parâmetro, ou o método `getByType()` com o tipo do serviço: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Criação do serviço -================== - -Geralmente, criamos um serviço simplesmente criando uma instância de uma determinada classe. Por exemplo: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Se precisarmos estender a configuração com outras chaves, a definição pode ser dividida em várias linhas: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -A chave `create` tem um alias `factory`, ambas as variantes são comuns na prática. No entanto, recomendamos usar `create`. - -Os argumentos do construtor ou do método de criação podem ser escritos alternativamente na chave `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Os serviços não precisam ser criados apenas pela simples criação de uma instância de classe, eles também podem ser o resultado da chamada de métodos estáticos ou métodos de outros serviços: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Observe que, para simplificar, `::` é usado em vez de `->`, veja [#expressões]. Os seguintes métodos de fábrica serão gerados: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -O contêiner de DI precisa saber o tipo do serviço criado. Se criarmos um serviço usando um método que não tem um tipo de retorno especificado, devemos especificar explicitamente esse tipo na configuração: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumentos -========== - -Passamos argumentos para o construtor e métodos de maneira muito semelhante ao próprio PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Para melhor legibilidade, podemos dividir os argumentos em linhas separadas. Nesse caso, o uso de vírgulas é opcional: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Você também pode nomear os argumentos e não precisa se preocupar com a ordem deles: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Se você quiser omitir alguns argumentos e usar seu valor padrão ou injetar um serviço usando [autowiring|autowiring], use um sublinhado: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Como argumentos, é possível passar serviços, usar parâmetros e muito mais, veja [#expressões]. - - -Setup -===== - -Na seção `setup`, definimos os métodos que devem ser chamados ao criar o serviço. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Isso seria assim em PHP: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Além de chamar métodos, também é possível passar valores para propriedades. A adição de um elemento a um array também é suportada, o que precisa ser escrito entre aspas para não colidir com a sintaxe NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -O que seria assim no código PHP: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -No setup, no entanto, também é possível chamar métodos estáticos ou métodos de outros serviços. Se você precisar passar o serviço atual como argumento, indique-o como `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Observe que, para simplificar, `::` é usado em vez de `->`, veja [#expressões]. O seguinte método de fábrica será gerado: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Expressões .{expressões} -======================== - -Nette DI nos dá recursos de expressão extraordinariamente ricos, com os quais podemos escrever quase qualquer coisa. Nos arquivos de configuração, podemos usar [parâmetros |configuration#Parâmetros]: - -```neon -# parâmetro -%wwwDir% - -# valor do parâmetro sob a chave -%mailer.user% - -# parâmetro dentro de uma string -'%wwwDir%/images' -``` - -Além disso, criar objetos, chamar métodos e funções: - -```neon -# criação de objeto -DateTime() - -# chamada de método estático -Collator::create(%locale%) - -# chamada de função PHP -::getenv(DB_USER) -``` - -Referenciar serviços pelo nome ou pelo tipo: - -```neon -# serviço por nome -@database - -# serviço por tipo -@Nette\Database\Connection -``` - -Usar a sintaxe first-class callable: .{data-version:3.2.0} - -```neon -# criação de callback, análogo a [@user, logout] -@user::logout(...) -``` - -Usar constantes: - -```neon -# constante de classe -FilesystemIterator::SKIP_DOTS - -# constante global obtida pela função PHP constant() -::constant(PHP_VERSION) -``` - -As chamadas de método podem ser encadeadas como em PHP. Apenas para simplificar, `::` é usado em vez de `->`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Você pode usar essas expressões em qualquer lugar, ao [criar serviços |#Criação do serviço], em [#argumentos], na seção [#Setup] ou em [parâmetros |configuration#Parâmetros]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Funções especiais ------------------ - -Nos arquivos de configuração, você pode usar estas funções especiais: - -- `not()` negação do valor -- `bool()`, `int()`, `float()`, `string()` conversão sem perdas para o tipo especificado -- `typed()` cria um array de todos os serviços do tipo especificado -- `tagged()` cria um array de todos os serviços com a tag especificada - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -Em comparação com a conversão de tipo clássica em PHP, como `(int)`, a conversão sem perdas lançará uma exceção para valores não numéricos. - -A função `typed()` cria um array de todos os serviços de um determinado tipo (classe ou interface). Ela omite serviços que têm o autowiring desativado. É possível especificar vários tipos separados por vírgula. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Você também pode passar um array de serviços de um determinado tipo como argumento automaticamente usando [autowiring |autowiring#Array de serviços]. - -A função `tagged()` então cria um array de todos os serviços com uma determinada tag. Aqui também você pode especificar várias tags separadas por vírgula. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -A chave `autowired` permite influenciar o comportamento do autowiring para um serviço específico. Para detalhes, veja [o capítulo sobre autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # o serviço foo é excluído do autowiring -``` - - -Serviços Lazy .{data-version:3.2.4} -=================================== - -Lazy loading é uma técnica que adia a criação de um serviço até o momento em que ele é realmente necessário. Na configuração global, é possível [habilitar a criação lazy |configuration#Serviços Lazy] para todos os serviços de uma vez. Para serviços individuais, você pode então substituir esse comportamento: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Quando um serviço é definido como lazy, ao solicitá-lo do contêiner de DI, recebemos um objeto substituto especial. Ele parece e se comporta da mesma forma que o serviço real, mas a inicialização real (chamada do construtor e setup) ocorre apenas na primeira chamada de qualquer um de seus métodos ou propriedades. - -.[note] -O lazy loading pode ser usado apenas para classes de usuário, não para classes internas do PHP. Requer PHP 8.4 ou mais recente. - - -Tags -==== - -As tags servem para adicionar informações complementares aos serviços. Você pode adicionar uma ou mais tags a um serviço: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -As tags também podem carregar valores: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Para obter todos os serviços com certas tags, você pode usar a função `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -No contêiner de DI, você pode obter os nomes de todos os serviços com uma determinada tag usando o método `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names é um array contendo o nome do serviço e o valor da tag -// por exemplo, ['foo' => 'monolog.logger.event', ...] -``` - - -Modo Inject -=========== - -Usando o sinalizador `inject: true`, a passagem de dependências é ativada através de propriedades públicas com a anotação [inject |best-practices:inject-method-attribute#Atributos Inject] e métodos [inject*() |best-practices:inject-method-attribute#Métodos inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Por padrão, `inject` é ativado apenas para presenters. - - -Modificação de serviços -======================= - -O contêiner de DI contém muitos serviços que foram adicionados através de extensões embutidas ou [de usuário|extensions]. Você pode modificar as definições desses serviços diretamente na configuração. Por exemplo, você pode alterar a classe do serviço `application.application`, que por padrão é `Nette\Application\Application`, para outra: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -O sinalizador `alteration` é informativo e indica que estamos apenas modificando um serviço existente. - -Também podemos complementar o setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Ao sobrescrever um serviço, podemos querer remover os argumentos originais, itens de setup ou tags, para o qual usamos `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Se você quiser remover um serviço adicionado por uma extensão, pode fazer assim: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/ro/@home.texy b/dependency-injection/ro/@home.texy deleted file mode 100644 index 4f32db93ab..0000000000 --- a/dependency-injection/ro/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection este un pattern de design care vă va schimba fundamental perspectiva asupra codului și dezvoltării. Vă va deschide calea către lumea aplicațiilor proiectate curat și sustenabile. - -- [Ce este Dependency Injection? |introduction] -- [Stare globală și singleton-uri |global-state] -- [Transmiterea dependențelor |passing-dependencies] -- [Ce este un container DI? |container] -- [Întrebări frecvente|faq] - - -Pachetul `nette/di` oferă un container DI compilat extrem de avansat pentru PHP. - -- [Nette DI Container |nette-container] -- [Configurație |configuration] -- [Definirea serviciilor |services] -- [Autowiring |autowiring] -- [Fabrici generate |factory] -- [Crearea extensiilor pentru Nette DI|extensions] diff --git a/dependency-injection/ro/@left-menu.texy b/dependency-injection/ro/@left-menu.texy deleted file mode 100644 index 5ae7872994..0000000000 --- a/dependency-injection/ro/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Ce este DI? |introduction] -- [Stare globală și singleton-uri |global-state] -- [Transmiterea dependențelor |passing-dependencies] -- [Ce este un container DI? |container] -- [Întrebări frecvente|faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Configurație |configuration] -- [Definirea serviciilor |services] -- [Autowiring |autowiring] -- [Fabrici generate |factory] -- [Crearea extensiilor pentru Nette DI|extensions] diff --git a/dependency-injection/ro/@meta.texy b/dependency-injection/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/dependency-injection/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/dependency-injection/ro/autowiring.texy b/dependency-injection/ro/autowiring.texy deleted file mode 100644 index 58ccaf9083..0000000000 --- a/dependency-injection/ro/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring este o caracteristică excelentă care poate transmite automat serviciile necesare către constructor și alte metode, astfel încât nu trebuie să le scriem deloc. Vă economisește mult timp. - -Datorită acestui fapt, putem omite marea majoritate a argumentelor atunci când scriem definiții de servicii. În loc de: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Este suficient să scrieți: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring se ghidează după tipuri, așa că pentru a funcționa, clasa `ArticleRepository` trebuie definită aproximativ astfel: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Pentru a putea utiliza autowiring, trebuie să existe **exact un serviciu** pentru fiecare tip în container. Dacă ar exista mai multe, autowiring nu ar ști pe care să îl transmită și ar arunca o excepție: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # ARUNCĂ EXCEPȚIE, se potrivesc atât mainDb cât și tempDb -``` - -Soluția ar fi fie să ocoliți autowiring-ul și să specificați explicit numele serviciului (adică `articles: Model\ArticleRepository(@mainDb)`). Dar este mai convenabil să [dezactivați |#Dezactivarea autowiring-ului] autowiring-ul pentru unul dintre servicii sau să [prioritizați |#Preferința autowiring-ului] primul serviciu. - - -Dezactivarea autowiring-ului ----------------------------- - -Putem dezactiva autowiring-ul unui serviciu folosind opțiunea `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # serviciul tempDb este exclus din autowiring - - articles: Model\ArticleRepository # prin urmare, transmite mainDb către constructor -``` - -Serviciul `articles` nu aruncă o excepție că există două servicii potrivite de tip `PDO` (adică `mainDb` și `tempDb`) care pot fi transmise constructorului, deoarece vede doar serviciul `mainDb`. - -.[note] -Configurarea autowiring-ului în Nette funcționează diferit față de Symfony, unde opțiunea `autowire: false` specifică faptul că autowiring-ul nu trebuie utilizat pentru argumentele constructorului serviciului respectiv. În Nette, autowiring-ul este întotdeauna utilizat, fie pentru argumentele constructorului, fie pentru orice altă metodă. Opțiunea `autowired: false` specifică faptul că instanța serviciului respectiv nu trebuie transmisă nicăieri prin autowiring. - - -Preferința autowiring-ului --------------------------- - -Dacă avem mai multe servicii de același tip și pentru unul dintre ele specificăm opțiunea `autowired`, acest serviciu devine preferat: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # devine preferat - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Serviciul `articles` nu aruncă o excepție că există două servicii potrivite de tip `PDO` (adică `mainDb` și `tempDb`), ci folosește serviciul preferat, adică `mainDb`. - - -Array de servicii ------------------ - -Autowiring poate transmite și array-uri de servicii de un anumit tip. Deoarece în PHP nu se poate scrie nativ tipul elementelor unui array, este necesar, pe lângă tipul `array`, să se adauge și un comentariu phpDoc cu tipul elementului în formatul `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -Containerul DI transmite apoi automat un array de servicii corespunzătoare tipului respectiv. Omită serviciile care au autowiring-ul dezactivat. - -Tipul din comentariu poate fi și în formatul `array<int, Class>` sau `list<Class>`. Dacă nu puteți influența forma comentariului phpDoc, puteți transmite array-ul de servicii direct în configurație folosind [`typed()` |services#Funcții speciale]. - - -Argumente scalare ------------------ - -Autowiring poate injecta doar obiecte și array-uri de obiecte. Argumentele scalare (de ex. șiruri, numere, booleeni) [le scriem în configurație |services#Argumente]. O alternativă este crearea unui [obiect de setări |best-practices:passing-settings-to-presenters], care încapsulează valoarea scalară (sau mai multe valori) sub formă de obiect, care apoi poate fi transmis din nou prin autowiring. - -```php -class MySettings -{ - public function __construct( - // readonly poate fi utilizat începând cu PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Creați un serviciu din acesta adăugându-l în configurație: - -```neon -services: - - MySettings('any value') -``` - -Toate clasele îl vor solicita apoi prin autowiring. - - -Restrângerea autowiring-ului ----------------------------- - -Autowiring-ul serviciilor individuale poate fi restrâns la anumite clase sau interfețe. - -În mod normal, autowiring-ul transmite serviciul către fiecare parametru al metodei al cărui tip corespunde serviciului. Restrângerea înseamnă că stabilim condiții pe care tipurile specificate la parametrii metodelor trebuie să le îndeplinească pentru ca serviciul să le fie transmis. - -Să ilustrăm acest lucru cu un exemplu: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Dacă le-am înregistra pe toate ca servicii, autowiring-ul ar eșua: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # ARUNCĂ EXCEPȚIE, se potrivesc serviciile parent și child - childDep: ChildDependent # autowiring transmite serviciul child către constructor -``` - -Serviciul `parentDep` aruncă excepția `Multiple services of type ParentClass found: parent, child`, deoarece ambele servicii `parent` și `child` se potrivesc constructorului său, iar autowiring-ul nu poate decide pe care să îl aleagă. - -Prin urmare, pentru serviciul `child`, putem restrânge autowiring-ul său la tipul `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # se poate scrie și 'autowired: self' - - parentDep: ParentDependent # autowiring transmite serviciul parent către constructor - childDep: ChildDependent # autowiring transmite serviciul child către constructor -``` - -Acum, serviciul `parent` este transmis constructorului serviciului `parentDep`, deoarece acum este singurul obiect potrivit. Autowiring-ul nu mai transmite serviciul `child` acolo. Da, serviciul `child` este încă de tip `ParentClass`, dar condiția de restrângere dată pentru tipul parametrului nu mai este valabilă, adică nu este adevărat că `ParentClass` *este un supratip* al `ChildClass`. - -Pentru serviciul `child`, `autowired: ChildClass` ar putea fi scris și ca `autowired: self`, deoarece `self` este un substituent pentru clasa serviciului curent. - -În cheia `autowired` este posibil să se specifice și mai multe clase sau interfețe ca un array: - -```neon -autowired: [BarClass, FooInterface] -``` - -Să încercăm să completăm exemplul cu interfețe: - -```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) - {} -} -``` - -Dacă nu restricționăm în niciun fel serviciul `child`, acesta se va potrivi constructorilor tuturor claselor `FooDependent`, `BarDependent`, `ParentDependent` și `ChildDependent`, iar autowiring-ul îl va transmite acolo. - -Dar dacă îi restrângem autowiring-ul la `ChildClass` folosind `autowired: ChildClass` (sau `self`), autowiring-ul îl va transmite doar constructorului `ChildDependent`, deoarece necesită un argument de tip `ChildClass` și este adevărat că `ChildClass` *este de tip* `ChildClass`. Niciun alt tip specificat la ceilalți parametri nu este un supratip al `ChildClass`, deci serviciul nu este transmis. - -Dacă îl restricționăm la `ParentClass` folosind `autowired: ParentClass`, autowiring-ul îl va transmite din nou constructorului `ChildDependent` (deoarece `ChildClass` necesar este un supratip al `ParentClass`) și, nou, și constructorului `ParentDependent`, deoarece tipul necesar `ParentClass` este, de asemenea, potrivit. - -Dacă îl restricționăm la `FooInterface`, va fi în continuare autowired în `ParentDependent` (necesarul `ParentClass` este un supratip al `FooInterface`) și `ChildDependent`, dar în plus și în constructorul `FooDependent`, însă nu în `BarDependent`, deoarece `BarInterface` nu este un supratip al `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring transmite child către constructor - barDep: BarDependent # ARUNCĂ EXCEPȚIE, niciun serviciu nu se potrivește - parentDep: ParentDependent # autowiring transmite child către constructor - childDep: ChildDependent # autowiring transmite child către constructor -``` diff --git a/dependency-injection/ro/configuration.texy b/dependency-injection/ro/configuration.texy deleted file mode 100644 index c78aeade7a..0000000000 --- a/dependency-injection/ro/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Configurarea containerului DI -***************************** - -.[perex] -Prezentare generală a opțiunilor de configurare pentru containerul Nette DI. - - -Fișier de configurare -===================== - -Containerul Nette DI este ușor de controlat folosind fișiere de configurare. Acestea sunt de obicei scrise în [formatul NEON |neon:format]. Pentru editare, recomandăm [editoare cu suport |best-practices:editors-and-tools#Editor IDE] pentru acest format. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Decorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "Container DI .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Extensii]: "Instalarea altor extensii DI .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Includerea fișierelor]: "Includerea fișierelor .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parametri]: "Parametri .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Înregistrarea automată a serviciilor .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Servicii .[prism-token prism-comment]" -</pre> - -.[note] -Pentru a scrie un șir care conține caracterul `%`, trebuie să îl escapați dublându-l la `%%`. - - -Parametri -========= - -În configurație puteți defini parametri care pot fi apoi utilizați ca parte a definițiilor serviciilor. Astfel puteți clarifica configurația sau puteți unifica și extrage valorile care se vor modifica. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Ne referim la parametrul `dsn` oriunde în configurație scriind `%dsn%`. Parametrii pot fi utilizați și în interiorul șirurilor precum `'%wwwDir%/images'`. - -Parametrii nu trebuie să fie doar șiruri sau numere, pot conține și array-uri: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Ne referim la cheia specifică ca `%mailer.user%`. - -Dacă aveți nevoie în codul dvs., de exemplu într-o clasă, să aflați valoarea oricărui parametru, transmiteți-l acelei clase. De exemplu, în constructor. Nu există niciun obiect global care să reprezinte configurația, pe care clasele să îl interogheze pentru valorile parametrilor. Acest lucru ar încălca principiul injecției de dependență. - - -Servicii -======== - -Vezi [capitolul separat |services]. - - -Decorator -========= - -Cum să modificați în masă toate serviciile de un anumit tip? De exemplu, să apelați o anumită metodă la toți presenterii care moștenesc de la un anumit strămoș comun? Pentru asta există decoratorul. - -```neon -decorator: - # pentru toate serviciile care sunt instanțe ale acestei clase sau interfețe - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # apelează această metodă - - $absoluteUrls = true # și setează variabila -``` - -Decoratorul poate fi utilizat și pentru setarea [tag-urilor |services#Tag-uri] sau activarea modului [inject |services#Mod Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Setări tehnice ale containerului DI. - -```neon -di: - # afișează DIC în Tracy Bar? - debugger: ... # (bool) implicit este true - - # tipuri de parametri care nu se autowirează niciodată - excluded: ... # (string[]) - - # permite crearea lazy a serviciilor? - lazy: ... # (bool) implicit este false - - # clasa de la care moștenește containerul DI - parentClass: ... # (string) implicit este Nette\DI\Container -``` - - -Servicii lazy .{data-version:3.2.4} ------------------------------------ - -Setarea `lazy: true` activează crearea lazy (amânată) a serviciilor. Acest lucru înseamnă că serviciile nu sunt create efectiv în momentul în care le solicităm din containerul DI, ci abia în momentul primei lor utilizări. Acest lucru poate accelera pornirea aplicației și reduce cerințele de memorie, deoarece se creează doar serviciile care sunt efectiv necesare în request-ul respectiv. - -Pentru un serviciu specific, crearea lazy poate fi [modificată |services#Servicii lazy]. - -.[note] -Obiectele lazy pot fi utilizate doar pentru clasele utilizatorului, nu și pentru clasele interne PHP. Necesită PHP 8.4 sau o versiune mai recentă. - - -Export metadate ---------------- - -Clasa containerului DI conține și multe metadate. Puteți reduce dimensiunea acesteia prin reducerea exportului de metadate. - -```neon -di: - export: - # exportă parametrii? - parameters: false # (bool) implicit este true - - # exportă tag-urile și care anume? - tags: # (string[]|bool) implicit sunt toate - - event.subscriber - - # exportă datele pentru autowiring și care anume? - types: # (string[]|bool) implicit sunt toate - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Dacă nu utilizați array-ul `$container->getParameters()`, puteți dezactiva exportul parametrilor. În plus, puteți exporta doar acele tag-uri prin care obțineți servicii folosind metoda `$container->findByTag(...)`. Dacă nu apelați deloc metoda, puteți dezactiva complet exportul tag-urilor folosind `false`. - -Puteți reduce semnificativ metadatele pentru [autowiring |autowiring] specificând clasele pe care le utilizați ca parametru al metodei `$container->getByType()`. Și din nou, dacă nu apelați deloc metoda (respectiv doar în [bootstrap |application:bootstrapping] pentru a obține `Nette\Application\Application`), puteți dezactiva complet exportul folosind `false`. - - -Extensii -======== - -Înregistrarea altor extensii DI. În acest fel adăugăm, de exemplu, extensia DI `Dibi\Bridges\Nette\DibiExtension22` sub numele `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Ulterior, o configurăm în secțiunea `dibi`: - -```neon -dibi: - host: localhost -``` - -Ca extensie se poate adăuga și o clasă care are parametri: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Includerea fișierelor -===================== - -Putem include alte fișiere de configurare în secțiunea `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Numele `parameters.php` nu este o greșeală de tipar, configurația poate fi scrisă și într-un fișier PHP, care o returnează ca array: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Dacă în fișierele de configurare apar elemente cu aceleași chei, acestea vor fi suprascrise sau, în cazul [array-urilor, combinate |#Combinare]. Fișierul inclus ulterior are prioritate mai mare decât cel anterior. Fișierul în care este specificată secțiunea `includes` are prioritate mai mare decât fișierele incluse în el. - - -Search -====== - -Adăugarea automată a serviciilor în containerul DI face munca extrem de plăcută. Nette adaugă automat presenterii în container, dar se pot adăuga ușor și orice alte clase. - -Este suficient să specificați în ce directoare (și subdirectoare) trebuie căutate clasele: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -De obicei, însă, nu dorim să adăugăm absolut toate clasele și interfețele, așa că le putem filtra: - -```neon -search: - - in: %appDir%/Forms - - # filtrare după numele fișierului (string|string[]) - files: - - *Factory.php - - # filtrare după numele clasei (string|string[]) - classes: - - *Factory -``` - -Sau putem selecta clase care moștenesc sau implementează cel puțin una dintre clasele specificate: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Se pot defini și reguli de excludere, adică măști pentru numele clasei sau strămoși ereditari, care, dacă se potrivesc, serviciul nu se adaugă în containerul DI: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Tuturor serviciilor li se pot seta tag-uri: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Combinare -========= - -Dacă în mai multe fișiere de configurare apar elemente cu aceleași chei, acestea vor fi suprascrise sau, în cazul array-urilor, combinate. Fișierul inclus ulterior are prioritate mai mare decât cel anterior. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>rezultat</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Pentru array-uri, se poate preveni combinarea specificând un semn de exclamare după numele cheii: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>rezultat</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Configurarea Injecției de Dependență}} diff --git a/dependency-injection/ro/container.texy b/dependency-injection/ro/container.texy deleted file mode 100644 index 2a47b7c357..0000000000 --- a/dependency-injection/ro/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Ce este un container DI? -************************ - -.[perex] -Containerul de injecție de dependență (DIC) este o clasă care poate instanția și configura obiecte. - -Poate vă va surprinde, dar în multe cazuri nu aveți nevoie de un container de injecție de dependență pentru a beneficia de avantajele injecției de dependență (pe scurt DI). Până la urmă, chiar și în [capitolul introductiv |introduction] am arătat DI pe exemple concrete și nu a fost nevoie de niciun container. - -Cu toate acestea, dacă trebuie să gestionați un număr mare de obiecte diferite cu multe dependențe, un container de injecție de dependență va fi cu adevărat util. Ceea ce este cazul aplicațiilor web construite pe un framework. - -În capitolul anterior, am prezentat clasele `Article` și `UserController`. Ambele au anumite dependențe, și anume baza de date și factory-ul `ArticleFactory`. Și pentru aceste clase vom crea acum un container. Desigur, pentru un exemplu atât de simplu nu are sens să avem un container. Dar îl vom crea pentru a arăta cum arată și cum funcționează. - -Iată un container simplu hardcodat pentru exemplul dat: - -```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()); - } -} -``` - -Utilizarea ar arăta astfel: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Întrebăm doar containerul despre obiect și nu mai trebuie să știm nimic despre cum să îl creăm și ce dependențe are; containerul știe toate acestea. Dependențele sunt injectate automat de container. Aici stă puterea sa. - -Containerul are deocamdată toate datele scrise hardcodat. Vom face deci următorul pas și vom adăuga parametri pentru ca containerul să fie cu adevărat util: - -```php -class Container -{ - public function __construct( - private array $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' => '***', -]); -``` - -Cititorii atenți ar fi putut observa o anumită problemă. De fiecare dată când obțin obiectul `UserController`, se creează și o nouă instanță `ArticleFactory` și a bazei de date. Cu siguranță nu dorim acest lucru. - -Vom adăuga deci metoda `getService()`, care va returna mereu aceleași instanțe: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') va apela createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -La primul apel, de ex. `$container->getService('Database')`, va lăsa `createDatabase()` să creeze obiectul bazei de date, pe care îl va stoca în array-ul `$services` și la următorul apel îl va returna direct. - -Modificăm și restul containerului pentru a utiliza `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Apropo, termenul serviciu se referă la orice obiect gestionat de container. De aceea și numele metodei `getService()`. - -Gata. Avem un container DI complet funcțional! Și îl putem folosi: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -După cum puteți vedea, scrierea unui DIC nu este nimic complicat. Merită menționat că obiectele în sine nu știu că sunt create de vreun container. Astfel, este posibil să se creeze în acest mod orice obiect în PHP fără a interveni în codul său sursă. - -Crearea și întreținerea manuală a clasei containerului poate deveni destul de repede un coșmar. De aceea, în capitolul următor vom vorbi despre [Containerul Nette DI |nette-container], care se poate genera și actualiza aproape singur. - - -{{maintitle: Ce este un container de injecție de dependență?}} diff --git a/dependency-injection/ro/extensions.texy b/dependency-injection/ro/extensions.texy deleted file mode 100644 index 016ab80c2d..0000000000 --- a/dependency-injection/ro/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Crearea extensiilor pentru Nette DI -*********************************** - -.[perex] -Generarea containerului DI, pe lângă fișierele de configurare, este influențată și de așa-numitele *extensii*. Le activăm în fișierul de configurare în secțiunea `extensions`. - -Astfel adăugăm extensia reprezentată de clasa `BlogExtension` sub numele `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Fiecare extensie a compilatorului moștenește de la [api:Nette\DI\CompilerExtension] și poate implementa următoarele metode, care sunt apelate succesiv în timpul construirii containerului DI: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Această metodă este apelată prima. Definește schema pentru validarea parametrilor de configurare. - -Configurăm extensia în secțiunea al cărei nume este același cu cel sub care a fost adăugată extensia, adică `blog`: - -```neon -# același nume ca extensia -blog: - postsPerPage: 10 - allowComments: false -``` - -Creăm o schemă care descrie toate opțiunile de configurare, inclusiv tipurile lor, valorile permise și, eventual, valorile implicite: - -```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), - ]); - } -} -``` - -Documentația o găsiți pe pagina [Schema |schema:]. În plus, se poate specifica ce opțiuni pot fi [dinamice |application:bootstrapping#Parametri dinamici] folosind `dynamic()`, de ex. `Expect::int()->dynamic()`. - -Accesăm configurația prin variabila `$this->config`, care este un obiect `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Se utilizează pentru adăugarea serviciilor în container. Pentru aceasta se folosește [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']) // sau setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -Convenția este de a prefixa serviciile adăugate de extensie cu numele său, pentru a evita conflictele de nume. Acest lucru îl face metoda `prefix()`, deci dacă extensia se numește `blog`, serviciul va purta numele `blog.articles`. - -Dacă trebuie să redenumim un serviciu, putem crea un alias cu numele original pentru a menține compatibilitatea retroactivă. Nette face acest lucru similar, de exemplu, pentru serviciul `routing.router`, care este disponibil și sub numele anterior `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Încărcarea serviciilor din fișier ---------------------------------- - -Serviciile nu trebuie create doar folosind API-ul clasei ContainerBuilder, ci și prin sintaxa cunoscută utilizată în fișierul de configurare NEON în secțiunea services. Prefixul `@extension` reprezintă extensia curentă. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Încărcăm serviciile: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // încărcarea fișierului de configurare pentru extensie - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Metoda este apelată în momentul în care containerul conține toate serviciile adăugate de extensiile individuale în metodele `loadConfiguration` și, de asemenea, de fișierele de configurare ale utilizatorului. În această fază a construirii, putem deci modifica definițiile serviciilor sau completa legăturile dintre ele. Pentru căutarea serviciilor în container după tag-uri se poate utiliza metoda `findByTag()`, iar după clasă sau interfață, metoda `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] -======================== - -În această fază, clasa containerului este deja generată sub forma unui obiect [ClassType |php-generator:#Clase], conține toate metodele care creează servicii și este pregătită pentru scrierea în cache. Putem încă modifica codul rezultat al clasei în acest moment. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -Clasa Configurator, după [crearea containerului |application:bootstrapping#index.php], apelează codul de inițializare, care se creează prin scrierea în obiectul `$this->initialization` folosind [metoda addBody() |php-generator:#Corpuri de metode și funcții]. - -Vom arăta un exemplu despre cum, de exemplu, să pornim sesiunea cu codul de inițializare sau să rulăm servicii care au tag-ul `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // pornirea automată a sesiunii - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // serviciile cu tag-ul run trebuie create după instanțierea containerului - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/ro/factory.texy b/dependency-injection/ro/factory.texy deleted file mode 100644 index 540c2320d7..0000000000 --- a/dependency-injection/ro/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Factory-uri generate -******************** - -.[perex] -Nette DI poate genera automat codul factory-urilor pe baza interfețelor, ceea ce vă economisește scrierea codului. - -Un factory este o clasă care produce și configurează obiecte. Le transmite deci și dependențele lor. Vă rugăm să nu confundați cu pattern-ul de design *factory method*, care descrie un mod specific de utilizare a factory-urilor și nu are legătură cu acest subiect. - -Cum arată un astfel de factory am arătat în [capitolul introductiv |introduction#Fabrica]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI poate genera automat codul factory-urilor. Tot ce trebuie să faceți este să creați o interfață și Nette DI va genera implementarea. Interfața trebuie să aibă exact o metodă numită `create` și să declare tipul returnat: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Deci, factory-ul `ArticleFactory` are o metodă `create`, care creează obiecte `Article`. Clasa `Article` poate arăta, de exemplu, astfel: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Adăugăm factory-ul în fișierul de configurare: - -```neon -services: - - ArticleFactory -``` - -Nette DI va genera implementarea corespunzătoare a factory-ului. - -În codul care utilizează factory-ul, solicităm astfel obiectul conform interfeței și Nette DI va utiliza implementarea generată: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // lăsăm factory-ul să creeze obiectul - $article = $this->articleFactory->create(); - } -} -``` - - -Factory parametrizat -==================== - -Metoda factory `create` poate accepta parametri, pe care îi transmite apoi constructorului. Să completăm, de exemplu, clasa `Article` cu ID-ul autorului articolului: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Adăugăm parametrul și în factory: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Datorită faptului că parametrul din constructor și parametrul din factory se numesc la fel, Nette DI îi transmite complet automat. - - -Definiție avansată -================== - -Definiția poate fi scrisă și într-o formă multi-linie folosind cheia `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -La scrierea în această formă mai lungă, este posibil să se specifice argumente suplimentare pentru constructor în cheia `arguments` și configurație suplimentară folosind `setup`, la fel ca la serviciile obișnuite. - -Exemplu: dacă metoda `create()` nu ar accepta parametrul `$authorId`, am putea specifica o valoare fixă în configurație, care ar fi transmisă constructorului `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Sau invers, dacă `create()` ar accepta parametrul `$authorId`, dar acesta nu ar face parte din constructor și s-ar transmite prin metoda `Article::setAuthorId()`, ne-am referi la el în secțiunea `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Nette poate genera, pe lângă factory-uri, și așa-numiții accesori. Aceștia sunt obiecte cu o metodă `get()`, care returnează un anumit serviciu din containerul DI. Apelarea repetată a `get()` returnează mereu aceeași instanță. - -Accesorii oferă lazy-loading pentru dependențe. Să presupunem că avem o clasă care scrie erori într-o bază de date specială. Dacă această clasă ar primi conexiunea la baza de date ca dependență prin constructor, conexiunea ar trebui creată întotdeauna, deși în practică eroarea apare doar excepțional și, prin urmare, în majoritatea cazurilor conexiunea ar rămâne neutilizată. În schimb, clasa primește un accesor și abia atunci când se apelează `get()`, se creează obiectul bazei de date: - -Cum se creează un accesor? Este suficient să scrieți o interfață și Nette DI va genera implementarea. Interfața trebuie să aibă exact o metodă numită `get` și să declare tipul returnat: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Adăugăm accesorul în fișierul de configurare, unde este definit și serviciul pe care îl va returna: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Deoarece accesorul returnează un serviciu de tip `PDO` și în configurație există un singur astfel de serviciu, îl va returna tocmai pe acesta. Dacă ar exista mai multe servicii de tipul respectiv, specificăm serviciul returnat folosind numele, de ex. `- PDOAccessor(@db1)`. - - -Factory/Accesor multiplu -======================== -Factory-urile și accesorii noștri au putut până acum să producă sau să returneze doar un singur obiect. Dar se pot crea foarte ușor și factory-uri multiple combinate cu accesori. Interfața unei astfel de clase va conține un număr arbitrar de metode cu numele `create<name>()` și `get<name>()`, de ex.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Deci, în loc să transmitem mai multe factory-uri și accesori generați, transmitem un factory mai complex care poate face mai multe lucruri. - -Alternativ, în loc de mai multe metode, se poate folosi `get()` cu un parametru: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Atunci este valabil că `MultiFactory::getArticle()` face același lucru ca `MultiFactoryAlt::get('article')`. Cu toate acestea, scrierea alternativă are dezavantajul că nu este evident ce valori `$name` sunt suportate și, logic, nici nu se pot distinge în interfață diferite valori returnate pentru diferite `$name`. - - -Definiție prin listă --------------------- -În acest mod se poate defini un factory multiplu în configurație: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # definește createArticle() - db: PDO(%dsn%, %user%, %password%) # definește getDb() - ) -``` - -Sau ne putem referi în definiția factory-ului la servicii existente folosind o referință: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # definește createArticle() - db: @\PDO # definește getDb() - ) -``` - - -Definiție prin tag-uri ----------------------- - -A doua opțiune este utilizarea [tag-urilor |services#Tag-uri] pentru definire: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/ro/faq.texy b/dependency-injection/ro/faq.texy deleted file mode 100644 index 241a7f3c64..0000000000 --- a/dependency-injection/ro/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Întrebări frecvente despre DI (FAQ) -*********************************** - - -Este DI un alt nume pentru IoC? -------------------------------- - -*Inversion of Control* (IoC) este un principiu axat pe modul în care este executat codul - dacă codul dvs. rulează cod străin sau dacă codul dvs. este integrat în cod străin, care îl apelează ulterior. IoC este un termen larg care include [evenimente |nette:glossary#Evenimente], așa-numitul [Principiu Hollywood |application:components#Stilul Hollywood] și alte aspecte. Parte a acestui concept sunt și factory-urile, despre care vorbește [Regula nr. 3: lasă pe seama factory-ului |introduction#Regula nr. 3: Lasă pe seama fabricii], și care reprezintă o inversiune pentru operatorul `new`. - -*Dependency Injection* (DI) se concentrează pe modul în care un obiect află despre alt obiect, adică despre dependențele sale. Este un pattern de design care necesită transmiterea explicită a dependențelor între obiecte. - -Se poate deci spune că DI este o formă specifică de IoC. Cu toate acestea, nu toate formele de IoC sunt potrivite din punct de vedere al curățeniei codului. De exemplu, printre anti-pattern-uri se numără tehnicile care lucrează cu [starea globală |global-state] sau așa-numitul [Service Locator |#Ce este Service Locator]. - - -Ce este Service Locator? ------------------------- - -Este o alternativă la Dependency Injection. Funcționează prin crearea unui depozit central unde sunt înregistrate toate serviciile sau dependențele disponibile. Când un obiect are nevoie de o dependență, o solicită de la Service Locator. - -Cu toate acestea, în comparație cu Dependency Injection, pierde din transparență: dependențele nu sunt transmise direct obiectelor și nu sunt la fel de ușor de identificat, ceea ce necesită examinarea codului pentru a descoperi și înțelege toate legăturile. Testarea este, de asemenea, mai complicată, deoarece nu putem transmite pur și simplu obiecte mock obiectelor testate, ci trebuie să trecem prin Service Locator. În plus, Service Locator perturbă designul codului, deoarece obiectele individuale trebuie să știe despre existența sa, ceea ce diferă de Dependency Injection, unde obiectele nu au cunoștință despre containerul DI. - - -Când este mai bine să nu folosim DI? ------------------------------------- - -Nu sunt cunoscute dificultăți asociate cu utilizarea pattern-ului de design Dependency Injection. Dimpotrivă, obținerea dependențelor din locații disponibile global duce la [o întreagă serie de complicații |global-state], la fel ca și utilizarea Service Locator-ului. Prin urmare, este recomandat să se utilizeze DI întotdeauna. Aceasta nu este o abordare dogmatică, ci pur și simplu nu a fost găsită o alternativă mai bună. - -Cu toate acestea, există anumite situații în care nu transmitem obiecte și le obținem din spațiul global. De exemplu, la depanarea codului, când trebuie să afișați valoarea unei variabile într-un anumit punct al programului, să măsurați durata unei anumite părți a programului sau să înregistrați un mesaj. În astfel de cazuri, când este vorba de acțiuni temporare care vor fi ulterior eliminate din cod, este legitim să se utilizeze un dumper, un cronometru sau un logger disponibil global. Aceste instrumente nu fac parte din designul codului. - - -Are utilizarea DI dezavantaje? ------------------------------- - -Implică utilizarea Dependency Injection vreun dezavantaj, cum ar fi o complexitate crescută a scrierii codului sau o performanță redusă? Ce pierdem când începem să scriem cod în conformitate cu DI? - -DI nu are impact asupra performanței sau a cerințelor de memorie ale aplicației. Performanța containerului DI poate juca un anumit rol, însă în cazul [Nette DI |nette-container], containerul este compilat în PHP pur, astfel încât overhead-ul său în timpul rulării aplicației este practic nul. - -La scrierea codului, este necesar să se creeze constructori care acceptă dependențe. În trecut, acest lucru putea fi anevoios, însă datorită IDE-urilor moderne și [promovării proprietăților constructorului |https://blog.nette.org/ro/php-8-0-complete-overview-of-news#toc-constructor-property-promotion], acum este o chestiune de câteva secunde. Factory-urile pot fi generate ușor folosind Nette DI și plugin-ul pentru PhpStorm printr-un clic de mouse. Pe de altă parte, dispare necesitatea de a scrie singleton-uri și puncte de acces statice. - -Se poate constata că o aplicație proiectată corect care utilizează DI nu este nici mai scurtă, nici mai lungă în comparație cu o aplicație care utilizează singleton-uri. Părțile de cod care lucrează cu dependențe sunt doar extrase din clasele individuale și mutate în locații noi, adică în containerul DI și în factory-uri. - - -Cum să rescrii o aplicație legacy la DI? ----------------------------------------- - -Trecerea de la o aplicație legacy la Dependency Injection poate fi un proces solicitant, în special pentru aplicații mari și complexe. Este important să abordați acest proces sistematic. - -- La trecerea la Dependency Injection, este important ca toți membrii echipei să înțeleagă principiile și procedurile utilizate. -- Mai întâi, efectuați o analiză a aplicației existente și identificați componentele cheie și dependențele lor. Creați un plan care să specifice ce părți vor fi refactorizate și în ce ordine. -- Implementați un container DI sau, și mai bine, utilizați o bibliotecă existentă, de exemplu Nette DI. -- Refactorizați treptat părțile individuale ale aplicației pentru a utiliza Dependency Injection. Acest lucru poate include modificarea constructorilor sau metodelor astfel încât să accepte dependențe ca parametri. -- Modificați locurile din cod unde se creează obiecte cu dependențe, astfel încât dependențele să fie injectate de container. Acest lucru poate include utilizarea factory-urilor. - -Rețineți că trecerea la Dependency Injection este o investiție în calitatea codului și în mentenabilitatea pe termen lung a aplicației. Deși poate fi dificil să efectuați aceste modificări, rezultatul ar trebui să fie un cod mai curat, mai modular și mai ușor de testat, pregătit pentru extinderi și întreținere viitoare. - - -De ce se preferă compoziția în locul moștenirii? ------------------------------------------------- -Este mai potrivit să se utilizeze [compoziția |nette:introduction-to-object-oriented-programming#Compoziție] în locul [moștenirii |nette:introduction-to-object-oriented-programming#Moștenire], deoarece servește la reutilizarea codului fără a ne preocupa de consecințele modificărilor. Oferă deci o legătură mai slabă, în care nu trebuie să ne temem că modificarea unui cod va necesita modificarea altui cod dependent. Un exemplu tipic este situația denumită [constructor hell |passing-dependencies#Constructor hell]. - - -Se poate utiliza Nette DI Container în afara Nette? ---------------------------------------------------- - -Categoric. Nette DI Container face parte din Nette, dar este proiectat ca o bibliotecă independentă care poate fi utilizată independent de celelalte părți ale framework-ului. Este suficient să o instalați folosind Composer, să creați un fișier de configurare cu definiția serviciilor dvs. și apoi, folosind câteva linii de cod PHP, să creați containerul DI. Și puteți începe imediat să beneficiați de avantajele Dependency Injection în proiectele dvs. - -Modul concret de utilizare, inclusiv codurile, este descris în capitolul [Containerul Nette DI |nette-container]. - - -De ce este configurația în fișiere NEON? ----------------------------------------- - -NEON este un limbaj de configurare simplu și ușor de citit, care a fost dezvoltat în cadrul Nette pentru setarea aplicațiilor, serviciilor și dependențelor lor. În comparație cu JSON sau YAML, oferă opțiuni mult mai intuitive și flexibile în acest scop. În NEON se pot descrie natural legături care în Symfony & YAMLu nu ar putea fi scrise fie deloc, fie doar printr-o descriere complicată. - - -Nu încetinește aplicația parsarea fișierelor NEON? --------------------------------------------------- - -Deși fișierele NEON se parsează foarte rapid, acest aspect nu contează deloc. Motivul este că parsarea fișierelor are loc doar o singură dată la prima rulare a aplicației. Apoi se generează codul containerului DI, se salvează pe disc și se rulează la fiecare request ulterior, fără a fi necesară o altă parsare. - -Așa funcționează în mediul de producție. În timpul dezvoltării, fișierele NEON se parsează de fiecare dată când conținutul lor se modifică, pentru ca dezvoltatorul să aibă mereu containerul DI actualizat. Parsarea în sine este, așa cum s-a spus, o chestiune de moment. - - -Cum accesez din clasa mea parametrii din fișierul de configurare? ------------------------------------------------------------------ - -Să ne amintim [Regula nr. 1: lasă-l să ți se transmită |introduction#Regula nr. 1: Primește ce ai nevoie]. Dacă o clasă necesită informații din fișierul de configurare, nu trebuie să ne gândim cum să ajungem la acele informații, ci pur și simplu le solicităm - de exemplu, prin constructorul clasei. Și realizăm transmiterea în fișierul de configurare. - -În acest exemplu, `%myParameter%` este un substituent pentru valoarea parametrului `myParameter`, care se transmite constructorului clasei `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Dacă doriți să transmiteți mai mulți parametri sau să utilizați autowiring, este recomandat [să împachetați parametrii într-un obiect |best-practices:passing-settings-to-presenters]. - - -Suportă Nette PSR-11: Container interface? ------------------------------------------- - -Nette DI Container nu suportă PSR-11 direct. Cu toate acestea, dacă aveți nevoie de interoperabilitate între Nette DI Container și biblioteci sau framework-uri care așteaptă PSR-11 Container Interface, puteți crea un [adaptor simplu |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], care va servi ca o punte între Nette DI Container și PSR-11. diff --git a/dependency-injection/ro/global-state.texy b/dependency-injection/ro/global-state.texy deleted file mode 100644 index 7008c14d33..0000000000 --- a/dependency-injection/ro/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Stare globală și singleton-uri -****************************** - -.[perex] -Avertisment: Următoarele construcții sunt un semn al unui cod prost proiectat: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` sau `static::$var` - -Apar unele dintre aceste construcții în codul dvs.? Atunci aveți ocazia să îl îmbunătățiți. Poate vă gândiți că sunt construcții obișnuite, pe care le vedeți poate chiar și în soluții demonstrative ale diverselor biblioteci și framework-uri. Dacă este așa, atunci designul codului lor nu este bun. - -Acum nu vorbim deloc despre vreo puritate academică. Toate aceste construcții au un lucru în comun: utilizează starea globală. Și aceasta are un impact distructiv asupra calității codului. Clasele mint despre dependențele lor. Codul devine imprevizibil. Încurcă programatorii și le reduce eficiența. - -În acest capitol vom explica de ce este așa și cum să evitați starea globală. - - -Cuplare globală ---------------- - -Într-o lume ideală, un obiect ar trebui să poată comunica doar cu obiectele care i-au fost [transmise direct |passing-dependencies]. Dacă creez două obiecte `A` și `B` și nu transmit niciodată o referință între ele, atunci nici `A`, nici `B`, nu pot ajunge la celălalt obiect sau să îi modifice starea. Aceasta este o proprietate foarte dorită a codului. Este similar cu situația în care aveți o baterie și un bec; becul nu va lumina până nu îl conectați la baterie cu un fir. - -Dar acest lucru nu este valabil pentru variabilele globale (statice) sau singleton-uri. Obiectul `A` ar putea ajunge *fără fir* la obiectul `C` și să îl modifice fără nicio transmitere de referință, prin apelarea `C::changeSomething()`. Dacă obiectul `B` se agață și el de `C` global, atunci `A` și `B` se pot influența reciproc prin intermediul `C`. - -Utilizarea variabilelor globale introduce în sistem o nouă formă de cuplare *fără fir*, care nu este vizibilă din exterior. Creează o perdea de fum care complică înțelegerea și utilizarea codului. Pentru ca dezvoltatorii să înțeleagă cu adevărat dependențele, trebuie să citească fiecare linie de cod sursă. În loc să se familiarizeze pur și simplu cu interfața claselor. Mai mult, este o cuplare complet inutilă. Starea globală se folosește deoarece este ușor accesibilă de oriunde și permite, de exemplu, scrierea în baza de date prin metoda globală (statică) `DB::insert()`. Dar, așa cum vom arăta, avantajul pe care îl aduce este nesemnificativ, în timp ce complicațiile pe care le provoacă sunt fatale. - -.[note] -Din punct de vedere comportamental, nu există nicio diferență între o variabilă globală și una statică. Sunt la fel de dăunătoare. - - -Acțiune înfricoșătoare la distanță ----------------------------------- - -"Acțiune înfricoșătoare la distanță" - așa a numit celebrul Albert Einstein în 1935 un fenomen din fizica cuantică care îi dădea fiori. -Este vorba despre inseparabilitatea cuantică, a cărei particularitate este că atunci când măsori informația despre o particulă, influențezi instantaneu cealaltă particulă, chiar dacă sunt la milioane de ani-lumină distanță. Ceea ce pare să încalce legea fundamentală a universului, că nimic nu se poate propaga mai repede decât lumina. - -În lumea software, putem numi "acțiune înfricoșătoare la distanță" situația în care pornim un proces despre care credem că este izolat (deoarece nu i-am transmis nicio referință), dar în locuri îndepărtate ale sistemului apar interacțiuni neașteptate și modificări de stare despre care nu aveam nicio idee. Acest lucru se poate întâmpla doar prin intermediul stării globale. - -Imaginați-vă că vă alăturați unei echipe de dezvoltatori ai unui proiect care are o bază de cod extinsă și matură. Noul dvs. șef vă cere să implementați o nouă funcționalitate și, ca un dezvoltator bun, începeți prin scrierea unui test. Dar, fiind nou în proiect, faceți multe teste exploratorii de tipul "ce se întâmplă dacă apelez această metodă". Și încercați să scrieți următorul test: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // numărul cardului dvs. - $cc->charge(100); -} -``` - -Rulați codul, poate de mai multe ori, și după un timp observați pe mobil notificări de la bancă că la fiecare rulare s-au retras 100 de dolari de pe cardul dvs. de plată 🤦‍♂️ - -Cum naiba a putut testul să provoace retragerea reală de bani? Operarea cu un card de plată nu este ușoară. Trebuie să comunicați cu un serviciu web terț, trebuie să cunoașteți URL-ul acestui serviciu web, trebuie să vă autentificați și așa mai departe. Nicio informație de acest gen nu este conținută în test. Mai rău, nici măcar nu știți unde sunt prezente aceste informații și, prin urmare, nici cum să mock-uiți dependențele externe, astfel încât fiecare rulare să nu ducă la retragerea din nou a 100 de dolari. Și cum trebuia să știți, ca dezvoltator nou, că ceea ce urmați să faceți va duce la sărăcirea cu 100 de dolari? - -Aceasta este acțiunea înfricoșătoare la distanță! - -Nu vă rămâne decât să scormoniți îndelung în multe coduri sursă, să întrebați colegii mai vechi și mai experimentați, până când înțelegeți cum funcționează legăturile în proiect. Acest lucru este cauzat de faptul că, privind interfața clasei `CreditCard`, nu se poate identifica starea globală care trebuie inițializată. Nici măcar privirea în codul sursă al clasei nu vă dezvăluie ce metodă de inițializare trebuie să apelați. În cel mai bun caz, puteți găsi o variabilă globală la care se accesează și din ea să încercați să ghiciți cum să o inițializați. - -Clasele dintr-un astfel de proiect sunt mincinoși patologici. Cardul de plată pretinde că este suficient să îl instanțiați și să apelați metoda `charge()`. În secret, însă, colaborează cu o altă clasă `PaymentGateway`, care reprezintă poarta de plată. Și interfața sa spune că poate fi inițializată separat, dar în realitate își extrage credențialele dintr-un fișier de configurare și așa mai departe. Dezvoltatorilor care au scris acest cod le este clar că `CreditCard` are nevoie de `PaymentGateway`. Au scris codul în acest fel. Dar pentru oricine este nou în proiect, este un mister total și împiedică învățarea. - -Cum să reparați situația? Ușor. **Lăsați API-ul să declare dependențele.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Observați cum legăturile din interiorul codului devin brusc evidente. Prin faptul că metoda `charge()` declară că are nevoie de `PaymentGateway`, nu trebuie să întrebați pe nimeni cum este legat codul. Știți că trebuie să creați instanța sa și, când încercați să faceți acest lucru, veți descoperi că trebuie să furnizați parametrii de acces. Fără ei, codul nici măcar nu ar rula. - -Și, cel mai important, acum puteți mock-ui poarta de plată, astfel încât să nu vi se taxeze 100 de dolari la fiecare rulare a testului. - -Starea globală face ca obiectele dvs. să poată accesa în secret lucruri care nu sunt declarate în API-ul lor și, în consecință, transformă API-urile dvs. în mincinoși patologici. - -Poate că nu v-ați gândit la asta înainte în acest fel, dar ori de câte ori utilizați starea globală, creați canale de comunicare secrete fără fir. Acțiunea înfricoșătoare la distanță îi obligă pe dezvoltatori să citească fiecare linie de cod pentru a înțelege interacțiunile potențiale, reduce productivitatea dezvoltatorilor și îi încurcă pe noii membri ai echipei. Dacă sunteți cel care a creat codul, cunoașteți dependențele reale, dar oricine vine după dvs. este neajutorat. - -Nu scrieți cod care utilizează starea globală, preferați transmiterea dependențelor. Adică injecția de dependență. - - -Fragilitatea stării globale ---------------------------- - -În codul care utilizează starea globală și singleton-uri, nu este niciodată sigur când și cine a modificat această stare. Acest risc apare deja la inițializare. Următorul cod ar trebui să creeze o conexiune la baza de date și să inițializeze poarta de plată, însă aruncă constant o excepție și găsirea cauzei este extrem de anevoioasă: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Trebuie să parcurgeți codul în detaliu pentru a descoperi că obiectul `PaymentGateway` accesează fără fir alte obiecte, dintre care unele necesită o conexiune la baza de date. Prin urmare, este necesar să inițializați baza de date înainte de `PaymentGateway`. Cu toate acestea, perdeaua de fum a stării globale ascunde acest lucru de dvs. Cât timp ați economisi dacă API-urile claselor individuale nu ar minți și și-ar declara dependențele? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -O problemă similară apare și la utilizarea accesului global la conexiunea bazei de date: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -La apelarea metodei `save()`, nu este sigur dacă a fost deja creată conexiunea la baza de date și cine poartă responsabilitatea pentru crearea sa. Dacă dorim, de exemplu, să schimbăm conexiunea la baza de date în timpul rulării, de exemplu pentru teste, ar trebui probabil să creăm alte metode precum `DB::reconnect(...)` sau `DB::reconnectForTest()`. - -Să luăm în considerare un exemplu: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Unde avem certitudinea că la apelarea `$article->save()` se utilizează într-adevăr baza de date de test? Ce se întâmplă dacă metoda `Foo::doSomething()` a schimbat conexiunea globală la baza de date? Pentru a afla, ar trebui să examinăm codul sursă al clasei `Foo` și probabil și al multor altor clase. Această abordare ar aduce însă doar un răspuns pe termen scurt, deoarece situația se poate schimba în viitor. - -Și ce se întâmplă dacă mutăm conexiunea la baza de date într-o variabilă statică în interiorul clasei `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Acest lucru nu a schimbat absolut nimic. Problema este starea globală și este complet irelevant în ce clasă se ascunde. În acest caz, la fel ca în cel precedent, nu avem niciun indiciu la apelarea metodei `$article->save()` despre în ce bază de date se va scrie. Oricine de la celălalt capăt al aplicației ar fi putut schimba oricând baza de date folosind `Article::setDb()`. Sub nasul nostru. - -Starea globală face aplicația noastră **extrem de fragilă**. - -Există însă o modalitate simplă de a aborda această problemă. Este suficient să lăsăm API-ul să declare dependențele, asigurându-se astfel funcționalitatea corectă. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Datorită acestei abordări, dispare teama de modificări ascunse și neașteptate ale conexiunii la baza de date. Acum avem certitudinea unde se salvează articolul și nicio modificare a codului în interiorul altei clase nelegate nu mai poate schimba situația. Codul nu mai este fragil, ci stabil. - -Nu scrieți cod care utilizează starea globală, preferați transmiterea dependențelor. Adică injecția de dependență. - - -Singleton ---------- - -Singleton este un pattern de design care, conform "definiției":https://en.wikipedia.org/wiki/Singleton_pattern din celebra publicație Gang of Four, limitează clasa la o singură instanță și oferă acces global la aceasta. Implementarea acestui pattern seamănă de obicei cu următorul cod: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // și alte metode care îndeplinesc funcțiile clasei respective -} -``` - -Din păcate, singleton introduce starea globală în aplicație. Și, așa cum am arătat mai sus, starea globală este nedorită. Prin urmare, singleton este considerat un antipattern. - -Nu utilizați singleton-uri în codul dvs. și înlocuiți-le cu alte mecanisme. Chiar nu aveți nevoie de singleton-uri. Cu toate acestea, dacă trebuie să garantați existența unei singure instanțe a clasei pentru întreaga aplicație, lăsați acest lucru pe seama [containerului DI |container]. Creați astfel un singleton de aplicație, adică un serviciu. Astfel, clasa încetează să se mai ocupe de asigurarea propriei unicități (adică nu va avea metoda `getInstance()` și variabila statică) și va îndeplini doar funcțiile sale. Astfel, nu va mai încălca principiul responsabilității unice. - - -Stare globală versus teste --------------------------- - -La scrierea testelor, presupunem că fiecare test este o unitate izolată și că nicio stare externă nu intră în el. Și nicio stare nu părăsește testele. După finalizarea testului, toată starea asociată cu testul ar trebui eliminată automat de garbage collector. Datorită acestui fapt, testele sunt izolate. Prin urmare, putem rula testele în orice ordine. - -Cu toate acestea, dacă sunt prezente stări globale/singleton-uri, toate aceste presupuneri plăcute se destramă. Starea poate intra și ieși din test. Brusc, ordinea testelor poate conta. - -Pentru a putea testa singleton-urile, dezvoltatorii trebuie adesea să le relaxeze proprietățile, de exemplu, permițând înlocuirea instanței cu alta. Astfel de soluții sunt, în cel mai bun caz, hack-uri care creează cod dificil de întreținut și de înțeles. Fiecare test sau metodă `tearDown()` care afectează orice stare globală trebuie să anuleze aceste modificări. - -Starea globală este cea mai mare durere de cap la testarea unitară! - -Cum să reparați situația? Ușor. Nu scrieți cod care utilizează singleton-uri, preferați transmiterea dependențelor. Adică injecția de dependență. - - -Constante globale ------------------ - -Starea globală nu se limitează doar la utilizarea singleton-urilor și a variabilelor statice, ci se poate referi și la constantele globale. - -Constantele a căror valoare nu ne aduce nicio informație nouă (`M_PI`) sau utilă (`PREG_BACKTRACK_LIMIT_ERROR`) sunt în mod clar în regulă. Dimpotrivă, constantele care servesc ca o modalitate de a transmite *fără fir* informații în interiorul codului nu sunt altceva decât o dependență ascunsă. Cum ar fi `LOG_FILE` în exemplul următor. Utilizarea constantei `FILE_APPEND` este complet corectă. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -În acest caz, ar trebui să declarăm un parametru în constructorul clasei `Foo`, pentru ca acesta să devină parte a API-ului: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Acum putem transmite informația despre calea către fișierul de logare și o putem schimba ușor după nevoie, ceea ce facilitează testarea și întreținerea codului. - - -Funcții globale și metode statice ---------------------------------- - -Dorim să subliniem că utilizarea în sine a metodelor statice și a funcțiilor globale nu este problematică. Am explicat în ce constă inadecvarea utilizării `DB::insert()` și a metodelor similare, dar întotdeauna a fost vorba doar de o chestiune de stare globală, care este stocată într-o variabilă statică. Metoda `DB::insert()` necesită existența unei variabile statice, deoarece în ea este stocată conexiunea la baza de date. Fără această variabilă, ar fi imposibil să se implementeze metoda. - -Utilizarea metodelor statice și a funcțiilor deterministe, precum `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` și multe altele, este în perfectă concordanță cu injecția de dependență. Aceste funcții returnează întotdeauna aceleași rezultate pentru aceiași parametri de intrare și sunt deci previzibile. Nu utilizează nicio stare globală. - -Există însă și funcții în PHP care nu sunt deterministe. Printre acestea se numără, de exemplu, funcția `htmlspecialchars()`. Al treilea său parametru `$encoding`, dacă nu este specificat, are ca valoare implicită valoarea opțiunii de configurare `ini_get('default_charset')`. De aceea se recomandă specificarea întotdeauna a acestui parametru și prevenirea astfel a unui eventual comportament imprevizibil al funcției. Nette face acest lucru în mod consecvent. - -Unele funcții, precum `strtolower()`, `strtoupper()` și altele similare, s-au comportat nedeterminist în trecutul recent și au fost dependente de setarea `setlocale()`. Acest lucru a cauzat multe complicații, cel mai adesea la lucrul cu limba turcă. Aceasta distinge literele mici și mari `I` cu și fără punct. Astfel, `strtolower('I')` returna caracterul `ı` și `strtoupper('i')` caracterul `İ`, ceea ce a dus la faptul că aplicațiile au început să provoace o serie de erori misterioase. Această problemă a fost însă eliminată în PHP versiunea 8.2 și funcțiile nu mai sunt dependente de locale. - -Este un exemplu frumos despre cum starea globală a chinuit mii de dezvoltatori din întreaga lume. Soluția a fost înlocuirea sa cu injecția de dependență. - - -Când este posibil să se utilizeze starea globală? -------------------------------------------------- - -Există anumite situații specifice în care este posibil să se utilizeze starea globală. De exemplu, la depanarea codului, când trebuie să afișați valoarea unei variabile sau să măsurați durata unei anumite părți a programului. În astfel de cazuri, care se referă la acțiuni temporare ce vor fi ulterior eliminate din cod, este posibil să se utilizeze legitim un dumper sau un cronometru disponibil global. Aceste instrumente nu fac parte din designul codului. - -Un alt exemplu sunt funcțiile pentru lucrul cu expresii regulate `preg_*`, care stochează intern expresiile regulate compilate într-un cache static în memorie. Astfel, când apelați aceeași expresie regulată de mai multe ori în diferite locuri ale codului, aceasta se compilează o singură dată. Cache-ul economisește performanța și, în același timp, este complet invizibil pentru utilizator, prin urmare o astfel de utilizare poate fi considerată legitimă. - - -Rezumat -------- - -Am discutat de ce are sens: - -1) Să eliminați toate variabilele statice din cod -2) Să declarați dependențele -3) Și să utilizați injecția de dependență - -Când vă gândiți la designul codului, gândiți-vă că fiecare `static $foo` reprezintă o problemă. Pentru ca codul dvs. să fie un mediu care respectă DI, este necesar să eliminați complet starea globală și să o înlocuiți folosind injecția de dependență. - -În timpul acestui proces, este posibil să descoperiți că este necesar să împărțiți clasa, deoarece are mai mult de o responsabilitate. Nu vă temeți de acest lucru; urmăriți principiul responsabilității unice. - -*Aș dori să îi mulțumesc lui Miško Hevery, ale cărui articole, precum [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], stau la baza acestui capitol.* diff --git a/dependency-injection/ro/introduction.texy b/dependency-injection/ro/introduction.texy deleted file mode 100644 index 1d0be17a88..0000000000 --- a/dependency-injection/ro/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Ce este Dependency Injection? -***************************** - -.[perex] -Acest capitol vă va introduce în practicile de programare de bază pe care ar trebui să le urmați atunci când scrieți toate aplicațiile. Acestea sunt elementele de bază necesare pentru a scrie cod curat, ușor de înțeles și de întreținut. - -Dacă adoptați aceste reguli și le urmați, Nette vă va sprijini la fiecare pas. Se va ocupa de sarcinile de rutină pentru dvs. și vă va oferi confort maxim, astfel încât să vă puteți concentra pe logica în sine. - -Principiile pe care le vom arăta aici sunt destul de simple. Nu trebuie să vă faceți griji pentru nimic. - - -Vă amintiți primul program? ---------------------------- - -Nu știm în ce limbaj l-ați scris, dar dacă ar fi fost PHP, probabil ar fi arătat cam așa: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // afișează 24 -``` - -Câteva rânduri triviale de cod, dar conțin atât de multe concepte cheie. Că există variabile. Că codul este împărțit în unități mai mici, cum ar fi funcțiile. Că le transmitem argumente de intrare și ele returnează rezultate. Lipsesc doar condițiile și buclele. - -Faptul că transmitem date de intrare unei funcții și aceasta returnează un rezultat este un concept perfect de înțeles, care este utilizat și în alte domenii, cum ar fi matematica. - -O funcție are semnătura sa, care constă în numele său, o listă de parametri și tipurile acestora și, în final, tipul valorii returnate. Ca utilizatori, suntem interesați de semnătură, de obicei nu trebuie să știm nimic despre implementarea internă. - -Acum imaginați-vă că semnătura funcției ar arăta astfel: - -```php -function soucet(float $x): float -``` - -O sumă cu un singur parametru? Ciudat... Și ce ziceți de asta? - -```php -function soucet(): float -``` - -Asta e deja foarte ciudat, nu-i așa? Cum se folosește funcția? - -```php -echo soucet(); // ce va afișa oare? -``` - -Privind un astfel de cod, am fi confuzi. Nu numai că un începător nu l-ar înțelege, dar nici un programator experimentat nu înțelege un astfel de cod. - -Vă întrebați cum ar arăta de fapt o astfel de funcție în interior? De unde ar lua termenii? Probabil că i-ar obține *într-un fel* singură, poate așa: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -În corpul funcției am descoperit legături ascunse către alte funcții globale sau metode statice. Pentru a afla de unde provin de fapt termenii, trebuie să investigăm mai departe. - - -Nu pe aici! ------------ - -Designul pe care tocmai l-am arătat este esența multor caracteristici negative: - -- semnătura funcției pretindea că nu are nevoie de termeni, ceea ce ne-a indus în eroare -- nu știm deloc cum să facem funcția să adune alte două numere -- a trebuit să ne uităm în cod pentru a afla de unde ia termenii -- am descoperit dependențe ascunse -- pentru o înțelegere completă, este necesar să examinăm și aceste dependențe - -Și este oare sarcina funcției de adunare să obțină intrări? Desigur că nu. Responsabilitatea sa este doar adunarea în sine. - - -Nu vrem să întâlnim un astfel de cod și cu siguranță nu vrem să-l scriem. Remedierea este simplă: revenirea la elementele de bază și pur și simplu folosirea parametrilor: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Regula nr. 1: Primește ce ai nevoie ------------------------------------ - -Cea mai importantă regulă este: **toate datele de care funcțiile sau clasele au nevoie trebuie să le fie transmise**. - -În loc să inventați modalități ascunse prin care acestea ar putea ajunge cumva singure la ele, pur și simplu transmiteți parametrii. Veți economisi timp necesar pentru a inventa căi ascunse, care cu siguranță nu vă vor îmbunătăți codul. - -Dacă veți respecta această regulă întotdeauna și peste tot, sunteți pe drumul către un cod fără dependențe ascunse. Către un cod care este de înțeles nu numai pentru autor, ci și pentru oricine îl va citi după el. Unde totul este de înțeles din semnăturile funcțiilor și claselor și nu este nevoie să căutați secrete ascunse în implementare. - -Această tehnică se numește tehnic **dependency injection** (injectarea dependențelor). Iar acele date se numesc **dependențe.** De fapt, este vorba de transmiterea obișnuită a parametrilor, nimic mai mult. - -.[note] -Vă rugăm să nu confundați dependency injection, care este un model de design (design pattern), cu „container DI”, care este un instrument, adică ceva diametral opus. Vom discuta despre containere mai târziu. - - -De la funcții la clase ----------------------- - -Și cum se leagă clasele de asta? O clasă este o unitate mai complexă decât o funcție simplă, dar regula nr. 1 se aplică în totalitate și aici. Doar că există [mai multe opțiuni pentru a pasa argumente|passing-dependencies]. De exemplu, destul de similar cu cazul unei funcții: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Sau folosind alte metode, sau direct constructorul: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Ambele exemple sunt pe deplin în concordanță cu dependency injection. - - -Exemple reale -------------- - -În lumea reală, nu veți scrie clase pentru adunarea numerelor. Să trecem la exemple din practică. - -Să avem o clasă `Article` care reprezintă un articol de blog: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // salvăm articolul în baza de date - } -} -``` - -și utilizarea va fi următoarea: - -```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()` salvează articolul într-un tabel din baza de date. Implementarea acesteia cu ajutorul [Nette Database |database:] ar fi o joacă de copil, dacă n-ar fi o mică problemă: de unde obține `Article` conexiunea la baza de date, adică obiectul clasei `Nette\Database\Connection`? - -Se pare că avem multe opțiuni. Poate să o ia de undeva dintr-o variabilă statică. Sau să moștenească de la o clasă care asigură conexiunea la baza de date. Sau să utilizeze așa-numitul [singleton |global-state#Singleton]. Sau așa-numitele facades, care sunt utilizate în 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], - ); - } -} -``` - -Excelent, am rezolvat problema. - -Sau nu? - -Să ne amintim [##Regula nr. 1: Primește ce ai nevoie]: toate dependențele de care clasa are nevoie trebuie să-i fie transmise. Pentru că dacă încălcăm regula, am pornit pe calea către un cod murdar, plin de dependențe ascunse, neinteligibil, iar rezultatul va fi o aplicație pe care va fi dureros să o întreținem și să o dezvoltăm. - -Utilizatorul clasei `Article` nu știe unde metoda `save()` salvează articolul. Într-un tabel din baza de date? În care, cel de producție sau cel de test? Și cum se poate schimba asta? - -Utilizatorul trebuie să se uite cum este implementată metoda `save()` și găsește utilizarea metodei `DB::insert()`. Așa că trebuie să investigheze mai departe cum își obține această metodă conexiunea la baza de date. Iar dependențele ascunse pot forma un lanț destul de lung. - -Într-un cod curat și bine proiectat nu există niciodată dependențe ascunse, facades Laravel sau variabile statice. Într-un cod curat și bine proiectat se transmit argumente: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Și mai practic, așa cum vom vedea mai departe, va fi prin constructor: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Dacă sunteți un programator experimentat, poate vă gândiți că `Article` nu ar trebui să aibă deloc metoda `save()`, ar trebui să reprezinte o componentă pură de date, iar de salvare ar trebui să se ocupe un repository separat. Asta are sens. Dar astfel am depăși cu mult subiectul dependency injection și efortul de a oferi exemple simple. - -Dacă scrieți o clasă care necesită, de exemplu, o bază de date pentru funcționarea sa, nu vă gândiți de unde să o obțineți, ci lăsați să vă fie transmisă. De exemplu, ca parametru al constructorului sau al altei metode. Recunoașteți dependențele. Recunoașteți-le în API-ul clasei dvs. Veți obține un cod inteligibil și previzibil. - -Și ce ziceți de această clasă, care loghează mesajele de eroare: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Ce credeți, am respectat [##Regula nr. 1: Primește ce ai nevoie]? - -Nu am respectat-o. - -Informația cheie, adică directorul cu fișierul de log, clasa *o obține singură* dintr-o constantă. - -Uitați-vă la exemplul de utilizare: - -```php -$logger = new Logger; -$logger->log('Temperatura este 23 °C'); -$logger->log('Temperatura este 10 °C'); -``` - -Fără a cunoaște implementarea, ați putea răspunde la întrebarea unde se scriu mesajele? V-ați fi gândit că pentru funcționare este necesară existența constantei `LOG_DIR`? Și ați putea crea o a doua instanță care să scrie în altă parte? Cu siguranță nu. - -Să corectăm clasa: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Clasa este acum mult mai inteligibilă, configurabilă și, prin urmare, mai utilă. - -```php -$logger = new Logger('/cale/catre/log.txt'); -$logger->log('Temperatura este 15 °C'); -``` - - -Dar nu mă interesează! ----------------------- - -*„Când creez un obiect Article și apelez save(), nu vreau să mă ocup de baza de date, vreau doar să fie salvat în cea pe care o am setată în configurație.”* - -*„Când folosesc Logger, vreau doar ca mesajul să fie scris și nu vreau să mă ocup de unde. Să se folosească setarea globală.”* - -Acestea sunt observații corecte. - -Ca exemplu, vom arăta o clasă care distribuie newslettere și care loghează cum a decurs: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('E-mailurile au fost trimise'); - - } catch (Exception $e) { - $logger->log('A apărut o eroare la trimitere'); - throw $e; - } - } -} -``` - -`Logger`-ul îmbunătățit, care nu mai folosește constanta `LOG_DIR`, necesită specificarea căii către fișier în constructor. Cum rezolvăm asta? Clasa `NewsletterDistributor` nu este deloc interesată unde se scriu mesajele, vrea doar să le scrie. - -Soluția este din nou [##Regula nr. 1: Primește ce ai nevoie]: toate datele de care clasa are nevoie, i le transmitem. - -Deci asta înseamnă că transmitem calea către log prin constructor, pe care apoi o folosim la crearea obiectului `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ NU AȘA! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Nu așa! Calea **nu face parte** din datele de care are nevoie clasa `NewsletterDistributor`; de acestea are nevoie `Logger`. Percepeți diferența? Clasa `NewsletterDistributor` are nevoie de logger ca atare. Așa că îl vom transmite pe acesta: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('E-mailurile au fost trimise'); - - } catch (Exception $e) { - $this->logger->log('A apărut o eroare la trimitere'); - throw $e; - } - } -} -``` - -Acum, din semnăturile clasei `NewsletterDistributor` este clar că logarea face parte din funcționalitatea sa. Iar sarcina de a înlocui loggerul cu altul, de exemplu pentru testare, este complet trivială. Mai mult, dacă constructorul clasei `Logger` s-ar schimba, acest lucru nu ar avea niciun impact asupra clasei noastre. - - -Regula nr. 2: Ia doar ce este al tău ------------------------------------- - -Nu vă lăsați induși în eroare și nu vă lăsați să vi se transmită dependențele dependențelor voastre. Lăsați să vi se transmită doar dependențele voastre. - -Datorită acestui fapt, codul care utilizează alte obiecte va fi complet independent de modificările constructorilor acestora. API-ul său va fi mai veridic. Și, mai presus de toate, va fi trivial să înlocuiți aceste dependențe cu altele. - - -Un nou membru al familiei -------------------------- - -În echipa de dezvoltare s-a decis crearea unui al doilea logger, care scrie în baza de date. Vom crea deci clasa `DatabaseLogger`. Așadar, avem două clase, `Logger` și `DatabaseLogger`, una scrie într-un fișier, cealaltă în baza de date... nu vi se pare ceva ciudat la această denumire? Nu ar fi mai bine să redenumim `Logger` în `FileLogger`? Cu siguranță da. - -Dar o vom face inteligent. Sub numele original vom crea o interfață: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… pe care ambii loggeri o vor implementa: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -Și datorită acestui fapt, nu va fi nevoie să schimbăm nimic în restul codului unde se utilizează loggerul. De exemplu, constructorul clasei `NewsletterDistributor` va fi în continuare mulțumit că necesită `Logger` ca parametru. Și va depinde doar de noi ce instanță îi vom transmite. - -**De aceea nu adăugăm niciodată sufixul `Interface` sau prefixul `I` la numele interfețelor.** Altfel nu ar fi posibil să dezvoltăm codul atât de frumos. - - -Houston, avem o problemă ------------------------- - -În timp ce în întreaga aplicație ne putem descurca cu o singură instanță de logger, fie el de fișier sau de bază de date, și pur și simplu o transmitem oriunde se loghează ceva, situația este destul de diferită în cazul clasei `Article`. Instanțele sale le creăm după nevoie, chiar de mai multe ori. Cum să gestionăm dependența de baza de date în constructorul său? - -Ca exemplu poate servi un controller care, după trimiterea unui formular, trebuie să salveze articolul în baza de date: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -O posibilă soluție se oferă direct: lăsăm obiectul bazei de date să fie transmis prin constructor către `EditController` și folosim `$article = new Article($this->db)`. - -La fel ca în cazul anterior cu `Logger` și calea către fișier, aceasta nu este abordarea corectă. Baza de date nu este o dependență a `EditController`, ci a `Article`. Transmiterea bazei de date contravine deci [Regulii nr. 2: Ia doar ce este al tău |#Regula nr. 2: Ia doar ce este al tău]. Când se schimbă constructorul clasei `Article` (se adaugă un nou parametru), va fi necesar să se modifice și codul în toate locurile unde se creează instanțe. Ufff. - -Houston, ce propui? - - -Regula nr. 3: Lasă pe seama fabricii ------------------------------------- - -Prin eliminarea dependențelor ascunse și transmiterea tuturor dependențelor ca argumente, am obținut clase mai configurabile și mai flexibile. Și, prin urmare, avem nevoie de ceva în plus, care să ne creeze și să ne configureze acele clase mai flexibile. Le vom numi fabrici. - -Regula este: dacă o clasă are dependențe, lăsați crearea instanțelor sale pe seama unei fabrici. - -Fabricile sunt înlocuitori mai inteligenți ai operatorului `new` în lumea dependency injection. - -.[note] -Vă rugăm să nu confundați cu modelul de design (design pattern) *factory method*, care descrie un mod specific de utilizare a fabricilor și nu are legătură cu acest subiect. - - -Fabrica -------- - -O fabrică este o metodă sau o clasă care produce și configurează obiecte. Clasa care produce `Article` o vom numi `ArticleFactory` și ar putea arăta, de exemplu, astfel: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Utilizarea sa în controller va fi următoarea: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // lăsăm fabrica să creeze obiectul - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Dacă în acest moment se schimbă semnătura constructorului clasei `Article`, singura parte a codului care trebuie să reacționeze este însăși fabrica `ArticleFactory`. Tot restul codului care lucrează cu obiecte `Article`, cum ar fi `EditController`, nu va fi afectat în niciun fel. - -Poate vă bateți acum capul dacă ne-am ajutat cu ceva. Cantitatea de cod a crescut și totul începe să pară suspect de complicat. - -Nu vă faceți griji, în curând vom ajunge la containerul Nette DI. Și acesta are o serie de ași în mânecă, care simplifică enorm construirea aplicațiilor care utilizează dependency injection. De exemplu, în loc de clasa `ArticleFactory`, va fi suficient să [scrie doar o interfață |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Dar anticipăm, mai aveți puțină răbdare :-) - - -Rezumat -------- - -La începutul acestui capitol am promis că vom arăta o metodă de a proiecta cod curat. Este suficient ca claselor - -1) [să le transmitem dependențele de care au nevoie |#Regula nr. 1: Primește ce ai nevoie] -2) [și, dimpotrivă, să nu le transmitem ceea ce nu au nevoie direct |#Regula nr. 2: Ia doar ce este al tău] -3) [și că obiectele cu dependențe sunt cel mai bine create în fabrici |#Regula nr. 3: Lasă pe seama fabricii] - -Poate nu pare așa la prima vedere, dar aceste trei reguli au consecințe de anvergură. Conduc la o perspectivă radical diferită asupra designului codului. Merită? Programatorii care au renunțat la vechile obiceiuri și au început să utilizeze consecvent dependency injection consideră acest pas un moment crucial în viața lor profesională. Li s-a deschis lumea aplicațiilor clare și ușor de întreținut. - -Dar ce se întâmplă dacă codul nu utilizează consecvent dependency injection? Ce se întâmplă dacă este construit pe metode statice sau singleton-uri? Aduce asta probleme? [Aduce și foarte fundamentale |global-state]. diff --git a/dependency-injection/ro/nette-container.texy b/dependency-injection/ro/nette-container.texy deleted file mode 100644 index c076686fe6..0000000000 --- a/dependency-injection/ro/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI este una dintre cele mai interesante biblioteci Nette. Poate genera și actualiza automat containere DI compilate, care sunt extrem de rapide și uimitor de ușor de configurat. - -Forma serviciilor pe care containerul DI trebuie să le creeze o definim de obicei folosind fișiere de configurație în [format NEON|neon:format]. Containerul pe care l-am creat manual în [capitolul anterior|container] s-ar scrie astfel: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Notația este într-adevăr concisă. - -Toate dependențele declarate în constructorii claselor `ArticleFactory` și `UserController`, Nette DI le descoperă și le transmite singur datorită așa-numitului [autowiring |autowiring], de aceea nu este nevoie să se specifice nimic în fișierul de configurație. Astfel, chiar dacă parametrii se schimbă, nu trebuie să modificați nimic în configurație. Containerul Nette se regenerează automat. Vă puteți concentra astfel exclusiv pe dezvoltarea aplicației. - -Dacă dorim să transmitem dependențe folosind setteri, folosim secțiunea [setup |services#Setup] pentru aceasta. - -Nette DI generează direct cod PHP pentru container. Rezultatul este deci un fișier `.php`, pe care îl puteți deschide și studia. Datorită acestui fapt, vedeți exact cum funcționează containerul. Îl puteți de asemenea depana în IDE și parcurge pas cu pas. Și cel mai important: PHP-ul generat este extrem de rapid. - -Nette DI poate genera și cod pentru [fabrici|factory] pe baza interfeței furnizate. De aceea, în loc de clasa `ArticleFactory`, ne va fi suficient să creăm în aplicație doar o interfață: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Exemplul complet îl găsiți [pe GitHub|https://github.com/nette-examples/di-example-doc]. - - -Utilizare independentă ----------------------- - -Implementarea bibliotecii Nette DI într-o aplicație este foarte ușoară. Mai întâi o instalăm cu Composer (pentru că descărcarea arhivelor zip este așaaa de învechită): - -```shell -composer require nette/di -``` - -Următorul cod creează o instanță a containerului DI conform configurației stocate în fișierul `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Containerul se generează o singură dată, codul său se scrie în cache (directorul `__DIR__ . '/temp'`) și la cererile ulterioare se încarcă doar de aici. - -Pentru crearea și obținerea serviciilor se folosesc metodele `getService()` sau `getByType()`. Astfel creăm obiectul `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -În timpul dezvoltării este util să activăm modul auto-refresh, în care containerul se regenerează automat dacă se modifică orice clasă sau fișier de configurație. Este suficient să specificăm `true` ca al doilea argument în constructorul `ContainerLoader`. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Utilizare cu framework-ul Nette -------------------------------- - -Așa cum am arătat, utilizarea Nette DI nu este limitată la aplicațiile scrise în Nette Framework, îl puteți implementa oriunde cu doar 3 rânduri de cod. Dacă însă dezvoltați aplicații în Nette Framework, configurarea și crearea containerului sunt gestionate de [Bootstrap |application:bootstrapping#Configurarea containerului DI]. diff --git a/dependency-injection/ro/passing-dependencies.texy b/dependency-injection/ro/passing-dependencies.texy deleted file mode 100644 index 3d70d4acba..0000000000 --- a/dependency-injection/ro/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Transmiterea dependențelor -************************** - -<div class=perex> - -Argumentele, sau în terminologia DI „dependențele”, pot fi transmise claselor în următoarele moduri principale: - -* transmitere prin constructor -* transmitere prin metodă (așa-numitul setter) -* setarea proprietății (variabilei membru) -* prin metodă, adnotare sau atribut *inject* - -</div> - -Acum vom arăta fiecare variantă cu exemple concrete. - - -Transmitere prin constructor -============================ - -Dependențele sunt transmise în momentul creării obiectului ca argumente ale constructorului: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Această formă este potrivită pentru dependențele obligatorii, de care clasa are neapărat nevoie pentru funcționarea sa, deoarece fără ele instanța nu va putea fi creată. - -Începând cu PHP 8.0 putem folosi o formă mai scurtă de notație ([constructor property promotion |https://blog.nette.org/ro/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), care este funcțional echivalentă: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Începând cu PHP 8.1, proprietatea poate fi marcată cu flag-ul `readonly`, care declară că conținutul proprietății nu se va mai schimba: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -Containerul DI transmite constructorului dependențele automat folosind [autowiring |autowiring]. Argumentele care nu pot fi transmise astfel (de ex. șiruri, numere, booleeni) [le scriem în configurație |services#Argumente]. - - -Constructor hell ----------------- - -Termenul *constructor hell* desemnează situația în care un descendent moștenește de la o clasă părinte al cărei constructor necesită dependențe, și în același timp descendentul necesită dependențe. În acest caz, trebuie să preia și să transmită și pe cele părintești: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Problema apare în momentul în care dorim să schimbăm constructorul clasei `BaseClass`, de exemplu când se adaugă o nouă dependență. Atunci este necesar să modificăm și toți constructorii descendenților. Ceea ce face o astfel de modificare un iad. - -Cum să prevenim asta? Soluția este **să preferăm [compoziția în detrimentul moștenirii |faq#De ce se preferă compoziția în locul moștenirii]**. - -Deci vom proiecta codul altfel. Vom evita clasele [abstracte |nette:introduction-to-object-oriented-programming#Clase abstracte] `Base*`. În loc ca `MyClass` să obțină o anumită funcționalitate prin moștenirea de la `BaseClass`, își va lăsa această funcționalitate să-i fie transmisă ca dependență: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Transmitere prin setter -======================= - -Dependențele sunt transmise prin apelarea unei metode care le stochează într-o proprietate privată. Convenția obișnuită de denumire a acestor metode este forma `set*()`, de aceea li se spune setteri, dar pot fi, desigur, numite oricum altfel. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Acest mod este potrivit pentru dependențele opționale, care nu sunt necesare pentru funcționarea clasei, deoarece nu este garantat că obiectul va primi efectiv dependența (adică că utilizatorul va apela metoda). - -În același timp, acest mod permite apelarea repetată a setterului și astfel modificarea dependenței. Dacă acest lucru nu este dorit, adăugăm o verificare în metodă sau, începând cu PHP 8.1, marcăm proprietatea `$cache` cu flag-ul `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('Dependența a fost deja setată.'); - } - $this->cache = $cache; - } -} -``` - -Apelarea setterului o definim în configurația containerului DI în [cheia setup |services#Setup]. Și aici se utilizează transmiterea automată a dependențelor prin autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Setarea proprietății -==================== - -Dependențele sunt transmise prin scrierea directă în proprietatea membru: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Acest mod este considerat nepotrivit, deoarece proprietatea membru trebuie declarată ca `public`. Și, prin urmare, nu avem control asupra faptului că dependența transmisă va fi într-adevăr de tipul dat (valabil înainte de PHP 7.4) și pierdem posibilitatea de a reacționa la dependența nou atribuită cu cod propriu, de exemplu, pentru a preveni modificarea ulterioară. În același timp, proprietatea devine parte a interfeței publice a clasei, ceea ce poate să nu fie de dorit. - -Setarea proprietății o definim în configurația containerului DI în [secțiunea setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -În timp ce cele trei moduri anterioare sunt valabile în general în toate limbajele orientate pe obiecte, injectarea prin metodă, adnotare sau atribut *inject* este specifică exclusiv presenterilor din Nette. Despre acestea se discută într-un [capitol separat |best-practices:inject-method-attribute]. - - -Ce mod să alegem? -================= - -- constructorul este potrivit pentru dependențele obligatorii, de care clasa are neapărat nevoie pentru funcționarea sa -- setterul este, dimpotrivă, potrivit pentru dependențele opționale sau dependențele care pot fi modificate ulterior -- proprietățile publice nu sunt potrivite diff --git a/dependency-injection/ro/services.texy b/dependency-injection/ro/services.texy deleted file mode 100644 index 08b7683a29..0000000000 --- a/dependency-injection/ro/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Definirea serviciilor -********************* - -.[perex] -Configurația este locul unde învățăm containerul DI cum să asambleze serviciile individuale și cum să le conecteze cu alte dependențe. Nette oferă o modalitate foarte clară și elegantă de a realiza acest lucru. - -Secțiunea `services` din fișierul de configurație în format NEON este locul unde definim serviciile proprii și configurațiile lor. Să vedem un exemplu simplu de definire a unui serviciu numit `database`, care reprezintă o instanță a clasei `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Configurația menționată va rezulta în următoarea metodă factory în [containerul DI|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Numele serviciilor ne permit să ne referim la ele în alte părți ale fișierului de configurație, în formatul `@numeServiciu`. Dacă nu este necesar să numim serviciul, putem folosi pur și simplu doar o liniuță: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Pentru a obține un serviciu din containerul DI, putem utiliza metoda `getService()` cu numele serviciului ca parametru, sau metoda `getByType()` cu tipul serviciului: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Crearea serviciului -=================== - -De cele mai multe ori, creăm un serviciu pur și simplu prin crearea unei instanțe a unei anumite clase. De exemplu: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Dacă avem nevoie să extindem configurația cu alte chei, definiția poate fi împărțită pe mai multe rânduri: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Cheia `create` are aliasul `factory`, ambele variante sunt comune în practică. Cu toate acestea, recomandăm utilizarea `create`. - -Argumentele constructorului sau ale metodei de creare pot fi alternativ scrise în cheia `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Serviciile nu trebuie create doar prin simpla instanțiere a unei clase, ele pot fi, de asemenea, rezultatul apelării metodelor statice sau metodelor altor servicii: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Observați că, pentru simplitate, în loc de `->` se folosește `::`, vezi [#Expresii]. Se vor genera aceste metode factory: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -Containerul DI trebuie să cunoască tipul serviciului creat. Dacă creăm un serviciu folosind o metodă care nu are specificat tipul returnat, trebuie să specificăm explicit acest tip în configurație: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumente -========= - -Transmitem argumente constructorului și metodelor într-un mod foarte similar cu cel din PHP însuși: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Pentru o mai bună lizibilitate, putem împărți argumentele pe rânduri separate. În acest caz, utilizarea virgulelor este opțională: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Puteți, de asemenea, să numiți argumentele și nu trebuie să vă mai faceți griji cu privire la ordinea lor: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Dacă doriți să omiteți unele argumente și să folosiți valoarea lor implicită sau să injectați un serviciu folosind [autowiring |autowiring], utilizați underscore `_`: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Ca argumente se pot transmite servicii, se pot utiliza parametri și multe altele, vezi [#Expresii]. - - -Setup -===== - -În secțiunea `setup` definim metodele care trebuie apelate la crearea serviciului. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -Acest lucru ar arăta astfel în PHP: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Pe lângă apelarea metodelor, se pot transmite și valori către proprietăți. Este suportată și adăugarea unui element într-un array, care trebuie scris între ghilimele pentru a nu intra în conflict cu sintaxa NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Ceea ce în codul PHP ar arăta astfel: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -În setup se pot apela însă și metode statice sau metode ale altor servicii. Dacă aveți nevoie să transmiteți serviciul curent ca argument, specificați-l ca `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Observați că, pentru simplitate, în loc de `->` se folosește `::`, vezi [#Expresii]. Se va genera o astfel de metodă factory: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Expresii -======== - -Nette DI ne oferă expresii extrem de bogate, cu ajutorul cărora putem scrie aproape orice. În fișierele de configurație putem astfel utiliza [parametri |configuration#Parametri]: - -```neon -# parametru -%wwwDir% - -# valoarea parametrului sub cheie -%mailer.user% - -# parametru în interiorul șirului -'%wwwDir%/images' -``` - -Mai departe, putem crea obiecte, apela metode și funcții: - -```neon -# crearea obiectului -DateTime() - -# apelarea metodei statice -Collator::create(%locale%) - -# apelarea funcției PHP -::getenv(DB_USER) -``` - -Ne putem referi la servicii fie după numele lor, fie după tip: - -```neon -# serviciu după nume -@database - -# serviciu după tip -@Nette\Database\Connection -``` - -Putem folosi sintaxa first-class callable: .{data-version:3.2.0} - -```neon -# crearea callback-ului, echivalent cu [@user, logout] -@user::logout(...) -``` - -Putem folosi constante: - -```neon -# constanta clasei -FilesystemIterator::SKIP_DOTS - -# constanta globală o obținem cu funcția PHP constant() -::constant(PHP_VERSION) -``` - -Apelurile metodelor pot fi înlănțuite la fel ca în PHP. Doar pentru simplitate, în loc de `->` se folosește `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Aceste expresii le puteți utiliza oriunde, la [crearea serviciilor |#Crearea serviciului], în [#argumente], în secțiunea [#Setup] sau în [parametri |configuration#Parametri]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Funcții speciale ----------------- - -În fișierele de configurație puteți utiliza aceste funcții speciale: - -- `not()` negația valorii -- `bool()`, `int()`, `float()`, `string()` conversie de tip fără pierderi la tipul specificat -- `typed()` creează un array al tuturor serviciilor de tipul specificat -- `tagged()` creează un array al tuturor serviciilor cu tag-ul dat - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -Spre deosebire de conversia de tip clasică în PHP, cum ar fi de ex. `(int)`, conversia de tip fără pierderi va arunca o excepție pentru valorile non-numerice. - -Funcția `typed()` creează un array al tuturor serviciilor de tipul dat (clasă sau interfață). Omite serviciile care au autowiring-ul dezactivat. Se pot specifica și mai multe tipuri separate prin virgulă. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Puteți transmite array-ul de servicii de un anumit tip ca argument și automat folosind [autowiring |autowiring#Array de servicii]. - -Funcția `tagged()` creează apoi un array al tuturor serviciilor cu un anumit tag. Și aici puteți specifica mai multe tag-uri separate prin virgulă. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Cheia `autowired` permite influențarea comportamentului autowiring-ului pentru un serviciu specific. Pentru detalii, vezi [capitolul despre autowiring|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # serviciul foo este exclus din autowiring -``` - - -Servicii lazy .{data-version:3.2.4} -=================================== - -Încărcarea leneșă (Lazy loading) este o tehnică care amână crearea unui serviciu până în momentul în care este efectiv necesar. În configurația globală se poate [permite crearea lazy |configuration#Servicii lazy] pentru toate serviciile simultan. Pentru servicii individuale, puteți apoi suprascrie acest comportament: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Când un serviciu este definit ca lazy, la solicitarea sa din containerul DI, primim un obiect substituent special. Acesta arată și se comportă la fel ca serviciul real, dar inițializarea reală (apelarea constructorului și a setup-ului) are loc abia la primul apel al oricărei metode sau proprietăți ale sale. - -.[note] -Încărcarea leneșă poate fi utilizată numai pentru clasele definite de utilizator, nu și pentru clasele interne PHP. Necesită PHP 8.4 sau o versiune mai recentă. - - -Tag-uri -======= - -Tag-urile servesc la adăugarea de informații suplimentare serviciilor. Puteți adăuga unul sau mai multe tag-uri unui serviciu: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Tag-urile pot purta și valori: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Pentru a obține toate serviciile cu anumite tag-uri, puteți utiliza funcția `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -În containerul DI puteți obține numele tuturor serviciilor cu un anumit tag folosind metoda `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names este un array care conține numele serviciului și valoarea tag-ului -// de ex. ['foo' => 'monolog.logger.event', ...] -``` - - -Mod Inject -========== - -Folosind flag-ul `inject: true` se activează transmiterea dependențelor prin proprietăți publice cu adnotarea [inject |best-practices:inject-method-attribute#Atribute Inject] și metodele [inject*() |best-practices:inject-method-attribute#Metode inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -În mod implicit, `inject` este activat doar pentru presenteri. - - -Modificarea serviciilor -======================= - -Containerul DI conține multe servicii care au fost adăugate prin extensii încorporate sau [extensii utilizator|extensions]. Puteți modifica definițiile acestor servicii direct în configurație. De exemplu, puteți schimba clasa serviciului `application.application`, care este standard `Nette\Application\Application`, cu alta: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Flag-ul `alteration` este informativ și indică faptul că doar modificăm un serviciu existent. - -Putem, de asemenea, completa setup-ul: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -La suprascrierea unui serviciu, putem dori să eliminăm argumentele originale, elementele setup sau tag-urile, pentru aceasta folosim `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Dacă doriți să eliminați un serviciu adăugat de o extensie, o puteți face astfel: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/sl/@home.texy b/dependency-injection/sl/@home.texy deleted file mode 100644 index e6abd1050d..0000000000 --- a/dependency-injection/sl/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection je načrtovalski vzorec, ki bo bistveno spremenil vaš pogled na kodo in razvoj. Odprl vam bo pot v svet čisto načrtovanih in vzdržljivih aplikacij. - -- [Kaj je Dependency Injection? |introduction] -- [Globalno stanje in singletoni |global-state] -- [Posredovanje odvisnosti |passing-dependencies] -- [Kaj je DI vsebnik? |container] -- [Pogosto zastavljena vprašanja|faq] - - -Paket `nette/di` ponuja izjemno napreden kompiliran DI vsebnik za PHP. - -- [Nette DI Vsebnik |nette-container] -- [Konfiguracija |configuration] -- [Definiranje storitev |services] -- [Autowiring |autowiring] -- [Generirane tovarne |factory] -- [Ustvarjanje razširitev za Nette DI|extensions] diff --git a/dependency-injection/sl/@left-menu.texy b/dependency-injection/sl/@left-menu.texy deleted file mode 100644 index 70ce6dceb5..0000000000 --- a/dependency-injection/sl/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Kaj je DI? |introduction] -- [Globalno stanje in singletoni |global-state] -- [Posredovanje odvisnosti |passing-dependencies] -- [Kaj je DI vsebnik? |container] -- [Pogosto zastavljena vprašanja|faq] - - -Nette DI --------- -- [Nette DI Vsebnik |nette-container] -- [Konfiguracija |configuration] -- [Definiranje storitev |services] -- [Autowiring |autowiring] -- [Generirane tovarne |factory] -- [Ustvarjanje razširitev za Nette DI|extensions] diff --git a/dependency-injection/sl/@meta.texy b/dependency-injection/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/dependency-injection/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/dependency-injection/sl/autowiring.texy b/dependency-injection/sl/autowiring.texy deleted file mode 100644 index 6f70c0b6e5..0000000000 --- a/dependency-injection/sl/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Autowiring -********** - -.[perex] -Autowiring je odlična lastnost, ki zna samodejno posredovati v konstruktor in druge metode zahtevane storitve, tako da jih sploh ni treba pisati. Prihrani vam veliko časa. - -Zahvaljujoč temu lahko izpustimo večino argumentov pri pisanju definicij storitev. Namesto: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Zadostuje napisati: - -```neon -services: - articles: Model\ArticleRepository -``` - -Autowiring se ravna po tipih, zato mora biti za delovanje razred `ArticleRepository` definiran približno takole: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Da bi lahko uporabili autowiring, mora za vsak tip v vsebniku obstajati **točno ena storitev**. Če bi jih bilo več, autowiring ne bi vedel, katero naj posreduje, in bi vrgel izjemo: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # VRŽE IZJEMO, ustrezata mainDb in tempDb -``` - -Rešitev bi bila bodisi obiti autowiring in eksplicitno navesti ime storitve (tj. `articles: Model\ArticleRepository(@mainDb)`). Pametneje pa je autowiring ene od storitev [izklopiti |#Izklop autowiringa] ali prvo storitev [dati prednost |#Prednost autowiringa]. - - -Izklop autowiringa ------------------- - -Autowiring storitve lahko izklopimo z uporabo možnosti `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # storitev tempDb je izključena iz autowiringa - - articles: Model\ArticleRepository # zato posreduje v konstruktor mainDb -``` - -Storitev `articles` ne bo vrgla izjeme, da obstajata dve ustrezni storitvi tipa `PDO` (tj. `mainDb` in `tempDb`), ki ju je mogoče posredovati v konstruktor, ker vidi samo storitev `mainDb`. - -.[note] -Konfiguracija autowiringa v Nette deluje drugače kot v Symfonyju, kjer možnost `autowire: false` pove, da se autowiring ne sme uporabljati za argumente konstruktorja dane storitve. V Nette se autowiring uporablja vedno, bodisi za argumente konstruktorja ali katere koli druge metode. Možnost `autowired: false` pove, da instanca dane storitve ne sme biti nikamor posredovana z uporabo autowiringa. - - -Prednost autowiringa --------------------- - -Če imamo več storitev istega tipa in pri eni od njih navedemo možnost `autowired`, postane ta storitev prednostna: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # postane prednostna - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Storitev `articles` ne bo vrgla izjeme, da obstajata dve ustrezni storitvi tipa `PDO` (tj. `mainDb` in `tempDb`), ampak bo uporabila prednostno storitev, torej `mainDb`. - - -Polje storitev --------------- - -Autowiring zna posredovati tudi polja storitev določenega tipa. Ker v PHP ni mogoče nativno zapisati tipa elementov polja, je treba poleg tipa `array` dopolniti tudi phpDoc komentar s tipom elementa v obliki `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -DI vsebnik nato samodejno posreduje polje storitev, ki ustrezajo danemu tipu. Izpusti storitve, ki imajo izklopljen autowiring. - -Tip v komentarju je lahko tudi v obliki `array<int, Class>` ali `list<Class>`. Če ne morete vplivati na obliko phpDoc komentarja, lahko polje storitev posredujete neposredno v konfiguraciji z uporabo [`typed()` |services#Posebne funkcije]. - - -Skalarni argumenti ------------------- - -Autowiring zna vstavljati samo objekte in polja objektov. Skalarne argumente (npr. nize, števila, booleane) [zapišemo v konfiguraciji |services#Argumenti]. Alternativa je ustvariti [settings-objekt |best-practices:passing-settings-to-presenters], ki skalarno vrednost (ali več vrednosti) zapakira v obliko objekta, ki ga nato lahko spet posredujemo z uporabo autowiringa. - -```php -class MySettings -{ - public function __construct( - // readonly je mogoče uporabiti od PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Iz njega ustvarite storitev z dodajanjem v konfiguracijo: - -```neon -services: - - MySettings('any value') -``` - -Vsi razredi jo nato zahtevajo z uporabo autowiringa. - - -Omejitev autowiringa --------------------- - -Posameznim storitvam lahko autowiring omejimo samo na določene razrede ali vmesnike. - -Običajno autowiring storitev posreduje v vsak parameter metode, katerega tipu storitev ustreza. Omejitev pomeni, da določimo pogoje, ki jim morajo ustrezati tipi, navedeni pri parametrih metod, da jim bo storitev posredovana. - -Poglejmo si to na primeru: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Če bi jih vse registrirali kot storitve, bi autowiring spodletel: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # VRŽE IZJEMO, ustrezata storitvi parent in child - childDep: ChildDependent # autowiring posreduje v konstruktor storitev child -``` - -Storitev `parentDep` vrže izjemo `Multiple services of type ParentClass found: parent, child`, ker v njen konstruktor ustrezata obe storitvi `parent` in `child`, in autowiring ne more odločiti, katero naj izbere. - -Pri storitvi `child` lahko zato omejimo njen autowiring na tip `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # lahko napišemo tudi 'autowired: self' - - parentDep: ParentDependent # autowiring posreduje v konstruktor storitev parent - childDep: ChildDependent # autowiring posreduje v konstruktor storitev child -``` - -Zdaj se v konstruktor storitve `parentDep` posreduje storitev `parent`, ker je zdaj to edini ustrezen objekt. Storitve `child` autowiring tja ne posreduje več. Da, storitev `child` je še vedno tipa `ParentClass`, vendar ne velja več omejitveni pogoj, dan za tip parametra, tj. ne velja, da je `ParentClass` *nadtip* `ChildClass`. - -Pri storitvi `child` bi bilo mogoče `autowired: ChildClass` zapisati tudi kot `autowired: self`, ker je `self` nadomestno ime za razred trenutne storitve. - -V ključu `autowired` je mogoče navesti tudi več razredov ali vmesnikov kot polje: - -```neon -autowired: [BarClass, FooInterface] -``` - -Poskusimo primer dopolniti še z vmesniki: - -```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) - {} -} -``` - -Če storitve `child` nikakor ne omejimo, bo ustrezala konstruktorjem vseh razredov `FooDependent`, `BarDependent`, `ParentDependent` in `ChildDependent`, in autowiring jo bo tja posredoval. - -Če pa njen autowiring omejimo na `ChildClass` z `autowired: ChildClass` (ali `self`), jo bo autowiring posredoval samo v konstruktor `ChildDependent`, ker zahteva argument tipa `ChildClass` in velja, da je `ChildClass` *tipa* `ChildClass`. Noben drug tip, naveden pri drugih parametrih, ni nadtip `ChildClass`, zato se storitev ne posreduje. - -Če jo omejimo na `ParentClass` z `autowired: ParentClass`, jo bo autowiring spet posredoval v konstruktor `ChildDependent` (ker je zahtevani `ChildClass` nadtip `ParentClass`) in na novo tudi v konstruktor `ParentDependent`, ker je zahtevani tip `ParentClass` prav tako ustrezen. - -Če jo omejimo na `FooInterface`, bo še vedno avtomatsko povezana v `ParentDependent` (zahtevani `ParentClass` je nadtip `FooInterface`) in `ChildDependent`, poleg tega pa tudi v konstruktor `FooDependent`, vendar ne v `BarDependent`, ker `BarInterface` ni nadtip `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # autowiring posreduje v konstruktor child - barDep: BarDependent # VRŽE IZJEMO, nobena storitev ne ustreza - parentDep: ParentDependent # autowiring posreduje v konstruktor child - childDep: ChildDependent # autowiring posreduje v konstruktor child -``` diff --git a/dependency-injection/sl/configuration.texy b/dependency-injection/sl/configuration.texy deleted file mode 100644 index d4b773c1a5..0000000000 --- a/dependency-injection/sl/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Konfiguracija DI vsebnika -************************* - -.[perex] -Pregled konfiguracijskih možnosti za Nette DI vsebnik. - - -Konfiguracijska datoteka -======================== - -Nette DI vsebnik se enostavno upravlja s konfiguracijskimi datotekami. Te se običajno zapisujejo v [formatu NEON|neon:format]. Za urejanje priporočamo [urejevalnike s podporo |best-practices:editors-and-tools#IDE urejevalnik] za ta format. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Dekorator .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI vsebnik .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Razširitve]: "Namestitev dodatnih DI razširitev .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Vključevanje datotek]: "Vključevanje datotek .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Parametri]: "Parametri .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Iskanje]: "Samodejna registracija storitev .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Storitve .[prism-token prism-comment]" -</pre> - -.[note] -Če želite zapisati niz, ki vsebuje znak `%`, ga morate ubežati z podvojitvijo na `%%`. - - -Parametri -========= - -V konfiguraciji lahko definirate parametre, ki jih lahko nato uporabite kot del definicij storitev. S tem lahko naredite konfiguracijo preglednejšo ali združite in izločite vrednosti, ki se bodo spreminjale. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -Na parameter `dsn` se sklicujemo kjerkoli v konfiguraciji z zapisom `%dsn%`. Parametre lahko uporabljamo tudi znotraj nizov kot `'%wwwDir%/images'`. - -Parametri niso nujno samo nizi ali števila, lahko vsebujejo tudi polja: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -Na določen ključ se sklicujemo kot `%mailer.user%`. - -Če potrebujete v vaši kodi, na primer v razredu, ugotoviti vrednost katerega koli parametra, ga posredujte v ta razred. Na primer v konstruktorju. Ne obstaja noben globalni objekt, ki bi predstavljal konfiguracijo, katerega bi razredi spraševali za vrednosti parametrov. To bi bilo kršenje načela dependency injection. - - -Storitve -======== - -Glej [samostojno poglavje|services]. - - -Decorator -========= - -Kako množično urediti vse storitve določenega tipa? Na primer poklicati določeno metodo pri vseh presenterjih, ki dedujejo od določenega skupnega prednika? Za to je tu decorator. - -```neon -decorator: - # pri vseh storitvah, ki so instanca tega razreda ali vmesnika - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # pokliči to metodo - - $absoluteUrls = true # in nastavi spremenljivko -``` - -Decorator se lahko uporablja tudi za nastavitev [oznak |services#Oznake] ali vklop načina [inject |services#Način Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Tehnične nastavitve DI vsebnika. - -```neon -di: - # prikazati DIC v Tracy Bar? - debugger: ... # (bool) privzeto je true - - # tipi parametrov, ki jih nikoli ne avtomatsko povezovati - excluded: ... # (string[]) - - # dovoliti leno ustvarjanje storitev? - lazy: ... # (bool) privzeto je false - - # razred, od katerega deduje DI vsebnik - parentClass: ... # (string) privzeto je Nette\DI\Container -``` - - -Lene storitve .{data-version:3.2.4} ------------------------------------ - -Nastavitev `lazy: true` aktivira leno (odloženo) ustvarjanje storitev. To pomeni, da storitve niso dejansko ustvarjene v trenutku, ko jih zahtevamo iz DI vsebnika, ampak šele v trenutku njihove prve uporabe. To lahko pospeši zagon aplikacije in zmanjša pomnilniške zahteve, saj se ustvarijo samo tiste storitve, ki so v danem zahtevku dejansko potrebne. - -Pri določeni storitvi lahko leno ustvarjanje [spremenimo |services#Lazy storitve]. - -.[note] -Lene objekte je mogoče uporabiti samo za uporabniške razrede, ne pa za interne PHP razrede. Zahteva PHP 8.4 ali novejšo različico. - - -Izvoz metapodatkov ------------------- - -Razred DI vsebnika vsebuje tudi veliko metapodatkov. Lahko ga zmanjšate tako, da zmanjšate izvoz metapodatkov. - -```neon -di: - export: - # izvoziti parametre? - parameters: false # (bool) privzeto je true - - # izvoziti oznake in katere? - tags: # (string[]|bool) privzeto so vse - - event.subscriber - - # izvoziti podatke za autowiring in katere? - types: # (string[]|bool) privzeto so vsi - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Če ne uporabljate polja `$container->getParameters()`, lahko izklopite izvoz parametrov. Nadalje lahko izvozite samo tiste oznake, prek katerih pridobivate storitve z metodo `$container->findByTag(...)`. Če metode sploh ne kličete, lahko popolnoma izklopite izvoz oznak z `false`. - -Znatno lahko zmanjšate metapodatke za [samodejnim povezovanjem |autowiring] tako, da navedete razrede, ki jih uporabljate kot parameter metode `$container->getByType()`. In spet, če metode sploh ne kličete (oz. samo v [bootstrapu|application:bootstrapping] za pridobitev `Nette\Application\Application`), lahko izvoz popolnoma izklopite z `false`. - - -Razširitve -========== - -Registracija dodatnih DI razširitev. Na ta način dodamo npr. DI razširitev `Dibi\Bridges\Nette\DibiExtension22` pod imenom `dibi` - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Nato jo torej konfiguriramo v sekciji `dibi`: - -```neon -dibi: - host: localhost -``` - -Kot razširitev lahko dodamo tudi razred, ki ima parametre: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Vključevanje datotek -==================== - -Druge konfiguracijske datoteke lahko vključimo v sekciji `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Ime `parameters.php` ni napaka, konfiguracija je lahko zapisana tudi v PHP datoteki, ki jo vrne kot polje: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Če se v konfiguracijskih datotekah pojavijo elementi z enakimi ključi, bodo prepisani ali v primeru [polj združeni |#Združevanje]. Kasneje vključena datoteka ima višjo prioriteto kot prejšnja. Datoteka, v kateri je navedena sekcija `includes`, ima višjo prioriteto kot v njej vključene datoteke. - - -Iskanje -======= - -Samodejno dodajanje storitev v DI vsebnik izjemno olajša delo. Nette samodejno dodaja v vsebnik presenterje, vendar je mogoče enostavno dodajati tudi katere koli druge razrede. - -Zadostuje navesti, v katerih mapah (in podmapah) naj išče razrede: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Običajno pa ne želimo dodati popolnoma vseh razredov in vmesnikov, zato jih lahko filtriramo: - -```neon -search: - - in: %appDir%/Forms - - # filtriranje po imenu datoteke (string|string[]) - files: - - *Factory.php - - # filtriranje po imenu razreda (string|string[]) - classes: - - *Factory -``` - -Ali pa lahko izberemo razrede, ki dedujejo ali implementirajo vsaj enega od navedenih razredov: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Lahko definiramo tudi izključujoča pravila, tj. maske imena razreda ali dedne prednike, ki če ustrezajo, se storitev v DI vsebnik ne doda: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Vsem storitvam lahko nastavimo oznake: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Združevanje -=========== - -Če se v več konfiguracijskih datotekah pojavijo elementi z enakimi ključi, bodo prepisani ali v primeru polj združeni. Kasneje vključena datoteka ima višjo prioriteto kot prejšnja. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>rezultat</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Pri poljih lahko preprečimo združevanje z navedbo klicaja za imenom ključa: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>rezultat</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Konfiguracija Dependency Injection}} diff --git a/dependency-injection/sl/container.texy b/dependency-injection/sl/container.texy deleted file mode 100644 index 6fd042ac1c..0000000000 --- a/dependency-injection/sl/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Kaj je DI vsebnik? -****************** - -.[perex] -Dependency injection vsebnik (DIC) je razred, ki zna instancirati in konfigurirati objekte. - -Morda vas bo presenetilo, toda v mnogih primerih ne potrebujete dependency injection vsebnika, da bi lahko izkoristili prednosti dependency injection (kratko DI). Saj smo si tudi v [uvodnem poglavju|introduction] na konkretnih primerih DI pokazali in noben vsebnik ni bil potreben. - -Če pa morate upravljati veliko število različnih objektov z mnogimi odvisnostmi, bo dependency injection vsebnik resnično koristen. Kar je na primer primer spletnih aplikacij, zgrajenih na ogrodju. - -V prejšnjem poglavju smo si predstavili razreda `Article` in `UserController`. Oba imata neke odvisnosti, in sicer podatkovno bazo in tovarno `ArticleFactory`. In za te razrede si zdaj ustvarimo vsebnik. Seveda za tako preprost primer nima smisla imeti vsebnika. Ampak ga bomo ustvarili, da si pokažemo, kako izgleda in deluje. - -Tukaj je preprost hardcoded vsebnik za navedeni primer: - -```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()); - } -} -``` - -Uporaba bi izgledala takole: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Vsebniku samo vprašamo za objekt in že nam ni treba vedeti ničesar o tem, kako ga ustvariti in kakšne ima odvisnosti; vse to ve vsebnik. Odvisnosti so z vsebnikom injicirane samodejno. V tem je njegova moč. - -Vsebnik ima zaenkrat zapisane vse podatke trdo kodirano. Naredimo torej naslednji korak in dodajmo parametre, da bo vsebnik resnično koristen: - -```php -class Container -{ - public function __construct( - private array $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' => '***', -]); -``` - -Bistri bralci so morda opazili določeno težavo. Vsakič, ko pridobim objekt `UserController`, se ustvari tudi nova instanca `ArticleFactory` in podatkovne baze. Tega zagotovo nočemo. - -Dodajmo zato metodo `getService()`, ki bo vračala vedno iste instance: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') bo klical createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -Pri prvem klicu npr. `$container->getService('Database')` si pusti od `createDatabase()` ustvariti objekt podatkovne baze, ki ga shrani v polje `$services` in pri naslednjem klicu ga takoj vrne. - -Prilagodimo tudi preostanek vsebnika, da bo uporabljal `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -Mimogrede, izraz storitev se nanaša na kateri koli objekt, ki ga upravlja vsebnik. Zato tudi ime metode `getService()`. - -Končano. Imamo popolnoma funkcionalen DI vsebnik! In lahko ga uporabimo: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Kot vidite, napisati DIC ni nič zapletenega. Omeniti velja, da sami objekti ne vedo, da jih ustvarja nek vsebnik. S tem je mogoče tako ustvarjati kateri koli objekt v PHP brez posega v njegovo izvorno kodo. - -Ročno ustvarjanje in vzdrževanje razreda vsebnika se lahko precej hitro spremeni v nočno moro. V naslednjem poglavju si zato povemo o [Nette DI Containeru|nette-container], ki se zna generirati in posodabljati skoraj sam. - - -{{maintitle: Kaj je dependency injection vsebnik?}} diff --git a/dependency-injection/sl/extensions.texy b/dependency-injection/sl/extensions.texy deleted file mode 100644 index 56ee1b9806..0000000000 --- a/dependency-injection/sl/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Ustvarjanje razširitev za Nette DI -********************************** - -.[perex] -Generiranje DI vsebnika poleg konfiguracijskih datotek vplivajo še t.i. *razširitve*. Aktiviramo jih v konfiguracijski datoteki v sekciji `extensions`. - -Tako dodamo razširitev, predstavljeno z razredom `BlogExtension`, pod imenom `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Vsaka razširitev kompilerja deduje od [api:Nette\DI\CompilerExtension] in lahko implementira naslednje metode, ki so postopoma klicane med sestavljanjem DI vsebnika: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Ta metoda se kliče prva. Definira shemo za validacijo konfiguracijskih parametrov. - -Razširitev konfiguriramo v sekciji, katere ime je enako tistemu, pod katerim je bila razširitev dodana, torej `blog`: - -```neon -# enako ime kot ima extension -blog: - postsPerPage: 10 - allowComments: false -``` - -Ustvarimo shemo, ki opisuje vse konfiguracijske možnosti, vključno z njihovimi tipi, dovoljenimi vrednostmi in po potrebi tudi privzetimi vrednostmi: - -```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), - ]); - } -} -``` - -Dokumentacijo najdete na strani [Shema |schema:]. Poleg tega lahko določimo, katere možnosti so lahko [dinamične |application:bootstrapping#Dinamični parametri] z uporabo `dynamic()`, npr. `Expect::int()->dynamic()`. - -Do konfiguracije dostopamo prek spremenljivke `$this->config`, ki je objekt `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Uporablja se za dodajanje storitev v vsebnik. Za to služi [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']); - } -} -``` - -Konvencija je, da storitve, dodane z razširitvijo, predponamo z njenim imenom, da ne pride do konflikta imen. To počne metoda `prefix()`, tako da če se razširitev imenuje `blog`, bo storitev nosila ime `blog.articles`. - -Če moramo storitev preimenovati, lahko zaradi ohranjanja povratne združljivosti ustvarimo alias s prvotnim imenom. Podobno to počne Nette npr. pri storitvi `routing.router`, ki je dostopna tudi pod prejšnjim imenom `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Nalaganje storitev iz datoteke ------------------------------- - -Storitve ne ustvarjamo samo z API-jem razreda ContainerBuilder, ampak tudi z znanim zapisom, uporabljenim v konfiguracijski datoteki NEON v sekciji services. Predpona `@extension` predstavlja trenutno razširitev. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Storitve naložimo: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // nalaganje konfiguracijske datoteke za razširitev - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Metoda se kliče v trenutku, ko vsebnik vsebuje vse storitve, dodane z posameznimi razširitvami v metodah `loadConfiguration` in tudi z uporabniškimi konfiguracijskimi datotekami. V tej fazi sestavljanja torej lahko definicije storitev urejamo ali dopolnimo povezave med njimi. Za iskanje storitev v vsebniku po oznakah lahko uporabimo metodo `findByTag()`, po razredu ali vmesniku pa metodo `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 tej fazi je razred vsebnika že generiran v obliki objekta [ClassType |php-generator:#Razredi], vsebuje vse metode, ki ustvarjajo storitve, in je pripravljen za zapis v predpomnilnik. Rezultatno kodo razreda lahko v tej točki še vedno urejamo. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -Razred Configurator po [ustvarjanju vsebnika |application:bootstrapping#index.php] kliče inicializacijsko kodo, ki se ustvarja z zapisom v objekt `$this->initialization` z uporabo [metode addBody() |php-generator:#Telesa metod in funkcij]. - -Pokažimo si primer, kako na primer z inicializacijsko kodo zagnati sejo ali zagnati storitve, ki imajo oznako `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // samodejni zagon seje - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // storitve z oznako run morajo biti ustvarjene po instanciranju vsebnika - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/sl/factory.texy b/dependency-injection/sl/factory.texy deleted file mode 100644 index f4cd79285c..0000000000 --- a/dependency-injection/sl/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Generirane tovarne -****************** - -.[perex] -Nette DI zna samodejno generirati kodo tovarn na podlagi vmesnikov, kar vam prihrani pisanje kode. - -Tovarna je razred, ki izdeluje in konfigurira objekte. Posreduje jim torej tudi njihove odvisnosti. Ne zamenjujte prosim z načrtovalskim vzorcem *factory method*, ki opisuje specifičen način uporabe tovarn in s to temo ni povezan. - -Kako taka tovarna izgleda, smo si pokazali v [uvodnem poglavju |introduction#Tovarna]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI zna kodo tovarn samodejno generirati. Vse, kar morate storiti, je ustvariti vmesnik in Nette DI bo generiral implementacijo. Vmesnik mora imeti točno eno metodo z imenom `create` in deklarirati povratni tip: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Torej tovarna `ArticleFactory` ima metodo `create`, ki ustvarja objekte `Article`. Razred `Article` lahko izgleda na primer takole: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Tovarno dodamo v konfiguracijsko datoteko: - -```neon -services: - - ArticleFactory -``` - -Nette DI bo generiral ustrezno implementacijo tovarne. - -V kodi, ki tovarno uporablja, tako zahtevamo objekt po vmesniku in Nette DI bo uporabil generirano implementacijo: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // pustimo tovarni ustvariti objekt - $article = $this->articleFactory->create(); - } -} -``` - - -Parametrizirana tovarna -======================= - -Tovarniška metoda `create` lahko sprejema parametre, ki jih nato posreduje v konstruktor. Dopolnimo na primer razred `Article` z ID avtorja članka: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Parameter dodamo tudi v tovarno: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Zahvaljujoč temu, da se parameter v konstruktorju in parameter v tovarni imenujeta enako, jih Nette DI popolnoma samodejno posreduje. - - -Napredna definicija -=================== - -Definicijo lahko zapišemo tudi v večvrstični obliki z uporabo ključa `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -Pri zapisu na ta daljši način je mogoče navesti dodatne argumente za konstruktor v ključu `arguments` in dopolnilno konfiguracijo z uporabo `setup`, enako kot pri običajnih storitvah. - -Primer: če metoda `create()` ne bi sprejemala parametra `$authorId`, bi lahko navedli fiksno vrednost v konfiguraciji, ki bi se posredovala v konstruktor `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Ali obratno, če bi `create()` parameter `$authorId` sprejemala, vendar ne bi bil del konstruktorja in bi se posredoval z metodo `Article::setAuthorId()`, bi se nanj sklicevali v sekciji `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Nette zna poleg tovarn generirati tudi t.i. accessorje. Gre za objekte z metodo `get()`, ki vrača določeno storitev iz DI vsebnika. Ponavljajoči klic `get()` vrača vedno isto instanco. - -Accessorji zagotavljajo odvisnostim lazy-loading. Imejmo razred, ki zapisuje napake v posebno podatkovno bazo. Če bi si ta razred pustil povezavo z podatkovno bazo posredovati kot odvisnost prek konstruktorja, bi se morala povezava vedno ustvariti, čeprav se v praksi napaka pojavi le izjemoma in bi torej večinoma povezava ostala neizkoriščena. Namesto tega si razred posreduje accessor in šele ko se pokliče njegov `get()`, pride do ustvarjanja objekta podatkovne baze: - -Kako ustvariti accessor? Zadostuje napisati vmesnik in Nette DI bo generiral implementacijo. Vmesnik mora imeti točno eno metodo z imenom `get` in deklarirati povratni tip: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Accessor dodamo v konfiguracijsko datoteko, kjer je tudi definicija storitve, ki jo bo vračal: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Ker accessor vrača storitev tipa `PDO` in je v konfiguraciji edina taka storitev, bo vračal prav njo. Če bi bilo storitev danega tipa več, določimo vračano storitev z imenom, npr. `- PDOAccessor(@db1)`. - - -Večkratna tovarna/accessor -========================== -Naše tovarne in accessorji so doslej vedno znali izdelovati ali vračati samo en objekt. Lahko pa zelo enostavno ustvarimo tudi večkratne tovarne, kombinirane z accessorji. Vmesnik takega razreda bo vseboval poljubno število metod z imeni `create<name>()` in `get<name>()`, npr.: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Torej namesto da bi si posredovali več generiranih tovarn in accessorjev, posredujemo eno kompleksnejšo tovarno, ki zna več. - -Alternativno lahko namesto več metod uporabimo `get()` s parametrom: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Potem velja, da `MultiFactory::getArticle()` počne isto kot `MultiFactoryAlt::get('article')`. Vendar ima alternativni zapis to slabost, da ni očitno, katere vrednosti `$name` so podprte in logično tudi ni mogoče v vmesniku ločiti različnih povratnih vrednosti za različne `$name`. - - -Definicija s seznamom ---------------------- -Na ta način lahko definiramo večkratno tovarno v konfiguraciji: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # definira createArticle() - db: PDO(%dsn%, %user%, %password%) # definira getDb() - ) -``` - -Ali pa se lahko v definiciji tovarne sklicujemo na obstoječe storitve z referenco: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # definira createArticle() - db: @\PDO # definira getDb() - ) -``` - - -Definicija z oznakami ---------------------- - -Druga možnost je uporaba [oznak |services#Oznake] za definicijo: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/sl/faq.texy b/dependency-injection/sl/faq.texy deleted file mode 100644 index b8e129a93b..0000000000 --- a/dependency-injection/sl/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Pogosto zastavljena vprašanja o DI (FAQ) -**************************************** - - -Je DI drugo ime za IoC? ------------------------ - -*Inversion of Control* (IoC) je načelo, osredotočeno na način, kako se koda izvaja - ali vaša koda izvaja tujo ali je vaša koda integrirana v tujo, ki jo nato kliče. IoC je širok pojem, ki vključuje [dogodke |nette:glossary#Dogodki eventi], tako imenovani [Hollywoodski princip |application:components#Hollywood style] in druge vidike. Del tega koncepta so tudi tovarne, o katerih govori [Pravilo št. 3: pusti tovarni |introduction#Pravilo št. 3: prepusti tovarni], in ki predstavljajo inverzijo za operator `new`. - -*Dependency Injection* (DI) se osredotoča na način, kako en objekt izve za drug objekt, torej za njegove odvisnosti. Gre za načrtovalski vzorec, ki zahteva eksplicitno posredovanje odvisnosti med objekti. - -Lahko torej rečemo, da je DI specifična oblika IoC. Vendar niso vse oblike IoC primerne z vidika čistosti kode. Na primer, med antivzorci so tehnike, ki delujejo z [globalnim stanjem |global-state] ali tako imenovani [Service Locator |#Kaj je Service Locator]. - - -Kaj je Service Locator? ------------------------ - -Gre za alternativo Dependency Injection. Deluje tako, da ustvari centralno shrambo, kjer so registrirane vse razpoložljive storitve ali odvisnosti. Ko objekt potrebuje odvisnost, zanjo prosi Service Locator. - -V primerjavi z Dependency Injection pa izgublja na transparentnosti: odvisnosti niso objektom posredovane neposredno in niso tako enostavno prepoznavne, kar zahteva pregled kode, da bi bile vse povezave odkrite in razumljene. Testiranje je prav tako bolj zapleteno, ker ne moremo preprosto posredovati mock objektov testiranim objektom, ampak moramo iti prek Service Locatorja. Poleg tega Service Locator krši načrtovanje kode, saj morajo posamezni objekti vedeti za njegov obstoj, kar se razlikuje od Dependency Injection, kjer objekti nimajo vedenja o DI vsebniku. - - -Kdaj je bolje DI ne uporabiti? ------------------------------- - -Niso znane nobene težave, povezane z uporabo načrtovalskega vzorca Dependency Injection. Nasprotno, pridobivanje odvisnosti iz globalno dostopnih mest vodi k [celi vrsti zapletov |global-state], enako velja za uporabo Service Locatorja. Zato je primerno uporabljati DI vedno. To ni dogmatski pristop, ampak preprosto ni bila najdena boljša alternativa. - -Kljub temu obstajajo določene situacije, ko si objektov ne posredujemo in jih pridobimo iz globalnega prostora. Na primer pri razhroščevanju kode, ko morate na določeni točki programa izpisati vrednost spremenljivke, izmeriti trajanje določenega dela programa ali zabeležiti sporočilo. V takih primerih, ko gre za začasna dejanja, ki bodo kasneje odstranjena iz kode, je legitimno uporabiti globalno dostopen dumper, štoparico ali logger. Ti orodji namreč ne spadajo k načrtovanju kode. - - -Ima uporaba DI svoje slabe strani? ----------------------------------- - -Ali uporaba Dependency Injection prinaša kakšne slabosti, kot na primer povečano zahtevnost pisanja kode ali poslabšano zmogljivost? Kaj izgubimo, ko začnemo pisati kodo v skladu z DI? - -DI nima vpliva na zmogljivost ali pomnilniške zahteve aplikacije. Določeno vlogo lahko igra zmogljivost DI Containerja, vendar v primeru [Nette DI |nette-container] je vsebnik preveden v čisti PHP, tako da je njegova režija med izvajanjem aplikacije v bistvu nična. - -Pri pisanju kode je včasih treba ustvarjati konstruktorje, ki sprejemajo odvisnosti. Prej je to lahko bilo dolgotrajno, vendar je zahvaljujoč sodobnim IDE in [constructor property promotion |https://blog.nette.org/sl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] zdaj vprašanje nekaj sekund. Tovarne lahko enostavno generiramo z Nette DI in vtičnikom za PhpStorm s klikom miške. Po drugi strani odpade potreba po pisanju singletonov in statičnih dostopnih točk. - -Lahko ugotovimo, da pravilno načrtovana aplikacija, ki uporablja DI, v primerjavi z aplikacijo, ki uporablja singletone, ni niti krajša niti daljša. Deli kode, ki delajo z odvisnostmi, so le izvzeti iz posameznih razredov in premaknjeni na nova mesta, torej v DI vsebnik in tovarne. - - -Kako prenoviti staro aplikacijo na DI? --------------------------------------- - -Prehod s stare aplikacije na Dependency Injection je lahko zahteven proces, zlasti pri velikih in kompleksnih aplikacijah. Pomembno je, da k temu procesu pristopimo sistematično. - -- Pri prehodu na Dependency Injection je pomembno, da vsi člani ekipe razumejo načela in postopke, ki se uporabljajo. -- Najprej izvedite analizo obstoječe aplikacije in identificirajte ključne komponente ter njihove odvisnosti. Ustvarite načrt, kateri deli bodo refaktorirani in v kakšnem vrstnem redu. -- Implementirajte DI vsebnik ali še bolje uporabite obstoječo knjižnico, na primer Nette DI. -- Postopoma refaktorirajte posamezne dele aplikacije, da bodo uporabljali Dependency Injection. To lahko vključuje prilagoditve konstruktorjev ali metod tako, da sprejemajo odvisnosti kot parametre. -- Prilagodite mesta v kodi, kjer se ustvarjajo objekti z odvisnostmi, da bodo namesto tega odvisnosti injicirane z vsebnikom. To lahko vključuje uporabo tovarn. - -Ne pozabite, da je prehod na Dependency Injection naložba v kakovost kode in dolgoročno vzdržljivost aplikacije. Čeprav je lahko zahtevno izvesti te spremembe, bi moral biti rezultat čistejša, bolj modularna in enostavno testirana koda, ki je pripravljena za prihodnje razširitve in vzdrževanje. - - -Zakaj se daje prednost kompoziciji pred dedovanjem? ---------------------------------------------------- -Primerneje je uporabljati [kompozicijo |nette:introduction-to-object-oriented-programming#Kompozicija] namesto [dedovanja |nette:introduction-to-object-oriented-programming#Dedovanje], ker služi za ponovno uporabo kode, ne da bi se morali ukvarjati s posledicami sprememb. Zagotavlja torej ohlapnejšo povezavo, pri kateri se nam ni treba bati, da bo sprememba neke kode povzročila potrebo po spremembi druge odvisne kode. Tipičen primer je situacija, označena kot [constructor hell |passing-dependencies#Constructor hell]. - - -Ali je mogoče uporabiti Nette DI Container zunaj Nette? -------------------------------------------------------- - -Vsekakor. Nette DI Container je del Nette, vendar je zasnovan kot samostojna knjižnica, ki jo je mogoče uporabiti neodvisno od drugih delov ogrodja. Zadostuje jo namestiti z Composerjem, ustvariti konfiguracijsko datoteko z definicijo vaših storitev in nato z nekaj vrsticami PHP kode ustvariti DI vsebnik. In takoj lahko začnete izkoriščati prednosti Dependency Injection v svojih projektih. - -Kako izgleda konkretna uporaba, vključno s kodami, opisuje poglavje [Nette DI Container |nette-container]. - - -Zakaj je konfiguracija v NEON datotekah? ----------------------------------------- - -NEON je preprost in lahko berljiv konfiguracijski jezik, ki je bil razvit v okviru Nette za nastavitev aplikacij, storitev in njihovih odvisnosti. V primerjavi z JSONom ali YAMLom ponuja za ta namen veliko bolj intuitivne in fleksibilne možnosti. V NEONu je mogoče naravno opisati povezave, ki jih v Symfony & YAMLu ne bi bilo mogoče zapisati bodisi sploh, bodisi le prek zapletenega opisa. - - -Ali razčlenjevanje NEON datotek upočasnjuje aplikacijo? -------------------------------------------------------- - -Čeprav se datoteke NEON razčlenjujejo zelo hitro, ta vidik sploh ni pomemben. Razlog je, da se razčlenjevanje datotek zgodi samo enkrat ob prvem zagonu aplikacije. Nato se generira koda DI vsebnika, shrani se na disk in se zažene ob vsakem naslednjem zahtevku, ne da bi bilo treba izvajati nadaljnje razčlenjevanje. - -Tako to deluje v produkcijskem okolju. Med razvojem se NEON datoteke razčlenjujejo vsakič, ko pride do spremembe njihove vsebine, da ima razvijalec vedno aktualen DI vsebnik. Samo razčlenjevanje je, kot je bilo rečeno, vprašanje trenutka. - - -Kako iz svojega razreda dostopam do parametrov v konfiguracijski datoteki? --------------------------------------------------------------------------- - -Imejmo v mislih [Pravilo št. 1: naj ti posredujejo |introduction#Pravilo št. 1: naj ti bo predano]. Če razred zahteva informacije iz konfiguracijske datoteke, nam ni treba razmišljati, kako do teh informacij priti, namesto tega jih preprosto zahtevamo - na primer prek konstruktorja razreda. In posredovanje izvedemo v konfiguracijski datoteki. - -V tej predstavitvi je `%myParameter%` nadomestni znak za vrednost parametra `myParameter`, ki se posreduje v konstruktor razreda `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Če želite posredovati več parametrov ali izkoristiti autowiring, je primerno [parametre zapakirati v objekt |best-practices:passing-settings-to-presenters]. - - -Ali Nette podpira PSR-11: Container interface? ----------------------------------------------- - -Nette DI Container ne podpira PSR-11 neposredno. Vendar, če potrebujete interoperabilnost med Nette DI Containerjem in knjižnicami ali ogrodji, ki pričakujejo PSR-11 Container Interface, lahko ustvarite [preprost adapter |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], ki bo služil kot most med Nette DI Containerjem in PSR-11. diff --git a/dependency-injection/sl/global-state.texy b/dependency-injection/sl/global-state.texy deleted file mode 100644 index 960d8c9cb9..0000000000 --- a/dependency-injection/sl/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Globalno stanje in singletoni -***************************** - -.[perex] -Opozorilo: Naslednje konstrukcije so znak slabo načrtovane kode: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` ali `static::$var` - -Ali se nekatere od teh konstrukcij pojavljajo v vaši kodi? Potem imate priložnost za njeno izboljšanje. Morda si mislite, da gre za običajne konstrukcije, ki jih vidite na primer tudi v vzorčnih rešitvah različnih knjižnic in ogrodij. Če je temu tako, potem načrtovanje njihove kode ni dobro. - -Zdaj zagotovo ne govorimo o neki akademski čistosti. Vse te konstrukcije imajo eno skupno: izkoriščajo globalno stanje. In to ima uničujoč vpliv na kakovost kode. Razredi lažejo o svojih odvisnostih. Koda postane nepredvidljiva. Zmede programerje in zmanjšuje njihovo učinkovitost. - -V tem poglavju si bomo razložili, zakaj je temu tako in kako se globalnemu stanju izogniti. - - -Globalna povezanost -------------------- - -V idealnem svetu bi moral objekt biti sposoben komunicirati samo z objekti, ki so mu bili [neposredno posredovani |passing-dependencies]. Če ustvarim dva objekta `A` in `B` in nikoli ne posredujem reference med njima, potem se niti `A` niti `B` ne moreta dostopati do drugega objekta ali spremeniti njegovega stanja. To je zelo zaželena lastnost kode. Podobno je, kot če imate baterijo in žarnico; žarnica ne bo svetila, dokler je z baterijo ne povežete z žico. - -To pa ne velja pri globalnih (statičnih) spremenljivkah ali singletonih. Objekt `A` bi se lahko *brezžično* dostopal do objekta `C` in ga modificiral brez kakršnega koli posredovanja reference, s klicem `C::changeSomething()`. Če se objekt `B` prav tako oprime globalnega `C`, potem se `A` in `B` lahko medsebojno vplivata prek `C`. - -Uporaba globalnih spremenljivk v sistem vnaša novo obliko *brezžične* povezanosti, ki od zunaj ni vidna. Ustvarja dimno zaveso, ki otežuje razumevanje in uporabo kode. Da bi razvijalci odvisnosti resnično razumeli, morajo prebrati vsako vrstico izvorne kode. Namesto zgolj seznanitve z vmesnikom razredov. Gre poleg tega za popolnoma nepotrebno povezanost. Globalno stanje se uporablja zato, ker je enostavno dostopno od kjerkoli in omogoča na primer zapis v podatkovno bazo prek globalne (statične) metode `DB::insert()`. Ampak kot si bomo pokazali, je prednost, ki jo to prinaša, neznatna, nasprotno pa povzroča usodne zaplete. - -.[note] -Z vidika obnašanja ni razlike med globalno in statično spremenljivko. Sta enako škodljivi. - - -Strašljivo delovanje na daljavo -------------------------------- - -"Strašljivo delovanje na daljavo" - tako je slavno leta 1935 Albert Einstein poimenoval pojav v kvantni fiziki, ki mu je naganjal kurjo polt. -Gre za kvantno prepletenost, katere posebnost je, da ko izmerite informacijo o enem delcu, s tem takoj vplivate na drugi delec, tudi če sta med seboj oddaljena milijone svetlobnih let. Kar navidezno krši osnovni zakon vesolja, da se nič ne more širiti hitreje od svetlobe. - -V svetu programske opreme lahko "strašljivo delovanje na daljavo" poimenujemo situacijo, ko zaženemo nek proces, za katerega menimo, da je izoliran (ker mu nismo posredovali nobenih referenc), vendar na oddaljenih mestih sistema pride do nepričakovanih interakcij in sprememb stanja, o katerih nismo imeli pojma. Do tega lahko pride samo prek globalnega stanja. - -Predstavljajte si, da se pridružite ekipi razvijalcev projekta, ki ima obsežno napredno kodno bazo. Vaš novi vodja vas prosi za implementacijo nove funkcije in vi kot pravi razvijalec začnete s pisanjem testa. Ker pa ste v projektu novi, delate veliko raziskovalnih testov tipa "kaj se zgodi, če pokličem to metodo". In poskusite napisati naslednji test: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // številka vaše kartice - $cc->charge(100); -} -``` - -Zaženete kodo, morda večkrat, in po nekem času opazite na mobilnem telefonu obvestila iz banke, da se je ob vsakem zagonu odštelo 100 dolarjev z vaše plačilne kartice 🤦‍♂️ - -Kako za vraga je lahko test povzročil dejansko odtegnitev denarja? Upravljanje s plačilno kartico ni enostavno. Morate komunicirati s spletno storitvijo tretje osebe, morate poznati URL te spletne storitve, morate se prijaviti in tako naprej. Nobena od teh informacij ni vsebovana v testu. Še huje, niti ne veste, kje so te informacije prisotne, in torej niti kako mockati zunanje odvisnosti, da vsak zagon ne bi vodil k temu, da se ponovno odšteje 100 dolarjev. In kako ste kot novi razvijalec morali vedeti, da bo to, kar se pripravljate storiti, vodilo k temu, da boste za 100 dolarjev revnejši? - -To je strašljivo delovanje na daljavo! - -Ne preostane vam drugega, kot da se dolgo prebijate skozi veliko izvorne kode, sprašujete starejše in izkušenejše kolege, preden razumete, kako povezave v projektu delujejo. To je posledica tega, da ob pogledu na vmesnik razreda `CreditCard` ni mogoče ugotoviti globalnega stanja, ki ga je treba inicializirati. Celo pogled v izvorno kodo razreda vam ne bo razkril, katero inicializacijsko metodo morate poklicati. V najboljšem primeru lahko najdete globalno spremenljivko, do katere se dostopa, in iz nje poskusite uganiti, kako jo inicializirati. - -Razredi v takem projektu so patološki lažnivci. Plačilna kartica se pretvarja, da jo zadostuje instancirati in poklicati metodo `charge()`. Skrito pa sodeluje z drugim razredom `PaymentGateway`, ki predstavlja plačilni prehod. Tudi njen vmesnik pravi, da jo je mogoče inicializirati samostojno, vendar v resnici potegne poverilnice iz neke konfiguracijske datoteke in tako naprej. Razvijalcem, ki so to kodo napisali, je jasno, da `CreditCard` potrebuje `PaymentGateway`. Kodo so napisali na ta način. Ampak za vsakogar, ki je v projektu nov, je to popolna uganka in ovira učenje. - -Kako situacijo popraviti? Enostavno. **Pustite API-ju, da deklarira odvisnosti.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Opazite, kako so naenkrat povezave znotraj kode očitne. S tem, ko metoda `charge()` deklarira, da potrebuje `PaymentGateway`, vam ni treba nikogar spraševati, kako je koda povezana. Veste, da morate ustvariti njeno instanco, in ko to poskusite, naletite na to, da morate dodati dostopne parametre. Brez njih kode ne bi bilo mogoče niti zagnati. - -In predvsem zdaj lahko plačilni prehod mockate, tako da se vam ob vsakem zagonu testa ne bo zaračunalo 100 dolarjev. - -Globalno stanje povzroča, da se vaši objekti lahko skrivaj dostopajo do stvari, ki niso deklarirane v njihovem API-ju, in posledično delajo iz vaših API-jev patološke lažnivce. - -Morda o tem prej niste tako razmišljali, ampak kadarkoli uporabljate globalno stanje, ustvarjate skrivne brezžične komunikacijske kanale. Strašljivo delovanje na daljavo sili razvijalce, da berejo vsako vrstico kode, da bi razumeli potencialne interakcije, zmanjšuje produktivnost razvijalcev in zmede nove člane ekipe. Če ste vi tisti, ki ste kodo ustvarili, poznate dejanske odvisnosti, ampak vsakdo, ki pride za vami, je nemočen. - -Ne pišite kode, ki izkorišča globalno stanje, dajte prednost posredovanju odvisnosti. Torej dependency injection. - - -Krhkost globalnega stanja -------------------------- - -V kodi, ki uporablja globalno stanje in singletone, nikoli ni gotovo, kdaj in kdo je to stanje spremenil. To tveganje se pojavlja že pri inicializaciji. Naslednja koda naj bi ustvarila povezavo s podatkovno bazo in inicializirala plačilni prehod, vendar nenehno meče izjemo in iskanje vzroka je izjemno dolgotrajno: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Morate podrobno pregledovati kodo, da ugotovite, da objekt `PaymentGateway` brezžično dostopa do drugih objektov, od katerih nekateri zahtevajo povezavo s podatkovno bazo. Torej je treba inicializirati podatkovno bazo prej kot `PaymentGateway`. Vendar dimna zavesa globalnega stanja to pred vami skriva. Koliko časa bi prihranili, če API posameznih razredov ne bi lagal in bi deklariral svoje odvisnosti? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Podobna težava se pojavlja tudi pri uporabi globalnega dostopa do povezave s podatkovno bazo: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -Pri klicu metode `save()` ni gotovo, ali je bila povezava s podatkovno bazo že ustvarjena in kdo nosi odgovornost za njeno ustvarjanje. Če želimo na primer spreminjati povezavo s podatkovno bazo med izvajanjem, na primer zaradi testov, bi morali najverjetneje ustvariti dodatne metode, kot na primer `DB::reconnect(...)` ali `DB::reconnectForTest()`. - -Razmislimo o primeru: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Kje imamo gotovost, da se pri klicu `$article->save()` res uporablja testna podatkovna baza? Kaj če je metoda `Foo::doSomething()` spremenila globalno povezavo s podatkovno bazo? Za ugotovitev bi morali pregledati izvorno kodo razreda `Foo` in verjetno tudi mnogih drugih razredov. Ta pristop bi prinesel le kratkoročen odgovor, saj se situacija lahko v prihodnosti spremeni. - -In kaj če povezavo s podatkovno bazo premaknemo v statično spremenljivko znotraj razreda `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -S tem se sploh nič ni spremenilo. Težava je globalno stanje in popolnoma vseeno je, v katerem razredu se skriva. V tem primeru, enako kot v prejšnjem, nimamo pri klicu metode `$article->save()` nobenega namiga o tem, v katero bazo podatkov se bo zapisalo. Kdorkoli na drugem koncu aplikacije je lahko kadarkoli z `Article::setDb()` bazo podatkov spremenil. Nam pod rokami. - -Globalno stanje naredi našo aplikacijo **izjemno krhko**. - -Obstaja pa preprost način, kako se s to težavo spopasti. Zadostuje, da API deklarira odvisnosti, s čimer se zagotovi pravilna funkcionalnost. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Zahvaljujoč temu pristopu odpade skrb za skrite in nepričakovane spremembe povezave z bazo podatkov. Zdaj imamo gotovost, kam se članek shranjuje in nobene spremembe kode znotraj druge nepovezane razreda že ne morejo situacije spremeniti. Koda ni več krhka, ampak stabilna. - -Ne pišite kode, ki izkorišča globalno stanje, dajte prednost posredovanju odvisnosti. Torej dependency injection. - - -Singleton ---------- - -Singleton je načrtovalski vzorec, ki po "definiciji":https://en.wikipedia.org/wiki/Singleton_pattern iz znane publikacije Gang of Four omejuje razred na eno samo instanco in ponuja globalni dostop do nje. Implementacija tega vzorca se običajno podobna naslednji kodi: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // in druge metode, ki opravljajo funkcije danega razreda -} -``` - -Na žalost singleton v aplikacijo uvaja globalno stanje. In kot smo si pokazali zgoraj, je globalno stanje nezaželeno. Zato je singleton obravnavan kot antipattern. - -Ne uporabljajte v svoji kodi singletonov in jih nadomestite z drugimi mehanizmi. Singletonov resnično ne potrebujete. Če pa morate zagotoviti obstoj ene same instance razreda za celotno aplikacijo, pustite to [DI vsebniku |container]. Ustvarite tako aplikacijski singleton, ali storitev. S tem se razred preneha ukvarjati z zagotavljanjem svoje lastne edinstvenosti (tj. ne bo imel metode `getInstance()` in statične spremenljivke) in bo opravljal samo svoje funkcije. Tako ne bo več kršil načela ene same odgovornosti. - - -Globalno stanje proti testom ----------------------------- - -Pri pisanju testov predpostavljamo, da je vsak test izolirana enota in da vanj ne vstopa nobeno zunanje stanje. In nobeno stanje testov ne zapušča. Po zaključku testa bi moralo biti vse povezano stanje s testom samodejno odstranjeno z garbage collectorjem. Zahvaljujoč temu so testi izolirani. Zato lahko teste izvajamo v poljubnem vrstnem redu. - -Če pa so prisotna globalna stanja/singletoni, se vse te prijetne predpostavke razblinijo. Stanje lahko vstopa v test in izstopa iz njega. Naenkrat lahko postane pomemben vrstni red testov. - -Da bi sploh lahko testirali singletone, morajo razvijalci pogosto sprostiti njihove lastnosti, na primer tako, da dovolijo zamenjavo instance z drugo. Take rešitve so v najboljšem primeru hack, ki ustvarja težko vzdržljivo in razumljivo kodo. Vsak test ali metoda `tearDown()`, ki vpliva na katero koli globalno stanje, mora te spremembe vrniti nazaj. - -Globalno stanje je največja bolečina pri unit testiranju! - -Kako situacijo popraviti? Enostavno. Ne pišite kode, ki izkorišča singletone, dajte prednost posredovanju odvisnosti. Torej dependency injection. - - -Globalne konstante ------------------- - -Globalno stanje se ne omejuje samo na uporabo singletonov in statičnih spremenljivk, ampak se lahko nanaša tudi na globalne konstante. - -Konstante, katerih vrednost nam ne prinaša nobene nove (`M_PI`) ali koristne (`PREG_BACKTRACK_LIMIT_ERROR`) informacije, so nedvomno v redu. Nasprotno pa konstante, ki služijo kot način, kako *brezžično* posredovati informacijo znotraj kode, niso nič drugega kot skrita odvisnost. Kot na primer `LOG_FILE` v naslednjem primeru. Uporaba konstante `FILE_APPEND` je popolnoma pravilna. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -V tem primeru bi morali deklarirati parameter v konstruktorju razreda `Foo`, da postane del API-ja: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Zdaj lahko posredujemo informacijo o poti do datoteke za beleženje in jo enostavno spreminjamo po potrebi, kar olajša testiranje in vzdrževanje kode. - - -Globalne funkcije in statične metode ------------------------------------- - -Želimo poudariti, da sama uporaba statičnih metod in globalnih funkcij ni problematična. Razložili smo, v čem je neprimernost uporabe `DB::insert()` in podobnih metod, vendar je vedno šlo le za zadevo globalnega stanja, ki je shranjeno v neki statični spremenljivki. Metoda `DB::insert()` zahteva obstoj statične spremenljivke, ker je v njej shranjena povezava z bazo podatkov. Brez te spremenljivke bi bilo nemogoče metodo implementirati. - -Uporaba determinističnih statičnih metod in funkcij, kot na primer `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` in mnogih drugih, je v popolnem skladu z dependency injection. Te funkcije vedno vračajo enake rezultate iz enakih vhodnih parametrov in so torej predvidljive. Ne uporabljajo nobenega globalnega stanja. - -Obstajajo pa tudi funkcije v PHP, ki niso deterministične. K njim spada na primer funkcija `htmlspecialchars()`. Njen tretji parameter `$encoding`, če ni naveden, ima kot privzeto vrednost vrednost konfiguracijske možnosti `ini_get('default_charset')`. Zato se priporoča ta parameter vedno navesti in preprečiti morebitno nepredvidljivo obnašanje funkcije. Nette to dosledno počne. - -Nekatere funkcije, kot na primer `strtolower()`, `strtoupper()` in podobne, so se v nedavni preteklosti nedeterministično obnašale in bile odvisne od nastavitve `setlocale()`. To je povzročalo veliko zapletov, najpogosteje pri delu s turškim jezikom. Ta namreč razlikuje malo in veliko črko `I` s piko in brez pike. Tako je `strtolower('I')` vračalo znak `ı` in `strtoupper('i')` znak `İ`, kar je vodilo k temu, da so aplikacije začele povzročati vrsto skrivnostnih napak. Ta težava pa je bila odpravljena v PHP različici 8.2 in funkcije niso več odvisne od locale. - -Gre za lep primer, kako je globalno stanje mučilo na tisoče razvijalcev po vsem svetu. Rešitev je bila zamenjava z dependency injection. - - -Kdaj je mogoče uporabiti globalno stanje? ------------------------------------------ - -Obstajajo določene specifične situacije, ko je mogoče izkoristiti globalno stanje. Na primer pri razhroščevanju kode, ko morate izpisati vrednost spremenljivke ali izmeriti trajanje določenega dela programa. V takih primerih, ki se nanašajo na začasna dejanja, ki bodo kasneje odstranjena iz kode, je mogoče legitimno izkoristiti globalno dostopen dumper ali štoparico. Ti orodji namreč niso del načrtovanja kode. - -Drug primer so funkcije za delo z regularnimi izrazi `preg_*`, ki interno shranjujejo prevedene regularne izraze v statični predpomnilnik v pomnilniku. Ko torej kličete isti regularni izraz večkrat na različnih mestih kode, se prevede samo enkrat. Predpomnilnik varčuje z zmogljivostjo in hkrati je za uporabnika popolnoma neviden, zato lahko tako uporabo štejemo za legitimno. - - -Povzetek --------- - -Pregledali smo, zakaj ima smisel: - -1) Odstraniti vse statične spremenljivke iz kode -2) Deklarirati odvisnosti -3) In uporabljati dependency injection - -Ko razmišljate o načrtovanju kode, mislite na to, da vsak `static $foo` predstavlja težavo. Da bi vaša koda bila okolje, ki spoštuje DI, je nujno popolnoma izkoreniniti globalno stanje in ga nadomestiti z dependency injection. - -Med tem procesom morda ugotovite, da je treba razred razdeliti, ker ima več kot eno odgovornost. Ne bojte se tega; prizadevajte si za načelo ene same odgovornosti. - -*Rad bi se zahvalil Mišku Heveryju, čigar članki, kot je [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], so osnova tega poglavja.* diff --git a/dependency-injection/sl/introduction.texy b/dependency-injection/sl/introduction.texy deleted file mode 100644 index 2a3c0bdcc6..0000000000 --- a/dependency-injection/sl/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Kaj je Vbrizgavanje odvisnosti? -******************************* - -.[perex] -To poglavje vas bo seznanilo z osnovnimi programerskimi postopki, ki jih morate upoštevati pri pisanju vseh aplikacij. Gre za osnove, potrebne za pisanje čiste, razumljive in vzdržljive kode. - -Če boste ta pravila sprejeli in jih upoštevali, vam bo Nette v vsakem koraku pomagal. Za vas bo reševal rutinske naloge in vam zagotovil maksimalno udobje, da se boste lahko osredotočili na samo logiko. - -Principi, ki jih bomo tukaj predstavili, so precej preprosti. Ničesar se vam ni treba bati. - - -Se spomnite svojega prvega programa? ------------------------------------- - -Ne vemo sicer, v katerem jeziku ste ga napisali, a če bi bil to PHP, bi verjetno izgledal nekako takole: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // izpiše 24 -``` - -Nekaj trivialnih vrstic kode, a v njih se skriva toliko ključnih konceptov. Da obstajajo spremenljivke. Da se koda deli na manjše enote, kot so na primer funkcije. Da jim predajamo vhodne argumente in one vračajo rezultate. Manjkajo le še pogoji in zanke. - -To, da funkciji predamo vhodne podatke in ona vrne rezultat, je popolnoma razumljiv koncept, ki se uporablja tudi na drugih področjih, kot na primer v matematiki. - -Funkcija ima svojo signaturo, ki jo sestavljajo njeno ime, seznam parametrov in njihovih tipov ter na koncu tip vrnjene vrednosti. Kot uporabnike nas zanima signatura, o notranji implementaciji običajno ne potrebujemo vedeti ničesar. - -Zdaj si predstavljajte, da bi signatura funkcije izgledala takole: - -```php -function soucet(float $x): float -``` - -Seštevanje z enim parametrom? To je čudno… Kaj pa takole? - -```php -function soucet(): float -``` - -To pa je že res zelo čudno, kajne? Kako se funkcija sploh uporablja? - -```php -echo soucet(); // kaj naj bi izpisalo? -``` - -Ob pogledu na takšno kodo bi bili zmedeni. Ne samo, da je ne bi razumel začetnik, takšne kode ne razume niti izkušen programer. - -Razmišljate, kako bi takšna funkcija sploh izgledala znotraj? Kje bi vzela seštevance? Očitno bi si jih *na nek način* priskrbela sama, na primer takole: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -V telesu funkcije smo odkrili skrite povezave na druge globalne funkcije ali statične metode. Da bi ugotovili, od kod se seštevanci dejansko vzamejo, moramo raziskovati naprej. - - -Tako ne! --------- - -Načrt, ki smo ga pravkar predstavili, je bistvo mnogih negativnih lastnosti: - -- signatura funkcije se je pretvarjala, da ne potrebuje seštevancev, kar nas je zmedlo -- sploh ne vemo, kako funkcijo pripraviti do tega, da sešteje drugi dve števili -- morali smo pogledati v kodo, da bi ugotovili, kje vzame seštevance -- odkrili smo skrite povezave -- za popolno razumevanje je treba preučiti tudi te povezave - -In ali je sploh naloga seštevalne funkcije, da si priskrbi vhode? Seveda ni. Njena odgovornost je le samo seštevanje. - - -S takšno kodo se nočemo srečati in je zagotovo nočemo pisati. Popravek je pri tem preprost: vrniti se k osnovam in preprosto uporabiti parametre: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Pravilo št. 1: naj ti bo predano --------------------------------- - -Najpomembnejše pravilo se glasi: **vsi podatki, ki jih funkcije ali razredi potrebujejo, jim morajo biti predani**. - -Namesto da bi si izmišljali skrite načine, s katerimi bi lahko sami prišli do njih, preprosto predajte parametre. Prihranili boste čas, potreben za izmišljanje skritih poti, ki zagotovo ne bodo izboljšale vaše kode. - -Če boste to pravilo vedno in povsod upoštevali, ste na poti h kodi brez skritih povezav. H kodi, ki je razumljiva ne samo avtorju, ampak tudi vsakomur, ki jo bo bral za njim. Kjer je vse razumljivo iz signatur funkcij in razredov in ni treba iskati skritih skrivnosti v implementaciji. - -Tej tehniki se strokovno reče **dependency injection** (vbrizgavanje odvisnosti). In tem podatkom se reče **odvisnosti.** Pri tem gre za povsem običajno predajanje parametrov, nič več. - -.[note] -Prosimo, ne zamenjujte dependency injection, ki je načrtovalski vzorec, z „dependency injection container“, ki je orodje, torej nekaj diametralno drugačnega. Z vsebniki se bomo ukvarjali kasneje. - - -Od funkcij k razredom ---------------------- - -In kako so s tem povezani razredi? Razred je kompleksnejša celota kot preprosta funkcija, vendar pravilo št. 1 velja brez izjeme tudi tukaj. Obstaja le [več možnosti, kako predati argumente|passing-dependencies]. Na primer precej podobno kot pri funkciji: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Ali z drugimi metodami ali neposredno s konstruktorjem: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Oba primera sta popolnoma v skladu z dependency injection. - - -Realni primeri --------------- - -V resničnem svetu ne boste pisali razredov za seštevanje števil. Premaknimo se k primerom iz prakse. - -Imejmo razred `Article`, ki predstavlja članek na blogu: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // shranimo članek v podatkovno bazo - } -} -``` - -in uporaba bo naslednja: - -```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()` shrani članek v podatkovno tabelo. Implementirati jo s pomočjo [Nette Database |database:] bi bilo enostavno, če ne bi bilo ene ovire: kje naj `Article` vzame povezavo s podatkovno bazo, tj. objekt razreda `Nette\Database\Connection`? - -Zdi se, da imamo veliko možnosti. Lahko jo vzame od nekod iz statične spremenljivke. Ali podeduje od razreda, ki zagotovi povezavo s podatkovno bazo. Ali uporabi t.i. [singleton |global-state#Singleton]. Ali t.i. facades, ki se uporabljajo 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], - ); - } -} -``` - -Odlično, problem smo rešili. - -Ali ne? - -Spomnimo se [##pravilo št. 1: naj ti bo predano]: vse odvisnosti, ki jih razred potrebuje, mu morajo biti predane. Ker če pravilo kršimo, smo stopili na pot k umazani kodi, polni skritih povezav, nerazumljivosti, in rezultat bo aplikacija, ki jo bo boleče vzdrževati in razvijati. - -Uporabnik razreda `Article` ne ve, kam metoda `save()` članek shranjuje. V podatkovno tabelo? V katero, produkcijsko ali testno? In kako je to mogoče spremeniti? - -Uporabnik mora pogledati, kako je implementirana metoda `save()`, in najde uporabo metode `DB::insert()`. Torej mora raziskovati naprej, kako si ta metoda priskrbi podatkovno povezavo. In skrite povezave lahko tvorijo precej dolgo verigo. - -V čisti in dobro zasnovani kodi se nikoli ne pojavljajo skrite povezave, Laravelove facades ali statične spremenljivke. V čisti in dobro zasnovani kodi se predajajo argumenti: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Še bolj praktično, kot bomo videli kasneje, bo to s konstruktorjem: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Če ste izkušen programer, morda mislite, da `Article` sploh ne bi smel imeti metode `save()`, moral bi predstavljati zgolj podatkovno komponento in za shranjevanje bi moral skrbeti ločen repozitorij. To ima smisel. Toda s tem bi se oddaljili daleč preko okvira teme, ki je dependency injection, in prizadevanja za navajanje preprostih primerov. - -Če boste pisali razred, ki za svoje delovanje potrebuje npr. podatkovno bazo, ne izmišljajte si, od kod jo dobiti, ampak naj vam jo predajo. Na primer kot parameter konstruktorja ali druge metode. Priznajte odvisnosti. Priznajte jih v API-ju vašega razreda. Dobili boste razumljivo in predvidljivo kodo. - -Kaj pa ta razred, ki beleži sporočila o napakah: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Kaj mislite, smo upoštevali [##pravilo št. 1: naj ti bo predano]? - -Nismo. - -Ključno informacijo, torej imenik z datoteko z logom, si razred *priskrbi sam* iz konstante. - -Poglejte primer uporabe: - -```php -$logger = new Logger; -$logger->log('Temperatura je 23 °C'); -$logger->log('Temperatura je 10 °C'); -``` - -Brez poznavanja implementacije, bi lahko odgovorili na vprašanje, kam se sporočila zapisujejo? Bi pomislili, da je za delovanje potrebna obstoj konstante `LOG_DIR`? In bi lahko ustvarili drugo instanco, ki bo zapisovala drugam? Zagotovo ne. - -Popravimo razred: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Razred je zdaj veliko bolj razumljiv, nastavljiv in torej uporabnejši. - -```php -$logger = new Logger('/pot/do/loga.txt'); -$logger->log('Temperatura je 15 °C'); -``` - - -Ampak to me ne zanima! ----------------------- - -*„Ko ustvarim objekt Article in pokličem save(), potem nočem reševati podatkovne baze, preprosto želim, da se shrani v tisto, ki jo imam nastavljeno v konfiguraciji.“* - -*„Ko uporabim Logger, preprosto želim, da se sporočilo zapiše, in nočem reševati kam. Naj se uporabi globalna nastavitev.“* - -To so pravilne pripombe. - -Kot primer si bomo pokazali razred, ki pošilja novice (newsletterje) in zabeleži, kako se je izšlo: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('E-pošta je bila poslana'); - - } catch (Exception $e) { - $logger->log('Prišlo je do napake pri pošiljanju'); - throw $e; - } - } -} -``` - -Izboljšan `Logger`, ki ne uporablja več konstante `LOG_DIR`, zahteva v konstruktorju navedbo poti do datoteke. Kako to rešiti? Razreda `NewsletterDistributor` sploh ne zanima, kam se sporočila zapisujejo, želi jih le zapisati. - -Rešitev je spet [##pravilo št. 1: naj ti bo predano]: vse podatke, ki jih razred potrebuje, mu predamo. - -Torej to pomeni, da si preko konstruktorja predamo pot do loga, ki jo nato uporabimo pri ustvarjanju objekta `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ TAKO NE! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Tako ne! Pot namreč **ne spada** med podatke, ki jih razred `NewsletterDistributor` potrebuje; te namreč potrebuje `Logger`. Zaznavate razliko? Razred `NewsletterDistributor` potrebuje logger kot takega. Torej si tega predamo: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('E-pošta je bila poslana'); - - } catch (Exception $e) { - $this->logger->log('Prišlo je do napake pri pošiljanju'); - throw $e; - } - } -} -``` - -Zdaj je iz signatur razreda `NewsletterDistributor` jasno, da je del njegove funkcionalnosti tudi logiranje. In naloga zamenjati logger za drugega, na primer zaradi testiranja, je popolnoma trivialna. Poleg tega, če bi se konstruktor razreda `Logger` spremenil, to ne bo imelo nobenega vpliva na naš razred. - - -Pravilo št. 2: vzemi, kar je tvoje ----------------------------------- - -Ne pustite se zmesti in ne pustite si predajati odvisnosti svojih odvisnosti. Pustite si predajati le svoje odvisnosti. - -Zahvaljujoč temu bo koda, ki uporablja druge objekte, popolnoma neodvisna od sprememb njihovih konstruktorjev. Njen API bo bolj resničen. In predvsem bo trivialno te odvisnosti zamenjati za druge. - - -Nov član družine ----------------- - -V razvojni ekipi je padla odločitev ustvariti drugi logger, ki zapisuje v podatkovno bazo. Ustvarili bomo torej razred `DatabaseLogger`. Imamo torej dva razreda, `Logger` in `DatabaseLogger`, eden zapisuje v datoteko, drugi v podatkovno bazo … se vam pri tem poimenovanju ne zdi nekaj čudnega? Ali ne bi bilo bolje preimenovati `Logger` v `FileLogger`? Zagotovo da. - -Ampak naredili bomo pametno. Pod prvotnim imenom bomo ustvarili vmesnik: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -… ki ga bosta oba loggerja implementirala: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -In zahvaljujoč temu ne bo treba ničesar spreminjati v preostalem delu kode, kjer se logger uporablja. Na primer konstruktor razreda `NewsletterDistributor` bo še vedno zadovoljen s tem, da kot parameter zahteva `Logger`. In samo od nas bo odvisno, katero instanco mu bomo predali. - -**Zato nikoli ne dajemo imenom vmesnikov pripone `Interface` ali predpone `I`.** Sicer ne bi bilo mogoče kode tako lepo razvijati. - - -Houston, imamo problem ----------------------- - -Medtem ko si lahko v celotni aplikaciji zadostujemo z eno samo instanco loggerja, bodisi datotečnega ali podatkovnega, in ga preprosto predajamo povsod tam, kjer se nekaj logira, je povsem drugače v primeru razreda `Article`. Njegove instance namreč ustvarjamo po potrebi, lahko tudi večkrat. Kako se spopasti s povezavo na podatkovno bazo v njegovem konstruktorju? - -Kot primer lahko služi kontroler, ki mora po oddaji obrazca shraniti članek v podatkovno bazo: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Možna rešitev se ponuja kar sama: pustimo si objekt podatkovne baze predati s konstruktorjem v `EditController` in uporabimo `$article = new Article($this->db)`. - -Enako kot v prejšnjem primeru z `Logger` in potjo do datoteke, to ni pravilen postopek. Podatkovna baza ni odvisnost `EditController`, ampak `Article`. Predajanje podatkovne baze torej gre proti [pravilu št. 2: vzemi, kar je tvoje |#Pravilo št. 2: vzemi kar je tvoje]. Ko se spremeni konstruktor razreda `Article` (doda se nov parameter), bo treba prilagoditi tudi kodo na vseh mestih, kjer se ustvarjajo instance. Ufff. - -Houston, kaj predlagaš? - - -Pravilo št. 3: prepusti tovarni -------------------------------- - -S tem, ko smo odpravili skrite povezave in vse odvisnosti predajamo kot argumente, smo dobili bolj nastavljive in prožne razrede. In zato potrebujemo še nekaj drugega, kar nam bo te prožnejše razrede ustvarilo in konfiguriralo. Temu bomo rekli tovarne. - -Pravilo se glasi: če ima razred odvisnosti, prepusti ustvarjanje njihovih instanc tovarni. - -Tovarne so pametnejša zamenjava za operator `new` v svetu dependency injection. - -.[note] -Prosimo, ne zamenjujte z načrtovalskim vzorcem *factory method*, ki opisuje specifičen način uporabe tovarn in s to temo ni povezan. - - -Tovarna -------- - -Tovarna je metoda ali razred, ki izdeluje in konfigurira objekte. Razred, ki izdeluje `Article`, bomo poimenovali `ArticleFactory` in bi lahko izgledal na primer takole: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Njegova uporaba v kontrolerju bo naslednja: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // pustimo tovarni ustvariti objekt - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Če se v tem trenutku spremeni signatura konstruktorja razreda `Article`, je edini del kode, ki se mora na to odzvati, sama tovarna `ArticleFactory`. Vse ostale kode, ki delajo z objekti `Article`, kot na primer `EditController`, se to nikakor ne dotakne. - -Morda si zdaj trkate po čelu, ali smo si sploh pomagali. Količina kode se je povečala in vse skupaj začenja izgledati sumljivo zapleteno. - -Ne skrbite, kmalu bomo prišli do Nette DI vsebnika. In ta ima vrsto asov v rokavu, s katerimi gradnjo aplikacij, ki uporabljajo dependency injection, neizmerno poenostavi. Tako na primer namesto razreda `ArticleFactory` bo zadostovalo [napisati zgolj vmesnik |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Ampak to prehitevamo, še počakajte :-) - - -Povzetek --------- - -Na začetku tega poglavja smo obljubili, da si bomo pokazali postopek, kako načrtovati čisto kodo. Zadostuje razredom - -1) [predajati odvisnosti, ki jih potrebujejo |#Pravilo št. 1: naj ti bo predano] -2) [in nasprotno ne predajati, česar neposredno ne potrebujejo |#Pravilo št. 2: vzemi kar je tvoje] -3) [in da se objekti z odvisnostmi najbolje izdelujejo v tovarnah |#Pravilo št. 3: prepusti tovarni] - -Morda se na prvi pogled ne zdi tako, a ta tri pravila imajo daljnosežne posledice. Vodijo k radikalno drugačnemu pogledu na načrtovanje kode. Se splača? Programerji, ki so opustili stare navade in začeli dosledno uporabljati dependency injection, menijo, da je ta korak ključni trenutek v njihovem poklicnem življenju. Odprl se jim je svet preglednih in vzdržljivih aplikacij. - -Kaj pa, če koda dosledno ne uporablja dependency injection? Kaj če je zgrajena na statičnih metodah ali singletonih? Ali to prinaša kakšne težave? [Prinaša in zelo bistvene |global-state]. diff --git a/dependency-injection/sl/nette-container.texy b/dependency-injection/sl/nette-container.texy deleted file mode 100644 index a74d0f73bc..0000000000 --- a/dependency-injection/sl/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI Vsebnik -**************** - -.[perex] -Nette DI je ena izmed najbolj zanimivih knjižnic Nette. Zna generirati in samodejno posodabljati prevedene DI vsebnike, ki so izjemno hitri in neverjetno enostavni za konfiguracijo. - -Podobo storitev, ki jih mora ustvarjati DI vsebnik, definiramo običajno s pomočjo konfiguracijskih datotek v [formatu NEON|neon:format]. Vsebnik, ki smo ga ročno ustvarili v [prejšnjem poglavju|container], bi se zapisal takole: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Zapis je resnično kratek. - -Vse odvisnosti, deklarirane v konstruktorjih razredov `ArticleFactory` in `UserController`, si Nette DI sam ugotovi in preda zahvaljujoč t.i. [autowiringu|autowiring], v konfiguracijski datoteki zato ni treba ničesar navajati. Torej tudi če pride do spremembe parametrov, vam ni treba v konfiguraciji ničesar spreminjati. Nette vsebnik samodejno pregenerira. Vi se lahko tam osredotočite izključno na razvoj aplikacije. - -Če želimo odvisnosti predajati s pomočjo setterjev, uporabimo za to sekcijo [setup |services#Setup]. - -Nette DI generira neposredno PHP kodo vsebnika. Rezultat je torej datoteka `.php`, ki jo lahko odprete in preučujete. Zahvaljujoč temu natančno vidite, kako vsebnik deluje. Lahko ga tudi razhroščujete v IDE in korakate skozi. In predvsem: generirana PHP koda je izjemno hitra. - -Nette DI zna tudi generirati kodo [tovarn|factory] na podlagi posredovanega vmesnika. Zato namesto razreda `ArticleFactory` bo zadostovalo ustvariti v aplikaciji le vmesnik: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Celoten primer najdete [na GitHubu|https://github.com/nette-examples/di-example-doc]. - - -Samostojna uporaba ------------------- - -Uvedba knjižnice Nette DI v aplikacijo je zelo enostavna. Najprej jo namestimo s Composerjem (ker je prenašanje zipov taaaako zastarelo): - -```shell -composer require nette/di -``` - -Naslednja koda ustvari instanco DI vsebnika glede na konfiguracijo, shranjeno v datoteki `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Vsebnik se generira le enkrat, njegova koda se zapiše v predpomnilnik (imenik `__DIR__ . '/temp'`) in pri naslednjih zahtevah se le še od tam naloži. - -Za ustvarjanje in pridobivanje storitev služita metodi `getService()` ali `getByType()`. Tako ustvarimo objekt `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -Med razvojem je koristno aktivirati način samodejnega osveževanja, ko se vsebnik samodejno pregenerira, če pride do spremembe kateregakoli razreda ali konfiguracijske datoteke. Zadostuje, da v konstruktorju `ContainerLoader` navedete kot drugi argument `true`. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Uporaba z ogrodjem Nette ------------------------- - -Kot smo pokazali, uporaba Nette DI ni omejena na aplikacije, napisane v Nette Frameworku, lahko ga s pomočjo le 3 vrstic kode uvedete kjerkoli. Če pa razvijate aplikacije v Nette Frameworku, ima konfiguracijo in ustvarjanje vsebnika na skrbi [Bootstrap |application:bootstrapping#Konfiguracija DI vsebnika]. diff --git a/dependency-injection/sl/passing-dependencies.texy b/dependency-injection/sl/passing-dependencies.texy deleted file mode 100644 index aa75ecf278..0000000000 --- a/dependency-injection/sl/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Predajanje odvisnosti -********************* - -<div class=perex> - -Argumente ali v terminologiji DI „odvisnosti“ lahko v razrede predajamo na naslednje glavne načine: - -* predajanje s konstruktorjem -* predajanje z metodo (t.i. setterjem) -* nastavitev spremenljivke -* z metodo, anotacijo ali atributom *inject* - -</div> - -Zdaj si bomo posamezne variante pokazali na konkretnih primerih. - - -Predajanje s konstruktorjem -=========================== - -Odvisnosti se predajajo v trenutku ustvarjanja objekta kot argumenti konstruktorja: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Ta oblika je primerna za obvezne odvisnosti, ki jih razred nujno potrebuje za svoje delovanje, saj brez njih instance ne bo mogoče ustvariti. - -Od PHP 8.0 lahko uporabimo krajšo obliko zapisa ([constructor property promotion |https://blog.nette.org/sl/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), ki je funkcionalno ekvivalentna: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Od PHP 8.1 lahko spremenljivko označimo z zastavico `readonly`, ki deklarira, da se vsebina spremenljivke ne bo več spremenila: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -DI vsebnik preda konstruktorju odvisnosti samodejno s pomočjo [autowiringa |autowiring]. Argumente, ki jih na ta način ni mogoče predati (npr. nizi, števila, booleani) [zapišemo v konfiguraciji |services#Argumenti]. - - -Constructor hell ----------------- - -Izraz *constructor hell* označuje situacijo, ko potomec deduje od starševskega razreda, katerega konstruktor zahteva odvisnosti, in hkrati potomec zahteva odvisnosti. Pri tem mora prevzeti in predati tudi starševske: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ CONSTRUCTOR HELL - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Težava nastane v trenutku, ko bomo želeli spremeniti konstruktor razreda `BaseClass`, na primer ko se doda nova odvisnost. Potem je namreč treba prilagoditi tudi vse konstruktorje potomcev. Kar iz takšne prilagoditve naredi pekel. - -Kako temu preprečiti? Rešitev je **dajati prednost [kompoziciji pred dedovanjem |faq#Zakaj se daje prednost kompoziciji pred dedovanjem]**. - -Torej bomo kodo zasnovali drugače. Izogibali se bomo [abstraktnim |nette:introduction-to-object-oriented-programming#Abstraktni razredi] `Base*` razredom. Namesto da bi `MyClass` pridobival določeno funkcionalnost s tem, da deduje od `BaseClass`, si bo to funkcionalnost pustil predati kot odvisnost: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Predajanje s setterjem -====================== - -Odvisnosti se predajajo s klicem metode, ki jih shrani v zasebno spremenljivko. Običajna konvencija poimenovanja teh metod je oblika `set*()`, zato se jim reče setterji, vendar se lahko seveda imenujejo kakorkoli drugače. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Ta način je primeren za neobvezne odvisnosti, ki niso nujne za delovanje razreda, saj ni zagotovljeno, da bo objekt odvisnost dejansko prejel (tj. da bo uporabnik metodo poklical). - -Hkrati ta način dopušča ponavljajoče klicanje setterja in s tem spreminjanje odvisnosti. Če to ni zaželeno, dodamo v metodo preverjanje ali od PHP 8.1 označimo lastnost `$cache` z zastavico `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('Odvisnost je že bila nastavljena'); - } - $this->cache = $cache; - } -} -``` - -Klic setterja definiramo v konfiguraciji DI vsebnika v [ključu setup |services#Setup]. Tudi tukaj se uporablja samodejno predajanje odvisnosti s pomočjo autowiringa: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Nastavitev spremenljivke -======================== - -Odvisnosti se predajajo z zapisom neposredno v člansko spremenljivko: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Ta način se šteje za neprimernega, ker mora biti članska spremenljivka deklarirana kot `public`. In zato nimamo nadzora nad tem, da bo predana odvisnost dejansko danega tipa (veljalo pred PHP 7.4) in izgubimo možnost reagirati na novo dodeljeno odvisnost z lastno kodo, na primer preprečiti nadaljnjo spremembo. Hkrati spremenljivka postane del javnega vmesnika razreda, kar morda ni zaželeno. - -Nastavitev spremenljivke definiramo v konfiguraciji DI vsebnika v [sekciji setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Medtem ko prejšnji trije načini veljajo na splošno v vseh objektno usmerjenih jezikih, je vbrizgavanje z metodo, anotacijo ali atributom *inject* specifično izključno za presenterje v Nette. O njih govori [samostojno poglavje |best-practices:inject-method-attribute]. - - -Kateri način izbrati? -===================== - -- konstruktor je primeren za obvezne odvisnosti, ki jih razred nujno potrebuje za svoje delovanje -- setter je nasprotno primeren za neobvezne odvisnosti ali odvisnosti, ki jih je mogoče še naprej spreminjati -- javne spremenljivke niso primerne diff --git a/dependency-injection/sl/services.texy b/dependency-injection/sl/services.texy deleted file mode 100644 index d3a0e1bc48..0000000000 --- a/dependency-injection/sl/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Definiranje storitev -******************** - -.[perex] -Konfiguracija je mesto, kjer učimo DI vsebnik, kako naj sestavlja posamezne storitve in kako jih povezuje z drugimi odvisnostmi. Nette ponuja zelo pregleden in eleganten način, kako to doseči. - -Sekcija `services` v konfiguracijski datoteki formata NEON je mesto, kjer definiramo lastne storitve in njihove konfiguracije. Poglejmo si preprost primer definicije storitve, imenovane `database`, ki predstavlja instanco razreda `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Navedena konfiguracija bo vodila do naslednje tovarne metode v [DI vsebniku|container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Imena storitev nam omogočajo, da se nanje sklicujemo v drugih delih konfiguracijske datoteke, in sicer v formatu `@imeStoritve`. Če storitve ni treba poimenovati, lahko preprosto uporabimo le alinejo: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Za pridobitev storitve iz DI vsebnika lahko uporabimo metodo `getService()` z imenom storitve kot parametrom ali metodo `getByType()` s tipom storitve: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Ustvarjanje storitve -==================== - -Večinoma ustvarimo storitev preprosto tako, da ustvarimo instanco določenega razreda. Na primer: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Če moramo konfiguracijo razširiti z dodatnimi ključi, lahko definicijo razpišemo v več vrstic: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Ključ `create` ima alias `factory`, obe varianti sta v praksi pogosti. Vendar priporočamo uporabo `create`. - -Argumenti konstruktorja ali ustvarjalne metode so lahko alternativno zapisani v ključu `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Storitve ni treba ustvarjati le s preprostim ustvarjanjem instance razreda, lahko so tudi rezultat klica statičnih metod ali metod drugih storitev: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Opazite, da se za enostavnost namesto `->` uporablja `::`, glej [#Izrazna sredstva]. Generirale se bodo te tovarne metode: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -DI vsebnik mora poznati tip ustvarjene storitve. Če ustvarjamo storitev s pomočjo metode, ki nima specificiranega vrnjenega tipa, moramo ta tip eksplicitno navesti v konfiguraciji: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Argumenti -========= - -V konstruktor in metode predajamo argumente na način, ki je zelo podoben kot v samem PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Za boljšo berljivost lahko argumente razpišemo v ločene vrstice. V takem primeru je uporaba vejic neobvezna: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Argumente lahko tudi poimenujete in vam ni treba skrbeti za njihov vrstni red: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Če želite nekatere argumente izpustiti in uporabiti njihovo privzeto vrednost ali dodati storitev s pomočjo [autowiringa|autowiring], uporabite podčrtaj: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Kot argumente lahko predajate storitve, uporabljate parametre in še veliko več, glej [#Izrazna sredstva]. - - -Setup -===== - -V sekciji `setup` definiramo metode, ki se morajo poklicati pri ustvarjanju storitve. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -To bi v PHP izgledalo takole: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Poleg klicanja metod lahko tudi predajate vrednosti v lastnosti. Podprto je tudi dodajanje elementa v polje, ki ga je treba zapisati v narekovajih, da ne pride do kolizije s sintakso NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Kar bi v PHP kodi izgledalo takole: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -V setupu lahko pa kličete tudi statične metode ali metode drugih storitev. Če morate kot argument predati trenutno storitev, jo navedite kot `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Opazite, da se za enostavnost namesto `->` uporablja `::`, glej [#Izrazna sredstva]. Generirala se bo takšna tovarna metoda: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Izrazna sredstva -================ - -Nette DI nam daje izjemno bogata izrazna sredstva, s katerimi lahko zapišemo skoraj karkoli. V konfiguracijskih datotekah lahko tako uporabljamo [parametre |configuration#Parametri]: - -```neon -# parameter -%wwwDir% - -# vrednost parametra pod ključem -%mailer.user% - -# parameter znotraj niza -'%wwwDir%/images' -``` - -Nadalje ustvarjati objekte, klicati metode in funkcije: - -```neon -# ustvarjanje objekta -DateTime() - -# klic statične metode -Collator::create(%locale%) - -# klic PHP funkcije -::getenv(DB_USER) -``` - -Sklicujemo se na storitve bodisi po njihovem imenu ali s pomočjo tipa: - -```neon -# storitev po imenu -@database - -# storitev po tipu -@Nette\Database\Connection -``` - -Uporabljati first-class callable sintakso: .{data-version:3.2.0} - -```neon -# ustvarjanje povratnega klica, podobno [@user, logout] -@user::logout(...) -``` - -Uporabljati konstante: - -```neon -# konstanta razreda -FilesystemIterator::SKIP_DOTS - -# globalno konstanto dobimo s PHP funkcijo constant() -::constant(PHP_VERSION) -``` - -Klicanje metod lahko verižimo enako kot v PHP. Le za enostavnost se namesto `->` uporablja `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Te izraze lahko uporabljate kjerkoli, pri [ustvarjanju storitev |#Ustvarjanje storitve], v [argumentih |#Argumenti], v sekciji [#setup] ali [parametrih |configuration#Parametri]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Posebne funkcije ----------------- - -V konfiguracijskih datotekah lahko uporabljate te posebne funkcije: - -- `not()` negacija vrednosti -- `bool()`, `int()`, `float()`, `string()` pretvorba brez izgube v dani tip -- `typed()` ustvari polje vseh storitev specificiranega tipa -- `tagged()` ustvari polje vseh storitev z dano oznako - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -V primerjavi s klasično pretvorbo v PHP, kot je npr. `(int)`, pretvorba brez izgube vrže izjemo za neštevilske vrednosti. - -Funkcija `typed()` ustvari polje vseh storitev danega tipa (razred ali vmesnik). Izpusti storitve, ki imajo izklopljen autowiring. Lahko navedete tudi več tipov, ločenih z vejico. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Polje storitev določenega tipa lahko predajate kot argument tudi samodejno s pomočjo [autowiringa |autowiring#Polje storitev]. - -Funkcija `tagged()` pa ustvarja polje vseh storitev z določeno oznako. Tudi tukaj lahko specificirate več oznak, ločenih z vejico. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Ključ `autowired` omogoča vplivanje na obnašanje autowiringa za specifično storitev. Za podrobnosti glej [poglavje o autowiringu|autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # storitev foo je izključena iz autowiringa -``` - - -Lazy storitve .{data-version:3.2.4} -=================================== - -Lazy loading je tehnika, ki odloži ustvarjanje storitve do trenutka, ko je dejansko potrebna. V globalni konfiguraciji lahko [omogočite lazy ustvarjanje |configuration#Lene storitve] za vse storitve hkrati. Za posamezne storitve pa lahko to obnašanje prepišete: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Ko je storitev definirana kot lazy, ob njeni zahtevi iz DI vsebnika dobimo poseben nadomestni objekt. Ta izgleda in se obnaša enako kot dejanska storitev, vendar se dejanska inicializacija (klic konstruktorja in setupa) zgodi šele ob prvem klicu katerekoli njene metode ali lastnosti. - -.[note] -Lazy loading je mogoče uporabiti samo za uporabniške razrede, ne pa za notranje PHP razrede. Zahteva PHP 8.4 ali novejšo različico. - - -Oznake -====== - -Oznake (tags) služijo za dodajanje dopolnilnih informacij k storitvam. Storitvi lahko dodate eno ali več oznak: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Oznake lahko nosijo tudi vrednosti: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Da bi dobili vse storitve z določenimi oznakami, lahko uporabite funkcijo `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -V DI vsebniku lahko dobite imena vseh storitev z določeno oznako s pomočjo metode `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names je polje, ki vsebuje ime storitve in vrednost oznake -// npr. ['foo' => 'monolog.logger.event', ...] -``` - - -Način Inject -============ - -S pomočjo zastavice `inject: true` se aktivira predajanje odvisnosti preko javnih spremenljivk z anotacijo [inject |best-practices:inject-method-attribute#Atributi Inject] in metod [inject*() |best-practices:inject-method-attribute#Metode inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -Privzeto je `inject` aktiviran samo za presenterje. - - -Modifikacija storitev -===================== - -DI vsebnik vsebuje veliko storitev, ki so bile dodane preko vgrajene ali [uporabniške razširitve|extensions]. Definicije teh storitev lahko prilagodite neposredno v konfiguraciji. Na primer, lahko spremenite razred storitve `application.application`, ki je standardno `Nette\Application\Application`, na drugega: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Zastavica `alteration` je informativna in pove, da le modificiramo obstoječo storitev. - -Lahko tudi dopolnimo setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -Pri prepisovanju storitve lahko želimo odstraniti prvotne argumente, postavke setupa ali oznake, za kar služi `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Če želite odstraniti storitev, dodano z razširitvijo, lahko to storite takole: - -```neon -services: - cache.journal: false -``` diff --git a/dependency-injection/uk/@home.texy b/dependency-injection/uk/@home.texy deleted file mode 100644 index 7a92429299..0000000000 --- a/dependency-injection/uk/@home.texy +++ /dev/null @@ -1,21 +0,0 @@ -Nette DI -******** - -.[perex] -Dependency Injection — це патерн проектування, який кардинально змінить ваш погляд на код та розробку. Він відкриє вам шлях до світу чисто спроектованих та підтримуваних застосунків. - -- [Що таке Dependency Injection? |introduction] -- [Глобальний стан та синглтони |global-state] -- [Передача залежностей |passing-dependencies] -- [Що таке DI-контейнер? |container] -- [Часті питання|faq] - - -Пакет `nette/di` надає надзвичайно просунутий компільований DI-контейнер для PHP. - -- [Nette DI Container |nette-container] -- [Конфігурація |configuration] -- [Визначення сервісів |services] -- [Autowiring |autowiring] -- [Згенеровані фабрики |factory] -- [Створення розширень для Nette DI|extensions] diff --git a/dependency-injection/uk/@left-menu.texy b/dependency-injection/uk/@left-menu.texy deleted file mode 100644 index 1c592f52fd..0000000000 --- a/dependency-injection/uk/@left-menu.texy +++ /dev/null @@ -1,17 +0,0 @@ -Dependency Injection -******************** -- [Що таке DI? |introduction] -- [Глобальний стан та синглтони |global-state] -- [Передача залежностей |passing-dependencies] -- [Що таке DI-контейнер? |container] -- [Часті питання|faq] - - -Nette DI --------- -- [Nette DI Container |nette-container] -- [Конфігурація |configuration] -- [Визначення сервісів |services] -- [Autowiring |autowiring] -- [Згенеровані фабрики |factory] -- [Створення розширень для Nette DI|extensions] diff --git a/dependency-injection/uk/@meta.texy b/dependency-injection/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/dependency-injection/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/dependency-injection/uk/autowiring.texy b/dependency-injection/uk/autowiring.texy deleted file mode 100644 index 387589ccd9..0000000000 --- a/dependency-injection/uk/autowiring.texy +++ /dev/null @@ -1,258 +0,0 @@ -Автоматичне підключення -*********************** - -.[perex] -Автоматичне підключення (Autowiring) — це чудова функція, яка вміє автоматично передавати до конструктора та інших методів необхідні сервіси, тому нам не потрібно їх взагалі писати. Це заощадить вам багато часу. - -Завдяки цьому ми можемо пропустити переважну більшість аргументів при написанні визначень сервісів. Замість: - -```neon -services: - articles: Model\ArticleRepository(@database, @cache.storage) -``` - -Достатньо написати: - -```neon -services: - articles: Model\ArticleRepository -``` - -Автоматичне підключення керується типами, тому для його роботи клас `ArticleRepository` має бути визначений приблизно так: - -```php -namespace Model; - -class ArticleRepository -{ - public function __construct(\PDO $db, \Nette\Caching\Storage $storage) - {} -} -``` - -Щоб можна було використовувати автоматичне підключення, для кожного типу в контейнері має бути **рівно один сервіс**. Якщо їх буде більше, автоматичне підключення не знатиме, який з них передати, і викине виняток: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - tempDb: PDO('sqlite::memory:') - articles: Model\ArticleRepository # ВИКИНЕ ВИНЯТОК, підходять mainDb і tempDb -``` - -Рішенням було б або обійти автоматичне підключення та явно вказати назву сервісу (тобто `articles: Model\ArticleRepository(@mainDb)`). Але зручніше [вимкнути |#Вимкнення автоматичного підключення] автоматичне підключення одного з сервісів або [надати перевагу |#Перевага автоматичного підключення] першому сервісу. - - -Вимкнення автоматичного підключення ------------------------------------ - -Автоматичне підключення сервісу можна вимкнути за допомогою опції `autowired: no`: - -```neon -services: - mainDb: PDO(%dsn%, %user%, %password%) - - tempDb: - create: PDO('sqlite::memory:') - autowired: false # сервіс tempDb виключено з автоматичного підключення - - articles: Model\ArticleRepository # отже, передасть до конструктора mainDb -``` - -Сервіс `articles` не викине виняток, що існують два відповідні сервіси типу `PDO` (тобто `mainDb` та `tempDb`), які можна передати до конструктора, оскільки він бачить лише сервіс `mainDb`. - -.[note] -Конфігурація автоматичного підключення в Nette працює інакше, ніж у Symfony, де опція `autowire: false` вказує, що не слід використовувати автоматичне підключення для аргументів конструктора даного сервісу. У Nette автоматичне підключення використовується завжди, чи то для аргументів конструктора, чи для будь-яких інших методів. Опція `autowired: false` вказує, що екземпляр даного сервісу не повинен передаватися нікуди за допомогою автоматичного підключення. - - -Перевага автоматичного підключення ----------------------------------- - -Якщо у нас є кілька сервісів одного типу і для одного з них ми вказуємо опцію `autowired`, цей сервіс стає пріоритетним: - -```neon -services: - mainDb: - create: PDO(%dsn%, %user%, %password%) - autowired: PDO # стає пріоритетним - - tempDb: - create: PDO('sqlite::memory:') - - articles: Model\ArticleRepository -``` - -Сервіс `articles` не викине виняток, що існують два відповідні сервіси типу `PDO` (тобто `mainDb` та `tempDb`), але використає пріоритетний сервіс, тобто `mainDb`. - - -Масив сервісів --------------- - -Автоматичне підключення вміє передавати і масиви сервісів певного типу. Оскільки в PHP неможливо нативно записати тип елементів масиву, потрібно крім типу `array` додати phpDoc коментар з типом елемента у форматі `ClassName[]`: - -```php -namespace Model; - -class ShipManager -{ - /** - * @param Shipper[] $shippers - */ - public function __construct(array $shippers) - {} -} -``` - -DI-контейнер потім автоматично передасть масив сервісів, що відповідають даному типу. Він пропустить сервіси, у яких вимкнено автоматичне підключення. - -Тип у коментарі може бути також у форматі `array<int, Class>` або `list<Class>`. Якщо ви не можете вплинути на вигляд phpDoc коментаря, ви можете передати масив сервісів безпосередньо в конфігурації за допомогою [`typed()` |services#Спеціальні функції]. - - -Скалярні аргументи ------------------- - -Автоматичне підключення вміє підставляти лише об'єкти та масиви об'єктів. Скалярні аргументи (наприклад, рядки, числа, булеві значення) [запишемо в конфігурації |services#Аргументи]. Альтернативою є створення [об'єкта налаштувань |best-practices:passing-settings-to-presenters], який інкапсулює скалярне значення (або кілька значень) у вигляді об'єкта, і його потім можна знову передавати за допомогою автоматичного підключення. - -```php -class MySettings -{ - public function __construct( - // readonly можна використовувати з PHP 8.1 - public readonly bool $value, - ) - {} -} -``` - -Ви створите з нього сервіс, додавши до конфігурації: - -```neon -services: - - MySettings('any value') -``` - -Усі класи потім запитають його за допомогою автоматичного підключення. - - -Звуження автоматичного підключення ----------------------------------- - -Для окремих сервісів можна звузити автоматичне підключення лише до певних класів або інтерфейсів. - -Зазвичай автоматичне підключення передає сервіс до кожного параметра методу, типу якого сервіс відповідає. Звуження означає, що ми встановлюємо умови, яким повинні відповідати типи, зазначені у параметрах методів, щоб їм було передано сервіс. - -Покажемо це на прикладі: - -```php -class ParentClass -{} - -class ChildClass extends ParentClass -{} - -class ParentDependent -{ - function __construct(ParentClass $obj) - {} -} - -class ChildDependent -{ - function __construct(ChildClass $obj) - {} -} -``` - -Якщо ми зареєструємо їх усі як сервіси, то автоматичне підключення зазнає невдачі: - -```neon -services: - parent: ParentClass - child: ChildClass - parentDep: ParentDependent # ВИКИНЕ ВИНЯТОК, підходять сервіси parent і child - childDep: ChildDependent # автоматичне підключення передасть до конструктора сервіс child -``` - -Сервіс `parentDep` викине виняток `Multiple services of type ParentClass found: parent, child`, оскільки до його конструктора підходять обидва сервіси `parent` і `child`, і автоматичне підключення не може вирішити, який з них вибрати. - -Тому для сервісу `child` ми можемо звузити його автоматичне підключення до типу `ChildClass`: - -```neon -services: - parent: ParentClass - child: - create: ChildClass - autowired: ChildClass # можна написати і 'autowired: self' - - parentDep: ParentDependent # автоматичне підключення передасть до конструктора сервіс parent - childDep: ChildDependent # автоматичне підключення передасть до конструктора сервіс child -``` - -Тепер до конструктора сервісу `parentDep` передається сервіс `parent`, оскільки тепер це єдиний відповідний об'єкт. Сервіс `child` автоматичне підключення туди вже не передасть. Так, сервіс `child` все ще є типу `ParentClass`, але вже не виконується звужуюча умова, задана для типу параметра, тобто не виконується, що `ParentClass` *є надтипом* `ChildClass`. - -Для сервісу `child` можна було б `autowired: ChildClass` записати також як `autowired: self`, оскільки `self` є заповнювачем для класу поточного сервісу. - -У ключі `autowired` можна вказати і кілька класів або інтерфейсів як масив: - -```neon -autowired: [BarClass, FooInterface] -``` - -Спробуємо доповнити приклад ще інтерфейсами: - -```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) - {} -} -``` - -Якщо ми ніяк не обмежимо сервіс `child`, він підійде до конструкторів усіх класів `FooDependent`, `BarDependent`, `ParentDependent` та `ChildDependent`, і автоматичне підключення його туди передасть. - -Але якщо ми звузимо його автоматичне підключення до `ChildClass` за допомогою `autowired: ChildClass` (або `self`), автоматичне підключення передасть його лише до конструктора `ChildDependent`, оскільки він вимагає аргумент типу `ChildClass` і виконується умова, що `ChildClass` *є типу* `ChildClass`. Жоден інший тип, зазначений у інших параметрах, не є надтипом `ChildClass`, тому сервіс не передається. - -Якщо ми обмежимо його до `ParentClass` за допомогою `autowired: ParentClass`, автоматичне підключення знову передасть його до конструктора `ChildDependent` (оскільки необхідний `ChildClass` є надтипом `ParentClass`) і тепер також до конструктора `ParentDependent`, оскільки необхідний тип `ParentClass` також є відповідним. - -Якщо ми обмежимо його до `FooInterface`, він все одно буде автоматично підключений до `ParentDependent` (необхідний `ParentClass` є надтипом `FooInterface`) та `ChildDependent`, але крім того, і до конструктора `FooDependent`, однак не до `BarDependent`, оскільки `BarInterface` не є надтипом `FooInterface`. - -```neon -services: - child: - create: ChildClass - autowired: FooInterface - - fooDep: FooDependent # автоматичне підключення передасть до конструктора child - barDep: BarDependent # ВИКИНЕ ВИНЯТОК, жоден сервіс не відповідає - parentDep: ParentDependent # автоматичне підключення передасть до конструктора child - childDep: ChildDependent # автоматичне підключення передасть до конструктора child -``` diff --git a/dependency-injection/uk/configuration.texy b/dependency-injection/uk/configuration.texy deleted file mode 100644 index be8b7c4869..0000000000 --- a/dependency-injection/uk/configuration.texy +++ /dev/null @@ -1,326 +0,0 @@ -Конфігурація DI-контейнера -************************** - -.[perex] -Огляд конфігураційних опцій для Nette DI-контейнера. - - -Конфігураційний файл -==================== - -Nette DI-контейнер легко керується за допомогою конфігураційних файлів. Вони зазвичай записуються у [форматі NEON|neon:format]. Для редагування рекомендуємо [редактори з підтримкою |best-practices:editors-and-tools#IDE редактор] цього формату. - -<pre> -"decorator .[prism-token prism-atrule]":[#decorator]: "Декоратор .[prism-token prism-comment]"<br> -"di .[prism-token prism-atrule]":[#DI]: "DI-контейнер .[prism-token prism-comment]"<br> -"extensions .[prism-token prism-atrule]":[#Розширення]: "Встановлення додаткових DI-розширень .[prism-token prism-comment]"<br> -"includes .[prism-token prism-atrule]":[#Включення файлів]: "Включення файлів .[prism-token prism-comment]"<br> -"parameters .[prism-token prism-atrule]":[#Параметри]: "Параметри .[prism-token prism-comment]"<br> -"search .[prism-token prism-atrule]":[#Search]: "Автоматична реєстрація сервісів .[prism-token prism-comment]"<br> -"services .[prism-token prism-atrule]":[services]: "Сервіси .[prism-token prism-comment]" -</pre> - -.[note] -Щоб записати рядок, що містить символ `%`, потрібно його екранувати, подвоївши до `%%`. - - -Параметри -========= - -У конфігурації можна визначити параметри, які потім можна використовувати як частину визначень сервісів. Це може зробити конфігурацію більш зрозумілою або об'єднати та виділити значення, які будуть змінюватися. - -```neon -parameters: - dsn: 'mysql:host=127.0.0.1;dbname=test' - user: root - password: secret -``` - -На параметр `dsn` можна посилатися будь-де в конфігурації записом `%dsn%`. Параметри можна використовувати і всередині рядків, як `'%wwwDir%/images'`. - -Параметри не обов'язково мають бути лише рядками або числами, вони також можуть містити масиви: - -```neon -parameters: - mailer: - host: smtp.example.com - secure: ssl - user: franta@gmail.com - languages: [cs, en, de] -``` - -На конкретний ключ можна посилатися як `%mailer.user%`. - -Якщо вам потрібно у вашому коді, наприклад, у класі, дізнатися значення будь-якого параметра, передайте його до цього класу. Наприклад, у конструкторі. Не існує жодного глобального об'єкта, що представляє конфігурацію, до якого класи могли б звертатися за значеннями параметрів. Це було б порушенням принципу dependency injection. - - -Сервіси -======= - -Див. [окремий розділ|services]. - - -Decorator -========= - -Як масово змінити всі сервіси певного типу? Наприклад, викликати певний метод у всіх presenter'ів, які успадковують від конкретного спільного предка? Для цього існує decorator. - -```neon -decorator: - # для всіх сервісів, що є екземплярами цього класу або інтерфейсу - App\Presentation\BasePresenter: - setup: - - setProjectId(10) # виклич цей метод - - $absoluteUrls = true # і встанови змінну -``` - -Decorator можна також використовувати для налаштування [тегів |services#Теги] або ввімкнення режиму [inject |services#Режим Inject]. - -```neon -decorator: - InjectableInterface: - tags: [mytag: 1] - inject: true -``` - - -DI -=== - -Технічні налаштування DI-контейнера. - -```neon -di: - # показати DI-контейнер у Tracy Bar? - debugger: ... # (bool) за замовчуванням true - - # типи параметрів, які ніколи не підключати автоматично - excluded: ... # (string[]) - - # дозволити ліниве створення сервісів? - lazy: ... # (bool) за замовчуванням false - - # клас, від якого успадковується DI-контейнер - parentClass: ... # (string) за замовчуванням Nette\DI\Container -``` - - -Lazy-сервіси .{data-version:3.2.4} ----------------------------------- - -Налаштування `lazy: true` активує ліниве (відкладене) створення сервісів. Це означає, що сервіси не створюються насправді в момент, коли ми їх запитуємо з DI-контейнера, а лише в момент їх першого використання. Це може прискорити запуск програми та зменшити споживання пам'яті, оскільки створюються лише ті сервіси, які дійсно потрібні в даному запиті. - -Для конкретного сервісу ліниве створення можна [змінити |services#Lazy-сервіси]. - -.[note] -Ліниві об'єкти можна використовувати лише для користувацьких класів, а не для внутрішніх класів PHP. Потребує PHP 8.4 або новішої версії. - - -Експорт метаданих ------------------ - -Клас DI-контейнера містить також багато метаданих. Ви можете зменшити його розмір, скоротивши експорт метаданих. - -```neon -di: - export: - # експортувати параметри? - parameters: false # (bool) за замовчуванням true - - # експортувати теги і які? - tags: # (string[]|bool) за замовчуванням всі - - event.subscriber - - # експортувати дані для автопідключення і які? - types: # (string[]|bool) за замовчуванням всі - - Nette\Database\Connection - - Symfony\Component\Console\Application -``` - -Якщо ви не використовуєте масив `$container->getParameters()`, ви можете вимкнути експорт параметрів. Далі, ви можете експортувати лише ті теги, через які ви отримуєте сервіси методом `$container->findByTag(...)`. Якщо ви взагалі не викликаєте цей метод, ви можете повністю вимкнути експорт тегів за допомогою `false`. - -Ви можете значно скоротити метадані для [автоматичного підключення|autowiring], вказавши класи, які ви використовуєте як параметр методу `$container->getByType()`. І знову ж таки, якщо ви взагалі не викликаєте цей метод (або лише в [bootstrap|application:bootstrapping] для отримання `Nette\Application\Application`), ви можете повністю вимкнути експорт за допомогою `false`. - - -Розширення -========== - -Реєстрація додаткових DI-розширень. Таким чином додамо, наприклад, DI-розширення `Dibi\Bridges\Nette\DibiExtension22` під назвою `dibi`. - -```neon -extensions: - dibi: Dibi\Bridges\Nette\DibiExtension22 -``` - -Потім ми конфігуруємо його в секції `dibi`: - -```neon -dibi: - host: localhost -``` - -Як розширення можна додати і клас, який має параметри: - -```neon -extensions: - application: Nette\Bridges\ApplicationDI\ApplicationExtension(%debugMode%, %appDir%, %tempDir%/cache) -``` - - -Включення файлів -================ - -Додаткові конфігураційні файли можна включити в секції `includes`: - -```neon -includes: - - parameters.php - - services.neon - - presenters.neon -``` - -Назва `parameters.php` не є помилкою, конфігурація може бути записана також у PHP-файлі, який поверне її як масив: - -```php -<?php -return [ - 'database' => [ - 'main' => [ - 'dsn' => 'sqlite::memory:', - ], - ], -]; -``` - -Якщо в конфігураційних файлах з'являться елементи з однаковими ключами, вони будуть перезаписані, або у випадку [масивів об'єднані |#Об єднання]. Файл, що включається пізніше, має вищий пріоритет, ніж попередній. Файл, у якому вказана секція `includes`, має вищий пріоритет, ніж файли, що включаються в ньому. - - -Search -====== - -Автоматичне додавання сервісів до DI-контейнера надзвичайно полегшує роботу. Nette автоматично додає до контейнера presenter'и, але можна легко додавати й будь-які інші класи. - -Достатньо вказати, у яких каталогах (та підкаталогах) слід шукати класи: - -```neon -search: - - in: %appDir%/Forms - - in: %appDir%/Model -``` - -Зазвичай, однак, ми не хочемо додавати абсолютно всі класи та інтерфейси, тому їх можна фільтрувати: - -```neon -search: - - in: %appDir%/Forms - - # фільтрація за назвою файлу (string|string[]) - files: - - *Factory.php - - # фільтрація за назвою класу (string|string[]) - classes: - - *Factory -``` - -Або ми можемо вибирати класи, які успадковують або реалізують принаймні один із зазначених класів: - - -```neon -search: - - in: %appDir% - extends: - - App\*Form - implements: - - App\*FormInterface -``` - -Можна визначити і правила виключення, тобто маски назви класу або предків, які, якщо відповідають, сервіс не додається до DI-контейнера: - -```neon -search: - - in: %appDir% - exclude: - files: ... - classes: ... - extends: ... - implements: ... -``` - -Усім сервісам можна встановити теги: - -```neon -search: - - in: %appDir% - tags: ... -``` - - -Об'єднання -========== - -Якщо у кількох конфігураційних файлах з'являться елементи з однаковими ключами, вони будуть перезаписані, або у випадку масивів об'єднані. Файл, що включається пізніше, має вищий пріоритет, ніж попередній. - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>результат</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> - <td> -```neon -items: - - 1 - - 2 - - 3 -``` - </td> -</tr> -</table> - -Для масивів можна запобігти об'єднанню, вказавши знак оклику після назви ключа: - -<table class=table> -<tr> - <th width=33%>config1.neon</th> - <th width=33%>config2.neon</th> - <th>результат</th> -</tr> -<tr> - <td> -```neon -items: - - 1 - - 2 -``` - </td> - <td> -```neon -items!: - - 3 -``` - </td> - <td> -```neon -items: - - 3 -``` - </td> -</tr> -</table> - -{{maintitle: Конфігурація Dependency Injection}} diff --git a/dependency-injection/uk/container.texy b/dependency-injection/uk/container.texy deleted file mode 100644 index 82010d258f..0000000000 --- a/dependency-injection/uk/container.texy +++ /dev/null @@ -1,142 +0,0 @@ -Що таке DI-контейнер? -********************* - -.[perex] -Dependency injection контейнер (DIC) — це клас, який вміє інстанціювати та конфігурувати об'єкти. - -Можливо, вас це здивує, але в багатьох випадках вам не потрібен dependency injection контейнер, щоб скористатися перевагами dependency injection (коротко DI). Адже навіть у [вступному розділі|introduction] ми показали DI на конкретних прикладах, і жоден контейнер не був потрібний. - -Однак, якщо вам потрібно керувати великою кількістю різних об'єктів з багатьма залежностями, dependency injection контейнер буде дійсно корисним. Що, наприклад, стосується веб-додатків, побудованих на фреймворку. - -У попередньому розділі ми представили класи `Article` та `UserController`. Обидва мають певні залежності, а саме базу даних та фабрику `ArticleFactory`. І для цих класів ми тепер створимо контейнер. Звичайно, для такого простого прикладу немає сенсу мати контейнер. Але ми створимо його, щоб показати, як він виглядає і працює. - -Ось простий жорстко закодований контейнер для наведеного прикладу: - -```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()); - } -} -``` - -Використання виглядало б так: - -```php -$container = new Container; -$controller = $container->createUserController(); -``` - -Ми лише запитуємо у контейнера об'єкт і вже не повинні нічого знати про те, як його створити та які у нього залежності; все це знає контейнер. Залежності контейнером вводяться автоматично. У цьому його сила. - -Контейнер поки що має всі дані записані жорстко. Зробимо наступний крок і додамо параметри, щоб контейнер став дійсно корисним: - -```php -class Container -{ - public function __construct( - private array $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' => '***', -]); -``` - -Уважні читачі, можливо, помітили певну проблему. Кожного разу, коли я отримую об'єкт `UserController`, також створюється новий екземпляр `ArticleFactory` та бази даних. Цього ми точно не хочемо. - -Тому додамо метод `getService()`, який буде повертати завжди ті самі екземпляри: - -```php -class Container -{ - private array $services = []; - - public function __construct( - private array $parameters, - ) { - } - - public function getService(string $name): object - { - if (!isset($this->services[$name])) { - // getService('Database') викличе createDatabase() - $method = 'create' . $name; - $this->services[$name] = $this->$method(); - } - return $this->services[$name]; - } - - // ... -} -``` - -При першому виклику, наприклад, `$container->getService('Database')`, він попросить `createDatabase()` створити об'єкт бази даних, який збереже в масиві `$services`, а при наступному виклику просто поверне його. - -Змінимо і решту контейнера, щоб він використовував `getService()`: - -```php -class Container -{ - // ... - - public function createArticleFactory(): ArticleFactory - { - return new ArticleFactory($this->getService('Database')); - } - - public function createUserController(): UserController - { - return new UserController($this->getService('ArticleFactory')); - } -} -``` - -До речі, терміном "сервіс" позначається будь-який об'єкт, керований контейнером. Тому й назва методу `getService()`. - -Готово. У нас є повністю функціональний DI-контейнер! І ми можемо його використовувати: - -```php -$container = new Container([ - 'db.dsn' => 'mysql:', - 'db.user' => 'root', - 'db.password' => '***', -]); - -$controller = $container->getService('UserController'); -$database = $container->getService('Database'); -``` - -Як бачите, написати DIC не так вже й складно. Варто нагадати, що самі об'єкти не знають, що їх створює якийсь контейнер. Таким чином, можна створювати будь-який об'єкт у PHP без втручання в його вихідний код. - -Ручне створення та підтримка класу контейнера може досить швидко стати кошмаром. Тому в наступному розділі ми поговоримо про [Nette DI Container|nette-container], який вміє генеруватися та оновлюватися майже самостійно. - - -{{maintitle: Що таке dependency injection контейнер?}} diff --git a/dependency-injection/uk/extensions.texy b/dependency-injection/uk/extensions.texy deleted file mode 100644 index c3e3aa8511..0000000000 --- a/dependency-injection/uk/extensions.texy +++ /dev/null @@ -1,194 +0,0 @@ -Створення розширень для Nette DI -******************************** - -.[perex] -На генерацію DI-контейнера, крім конфігураційних файлів, впливають також так звані *розширення*. Ми активуємо їх у конфігураційному файлі в секції `extensions`. - -Так ми додаємо розширення, представлене класом `BlogExtension`, під назвою `blog`: - -```neon -extensions: - blog: BlogExtension -``` - -Кожне розширення компілятора успадковує від [api:Nette\DI\CompilerExtension] і може реалізовувати наступні методи, які послідовно викликаються під час складання DI-контейнера: - -1. getConfigSchema() -2. loadConfiguration() -3. beforeCompile() -4. afterCompile() - - -getConfigSchema() .[method] -=========================== - -Цей метод викликається першим. Він визначає схему для валідації конфігураційних параметрів. - -Розширення конфігуруємо в секції, назва якої збігається з тією, під якою було додано розширення, тобто `blog`: - -```neon -# та сама назва, що й у розширення -blog: - postsPerPage: 10 - allowComments: false -``` - -Створимо схему, що описує всі конфігураційні опції, включаючи їхні типи, допустимі значення та, можливо, значення за замовчуванням: - -```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), - ]); - } -} -``` - -Документацію знайдете на сторінці [Schema |schema:]. Крім того, можна визначити, які опції можуть бути [динамічними |application:bootstrapping#Динамічні параметри] за допомогою `dynamic()`, наприклад `Expect::int()->dynamic()`. - -До конфігурації ми отримуємо доступ через змінну `$this->config`, яка є об'єктом `stdClass`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $num = $this->config->postPerPage; - if ($this->config->allowComments) { - // ... - } - } -} -``` - - -loadConfiguration() .[method] -============================= - -Використовується для додавання сервісів до контейнера. Для цього служить [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']) // або setCreator() - ->addSetup('setLogger', ['@logger']); - } -} -``` - -Конвенція полягає в тому, щоб префіксувати сервіси, додані розширенням, його назвою, щоб уникнути конфліктів імен. Це робить метод `prefix()`, тому якщо розширення називається `blog`, сервіс матиме назву `blog.articles`. - -Якщо потрібно перейменувати сервіс, для збереження зворотної сумісності можна створити псевдонім з оригінальною назвою. Подібно Nette робить, наприклад, для сервісу `routing.router`, який доступний і під попередньою назвою `router`. - -```php -$builder->addAlias('router', 'routing.router'); -``` - - -Завантаження сервісів з файлу ------------------------------ - -Сервіси можна створювати не лише за допомогою API класу ContainerBuilder, але й відомим записом, що використовується в конфігураційному файлі NEON у секції services. Префікс `@extension` представляє поточне розширення. - -```neon -services: - articles: - create: MyBlog\ArticlesModel(@connection) - - comments: - create: MyBlog\CommentsModel(@connection, @extension.articles) - - articlesList: - create: MyBlog\Components\ArticlesList(@extension.articles) -``` - -Завантажимо сервіси: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - $builder = $this->getContainerBuilder(); - - // завантаження конфігураційного файлу для розширення - $this->compiler->loadDefinitionsFromConfig( - $this->loadFromFile(__DIR__ . '/blog.neon')['services'], - ); - } -} -``` - - -beforeCompile() .[method] -========================= - -Метод викликається в момент, коли контейнер містить усі сервіси, додані окремими розширеннями в методах `loadConfiguration`, а також користувацькими конфігураційними файлами. На цій стадії складання ми можемо редагувати визначення сервісів або доповнювати зв'язки між ними. Для пошуку сервісів у контейнері за тегами можна використовувати метод `findByTag()`, а за класом чи інтерфейсом - метод `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] -======================== - -На цій фазі клас контейнера вже згенеровано у вигляді об'єкта [ClassType |php-generator:#Класи], він містить усі методи, що створюють сервіси, і готовий до запису в кеш. Кінцевий код класу ми можемо на цьому етапі ще змінити. - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function afterCompile(Nette\PhpGenerator\ClassType $class) - { - $method = $class->getMethod('__construct'); - // ... - } -} -``` - - -$initialization .[method] -========================= - -Клас Configurator після [створення контейнера |application:bootstrapping#index.php] викликає ініціалізаційний код, який створюється записом в об'єкт `$this->initialization` за допомогою [методу addBody() |php-generator:#Тіла методів та функцій]. - -Покажемо приклад, як, наприклад, ініціалізаційним кодом запустити сесію або запустити сервіси, що мають тег `run`: - -```php -class BlogExtension extends Nette\DI\CompilerExtension -{ - public function loadConfiguration() - { - // автоматичний запуск сесії - if ($this->config->session->autoStart) { - $this->initialization->addBody('$this->getService("session")->start()'); - } - - // сервіси з тегом run мають бути створені після інстанціювання контейнера - $builder = $this->getContainerBuilder(); - foreach ($builder->findByTag('run') as $name => $foo) { - $this->initialization->addBody('$this->getService(?);', [$name]); - } - } -} -``` diff --git a/dependency-injection/uk/factory.texy b/dependency-injection/uk/factory.texy deleted file mode 100644 index c3f0ffeb36..0000000000 --- a/dependency-injection/uk/factory.texy +++ /dev/null @@ -1,226 +0,0 @@ -Згенеровані фабрики -******************* - -.[perex] -Nette DI вміє автоматично генерувати код фабрик на основі інтерфейсів, що заощаджує вам написання коду. - -Фабрика — це клас, який виробляє та конфігурує об'єкти. Отже, вона передає їм і їхні залежності. Будь ласка, не плутайте з патерном проектування *factory method*, який описує специфічний спосіб використання фабрик і не пов'язаний з цією темою. - -Як виглядає така фабрика, ми показали у [вступному розділі |introduction#Фабрика]: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Nette DI вміє автоматично генерувати код фабрик. Все, що вам потрібно зробити, це створити інтерфейс, і Nette DI згенерує реалізацію. Інтерфейс повинен мати рівно один метод з назвою `create` та декларувати тип повернення: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Отже, фабрика `ArticleFactory` має метод `create`, який створює об'єкти `Article`. Клас `Article` може виглядати, наприклад, так: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } -} -``` - -Фабрику додаємо до конфігураційного файлу: - -```neon -services: - - ArticleFactory -``` - -Nette DI згенерує відповідну реалізацію фабрики. - -У коді, який використовує фабрику, ми запитуємо об'єкт за інтерфейсом, і Nette DI використає згенеровану реалізацію: - -```php -class UserController -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function foo() - { - // дозволимо фабриці створити об'єкт - $article = $this->articleFactory->create(); - } -} -``` - - -Параметризована фабрика -======================= - -Фабричний метод `create` може приймати параметри, які потім передасть до конструктора. Доповнимо, наприклад, клас `Article` ідентифікатором автора статті: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - private int $authorId, - ) { - } -} -``` - -Параметр додамо також до фабрики: - -```php -interface ArticleFactory -{ - function create(int $authorId): Article; -} -``` - -Завдяки тому, що параметр у конструкторі та параметр у фабриці називаються однаково, Nette DI їх повністю автоматично передасть. - - -Розширена дефініція -=================== - -Визначення можна записати і в багаторядковому вигляді за допомогою ключа `implement`: - -```neon -services: - articleFactory: - implement: ArticleFactory -``` - -При записі цим довшим способом можна вказати додаткові аргументи для конструктора в ключі `arguments` та додаткову конфігурацію за допомогою `setup`, так само, як для звичайних сервісів. - -Приклад: якби метод `create()` не приймав параметр `$authorId`, ми могли б вказати фіксоване значення в конфігурації, яке передавалося б до конструктора `Article`: - -```neon -services: - articleFactory: - implement: ArticleFactory - arguments: - authorId: 123 -``` - -Або навпаки, якби `create()` приймав параметр `$authorId`, але він не був би частиною конструктора і передавався б методом `Article::setAuthorId()`, ми б посилалися на нього в секції `setup`: - -```neon -services: - articleFactory: - implement: ArticleFactory - setup: - - setAuthorId($authorId) -``` - - -Accessor -======== - -Nette, крім фабрик, вміє генерувати так звані accessor'и. Це об'єкти з методом `get()`, який повертає певний сервіс з DI-контейнера. Повторний виклик `get()` повертає завжди той самий екземпляр. - -Accessor'и забезпечують ліниве завантаження (lazy-loading) залежностей. Уявімо клас, який записує помилки до спеціальної бази даних. Якби цей клас отримував підключення до бази даних як залежність через конструктор, підключення завжди б створювалося, хоча на практиці помилка виникає лише зрідка, і тому здебільшого з'єднання залишалося б невикористаним. Замість цього клас передає accessor, і лише коли викликається його `get()`, відбувається створення об'єкта бази даних: - -Як створити accessor? Достатньо написати інтерфейс, і Nette DI згенерує реалізацію. Інтерфейс повинен мати рівно один метод з назвою `get` та декларувати тип повернення: - -```php -interface PDOAccessor -{ - function get(): PDO; -} -``` - -Accessor додаємо до конфігураційного файлу, де також є визначення сервісу, який він буде повертати: - -```neon -services: - - PDOAccessor - - PDO(%dsn%, %user%, %password%) -``` - -Оскільки accessor повертає сервіс типу `PDO`, а в конфігурації є лише один такий сервіс, він повертатиме саме його. Якщо сервісів даного типу було б більше, ми б визначили сервіс, що повертається, за допомогою назви, наприклад, `- PDOAccessor(@db1)`. - - -Багаторазова фабрика/accessor -============================= -Наші фабрики та accessor'и досі вміли завжди виробляти або повертати лише один об'єкт. Але можна дуже легко створити і багаторазові фабрики, комбіновані з accessor'ами. Інтерфейс такого класу міститиме довільну кількість методів з назвами `create<name>()` та `get<name>()`, наприклад: - -```php -interface MultiFactory -{ - function createArticle(): Article; - function getDb(): PDO; -} -``` - -Отже, замість того, щоб передавати кілька згенерованих фабрик та accessor'ів, ми передамо одну більш комплексну фабрику, яка вміє більше. - -Альтернативно, замість кількох методів можна використовувати `get()` з параметром: - -```php -interface MultiFactoryAlt -{ - function get($name): PDO; -} -``` - -Тоді виконується, що `MultiFactory::getArticle()` робить те саме, що й `MultiFactoryAlt::get('article')`. Однак альтернативний запис має той недолік, що незрозуміло, які значення `$name` підтримуються, і логічно також неможливо в інтерфейсі розрізнити різні значення, що повертаються, для різних `$name`. - - -Визначення списком ------------------- -Таким чином можна визначити багаторазову фабрику в конфігурації: .{data-version:3.2.0} - -```neon -services: - - MultiFactory( - article: Article # визначає createArticle() - db: PDO(%dsn%, %user%, %password%) # визначає getDb() - ) -``` - -Або ми можемо у визначенні фабрики посилатися на існуючі сервіси за допомогою посилання: - -```neon -services: - article: Article - - PDO(%dsn%, %user%, %password%) - - MultiFactory( - article: @article # визначає createArticle() - db: @\PDO # визначає getDb() - ) -``` - - -Визначення за допомогою тегів ------------------------------ - -Другою можливістю є використання для визначення [тегів |services#Теги]: - -```neon -services: - - App\Core\RouterFactory::createRouter - - App\Model\DatabaseAccessor( - db1: @database.db1.explorer - ) -``` diff --git a/dependency-injection/uk/faq.texy b/dependency-injection/uk/faq.texy deleted file mode 100644 index 449d37a9da..0000000000 --- a/dependency-injection/uk/faq.texy +++ /dev/null @@ -1,106 +0,0 @@ -Часті питання про DI (FAQ) -************************** - - -Чи є DI іншою назвою для IoC? ------------------------------ - -*Inversion of Control* (IoC) — це принцип, зосереджений на способі виконання коду: чи ваш код запускає чужий, чи ваш код інтегрований у чужий, який його потім викликає. IoC — це широкий термін, що охоплює [події |nette:glossary#Події události], так званий [Голлівудський принцип |application:components#Голлівудський стиль] та інші аспекти. Частиною цієї концепції є також фабрики, про які йдеться у [Правило №3: залиште це фабриці |introduction#Правило 3: доручи це фабриці], і які представляють інверсію для оператора `new`. - -*Dependency Injection* (DI) зосереджується на способі, яким один об'єкт дізнається про інший об'єкт, тобто про його залежності. Це патерн проектування, який вимагає явного передавання залежностей між об'єктами. - -Отже, можна сказати, що DI є специфічною формою IoC. Однак не всі форми IoC є доцільними з точки зору чистоти коду. Наприклад, до антипатернів належать техніки, що працюють з [глобальним станом |global-state] або так званий [Service Locator |#Що таке Service Locator]. - - -Що таке Service Locator? ------------------------- - -Це альтернатива Dependency Injection. Він працює так, що створює центральне сховище, де реєструються всі доступні сервіси або залежності. Коли об'єкту потрібна залежність, він запитує її у Service Locator. - -Однак, порівняно з Dependency Injection, він втрачає прозорість: залежності не передаються об'єктам безпосередньо і їх не так легко ідентифікувати, що вимагає дослідження коду для виявлення та розуміння всіх зв'язків. Тестування також складніше, оскільки ми не можемо просто передавати mock-об'єкти тестованим об'єктам, а повинні робити це через Service Locator. Крім того, Service Locator порушує дизайн коду, оскільки окремі об'єкти повинні знати про його існування, що відрізняється від Dependency Injection, де об'єкти не мають уявлення про DI-контейнер. - - -Коли краще не використовувати DI? ---------------------------------- - -Немає відомих труднощів, пов'язаних з використанням патерну проектування Dependency Injection. Навпаки, отримання залежностей з глобально доступних місць призводить до [цілої низки ускладнень |global-state], так само як і використання Service Locator. Тому доцільно використовувати DI завжди. Це не догматичний підхід, а просто не було знайдено кращої альтернативи. - -Проте існують певні ситуації, коли ми не передаємо об'єкти, а отримуємо їх з глобального простору. Наприклад, при налагодженні коду, коли потрібно в конкретній точці програми вивести значення змінної, виміряти тривалість певної частини програми або записати повідомлення. У таких випадках, коли йдеться про тимчасові дії, які пізніше будуть видалені з коду, легітимно використовувати глобально доступний дампер, секундомір або логер. Ці інструменти не належать до дизайну коду. - - -Чи має використання DI свої тіньові сторони? --------------------------------------------- - -Чи несе використання Dependency Injection якісь недоліки, такі як підвищена складність написання коду або погіршена продуктивність? Що ми втрачаємо, коли починаємо писати код відповідно до DI? - -DI не впливає на продуктивність або споживання пам'яті програми. Певну роль може відігравати продуктивність DI-контейнера, однак у випадку [Nette DI |nette-container] контейнер компілюється в чистий PHP, тому його накладні витрати під час роботи програми практично нульові. - -При написанні коду буває необхідно створювати конструктори, що приймають залежності. Раніше це могло бути трудомістким, однак завдяки сучасним IDE та [constructor property promotion |https://blog.nette.org/uk/php-8-0-complete-overview-of-news#toc-constructor-property-promotion] це тепер питання кількох секунд. Фабрики можна легко генерувати за допомогою Nette DI та плагіна для PhpStorm кліком миші. З іншого боку, відпадає потреба писати singleton'и та статичні точки доступу. - -Можна констатувати, що правильно спроектована програма, що використовує DI, не є ні коротшою, ні довшою порівняно з програмою, що використовує singleton'и. Частини коду, що працюють із залежностями, просто вилучаються з окремих класів і переміщуються на нові місця, тобто до DI-контейнера та фабрик. - - -Як legacy-додаток переписати на DI? ------------------------------------ - -Перехід від legacy-додатка до Dependency Injection може бути складним процесом, особливо для великих і комплексних додатків. Важливо підходити до цього процесу систематично. - -- При переході на Dependency Injection важливо, щоб усі члени команди розуміли принципи та процедури, що використовуються. -- Спочатку проведіть аналіз існуючого додатка та ідентифікуйте ключові компоненти та їхні залежності. Створіть план, які частини будуть рефакторені та в якому порядку. -- Реалізуйте DI-контейнер або, ще краще, використайте існуючу бібліотеку, наприклад, Nette DI. -- Поступово рефакторте окремі частини додатка, щоб вони використовували Dependency Injection. Це може включати зміни конструкторів або методів так, щоб вони приймали залежності як параметри. -- Змініть місця в коді, де створюються об'єкти із залежностями, щоб замість цього залежності вводилися контейнером. Це може включати використання фабрик. - -Пам'ятайте, що перехід на Dependency Injection — це інвестиція в якість коду та довгострокову підтримку додатка. Хоча може бути складно виконати ці зміни, результатом має бути чистіший, модульніший та легко тестований код, готовий до майбутнього розширення та підтримки. - - -Чому композиції надається перевага перед успадкуванням? -------------------------------------------------------- -Доцільніше використовувати [композицію |nette:introduction-to-object-oriented-programming#Композиція] замість [успадкування |nette:introduction-to-object-oriented-programming#Успадкування], оскільки вона служить для повторного використання коду, не турбуючись про наслідки змін. Таким чином, вона забезпечує вільніший зв'язок, коли нам не потрібно турбуватися, що зміна якогось коду спричинить необхідність зміни іншого залежного коду. Типовим прикладом є ситуація, що позначається як [пекло конструкторів |passing-dependencies#Пекло конструкторів]. - - -Чи можна використовувати Nette DI Container поза Nette? -------------------------------------------------------- - -Безумовно. Nette DI Container є частиною Nette, але він розроблений як самостійна бібліотека, яка може бути використана незалежно від інших частин фреймворку. Достатньо встановити її за допомогою Composer, створити конфігураційний файл з визначенням ваших сервісів, а потім за допомогою кількох рядків PHP-коду створити DI-контейнер. І одразу можете почати використовувати переваги Dependency Injection у своїх проектах. - -Як виглядає конкретне використання, включаючи коди, описує розділ [Nette DI Container |nette-container]. - - -Чому конфігурація у файлах NEON? --------------------------------- - -NEON — це проста та легко читабельна конфігураційна мова, яка була розроблена в рамках Nette для налаштування додатків, сервісів та їхніх залежностей. Порівняно з JSON або YAML, вона пропонує для цієї мети набагато інтуїтивніші та гнучкіші можливості. У NEON можна природно описати зв'язки, які в Symfony & YAMLu було б неможливо записати або взагалі, або лише за допомогою складного опису. - - -Чи не сповільнює додаток парсинг файлів NEON? ---------------------------------------------- - -Хоча файли NEON парсяться дуже швидко, цей аспект взагалі не має значення. Причина в тому, що парсинг файлів відбувається лише один раз при першому запуску додатка. Потім генерується код DI-контейнера, зберігається на диску і запускається при кожному наступному запиті, без необхідності виконувати подальший парсинг. - -Так це працює в робочому середовищі. Під час розробки файли NEON парсяться кожного разу, коли відбувається зміна їхнього вмісту, щоб розробник завжди мав актуальний DI-контейнер. Сам парсинг, як було сказано, є питанням миттєвості. - - -Як отримати доступ до параметрів у конфігураційному файлі з мого класу? ------------------------------------------------------------------------ - -Пам'ятаймо [Правило №1: нехай тобі це передадуть |introduction#Правило 1: нехай тобі це передадуть]. Якщо клас вимагає інформацію з конфігураційного файлу, нам не потрібно думати, як отримати цю інформацію, замість цього ми просто просимо її — наприклад, через конструктор класу. А передачу здійснюємо в конфігураційному файлі. - -У цьому прикладі `%myParameter%` є заповнювачем для значення параметра `myParameter`, який передається до конструктора класу `MyClass`: - -```php -# config.neon -parameters: - myParameter: Some value - -services: - - MyClass(%myParameter%) -``` - -Якщо ви хочете передавати більше параметрів або використовувати автоматичне підключення, доцільно [упакувати параметри в об'єкт |best-practices:passing-settings-to-presenters]. - - -Чи підтримує Nette PSR-11: Container interface? ------------------------------------------------ - -Nette DI Container не підтримує PSR-11 безпосередньо. Однак, якщо вам потрібна взаємодія між Nette DI Container та бібліотеками або фреймворками, які очікують PSR-11 Container Interface, ви можете створити [простий адаптер |https://gist.github.com/dg/7f02403bd36d9d1c73802a6268a4361f], який слугуватиме мостом між Nette DI Container та PSR-11. diff --git a/dependency-injection/uk/global-state.texy b/dependency-injection/uk/global-state.texy deleted file mode 100644 index c573434ce1..0000000000 --- a/dependency-injection/uk/global-state.texy +++ /dev/null @@ -1,294 +0,0 @@ -Глобальний стан та singleton'и -****************************** - -.[perex] -Попередження: Наступні конструкції є ознакою погано спроектованого коду: - -- `Foo::getInstance()` -- `DB::insert(...)` -- `Article::setDb($db)` -- `ClassName::$var` або `static::$var` - -Чи зустрічаються деякі з цих конструкцій у вашому коді? Тоді у вас є можливість його покращити. Можливо, ви думаєте, що це звичайні конструкції, які ви бачите, наприклад, у демонстраційних рішеннях різних бібліотек та фреймворків. Якщо це так, то дизайн їхнього коду не є добрим. - -Зараз ми точно не говоримо про якусь академічну чистоту. Всі ці конструкції мають одну спільну рису: вони використовують глобальний стан. А він має руйнівний вплив на якість коду. Класи брешуть про свої залежності. Код стає непередбачуваним. Плутає програмістів та знижує їхню ефективність. - -У цьому розділі ми пояснимо, чому це так, і як уникнути глобального стану. - - -Глобальний зв'язок ------------------- - -В ідеальному світі об'єкт повинен мати можливість спілкуватися лише з об'єктами, які йому були [безпосередньо передані |passing-dependencies]. Якщо я створю два об'єкти `A` та `B` і ніколи не передам посилання між ними, то ні `A`, ні `B` не зможуть отримати доступ до іншого об'єкта або змінити його стан. Це дуже бажана властивість коду. Це схоже на те, якби у вас була батарейка та лампочка; лампочка не світитиме, доки ви не з'єднаєте її з батарейкою дротом. - -Але це не стосується глобальних (статичних) змінних або singleton'ів. Об'єкт `A` міг би *бездротово* отримати доступ до об'єкта `C` та модифікувати його без будь-якої передачі посилання, викликавши `C::changeSomething()`. Якщо об'єкт `B` також звернеться до глобального `C`, то `A` та `B` можуть взаємно впливати один на одного через `C`. - -Використання глобальних змінних вносить у систему нову форму *бездротового* зв'язку, яка невидима ззовні. Створює димову завісу, що ускладнює розуміння та використання коду. Щоб розробники дійсно зрозуміли залежності, вони повинні прочитати кожен рядок вихідного коду. Замість простого ознайомлення з інтерфейсом класів. До того ж, це абсолютно зайвий зв'язок. Глобальний стан використовується тому, що він легко доступний звідусіль і дозволяє, наприклад, записати в базу даних через глобальний (статичний) метод `DB::insert()`. Але, як ми покажемо, перевага, яку це дає, незначна, натомість ускладнення це спричиняє фатальні. - -.[note] -З точки зору поведінки немає різниці між глобальною та статичною змінною. Вони однаково шкідливі. - - -Моторошна дія на відстані -------------------------- - -"Моторошна дія на відстані" - так славетно назвав у 1935 році Альберт Ейнштейн явище в квантовій фізиці, яке викликало у нього мурашки по шкірі. -Йдеться про квантове заплутування, особливістю якого є те, що коли ви вимірюєте інформацію про одну частинку, ви миттєво впливаєте на іншу частинку, навіть якщо вони знаходяться на відстані мільйонів світлових років одна від одної. Що, здавалося б, порушує основний закон Всесвіту, що ніщо не може поширюватися швидше за світло. - -У світі програмного забезпечення ми можемо назвати "моторошною дією на відстані" ситуацію, коли ми запускаємо якийсь процес, про який вважаємо, що він ізольований (оскільки ми не передали йому жодних посилань), але у віддалених місцях системи відбуваються несподівані взаємодії та зміни стану, про які ми не мали уявлення. Це може статися лише через глобальний стан. - -Уявіть, що ви приєдналися до команди розробників проекту, який має велику розвинену кодову базу. Ваш новий керівник просить вас реалізувати нову функцію, і ви, як правильний розробник, починаєте з написання тесту. Але оскільки ви новачок у проекті, ви робите багато дослідницьких тестів типу "що станеться, якщо я викличу цей метод". І спробуєте написати наступний тест: - -```php -function testCreditCardCharge() -{ - $cc = new CreditCard('1234567890123456', 5, 2028); // номер вашої картки - $cc->charge(100); -} -``` - -Ви запускаєте код, можливо, кілька разів, і через деякий час помічаєте на мобільному сповіщення від банку, що при кожному запуску з вашої платіжної картки списувалося 100 доларів 🤦‍♂️ - -Як, чорт забирай, тест міг спричинити реальне списання грошей? Оперувати платіжною карткою непросто. Ви повинні спілкуватися з веб-сервісом третьої сторони, ви повинні знати URL цього веб-сервісу, ви повинні увійти в систему і так далі. Жодна з цих інформацій не міститься в тесті. Ба більше, ви навіть не знаєте, де ця інформація знаходиться, а отже, і як мокувати зовнішні залежності, щоб кожен запуск не призводив до того, що знову списується 100 доларів. І як ви, як новий розробник, мали знати, що те, що ви збираєтеся зробити, призведе до того, що ви станете на 100 доларів біднішими? - -Це моторошна дія на відстані! - -Вам не залишається нічого іншого, як довго копатися в купі вихідних кодів, питати старших та досвідченіших колег, перш ніж ви зрозумієте, як працюють зв'язки в проекті. Це спричинено тим, що при погляді на інтерфейс класу `CreditCard` неможливо визначити глобальний стан, який потрібно ініціалізувати. Навіть погляд на вихідний код класу вам не підкаже, який ініціалізаційний метод ви маєте викликати. У кращому випадку ви можете знайти глобальну змінну, до якої здійснюється доступ, і з неї спробувати здогадатися, як її ініціалізувати. - -Класи в такому проекті є патологічними брехунами. Платіжна картка вдає, що її достатньо інстанціювати та викликати метод `charge()`. Але приховано вона співпрацює з іншим класом `PaymentGateway`, який представляє платіжний шлюз. Його інтерфейс також говорить, що його можна ініціалізувати окремо, але насправді він витягує облікові дані з якогось конфігураційного файлу і так далі. Розробникам, які написали цей код, зрозуміло, що `CreditCard` потребує `PaymentGateway`. Вони написали код таким чином. Але для кожного, хто є новачком у проекті, це повна загадка і заважає навчанню. - -Як виправити ситуацію? Легко. **Нехай API декларує залежності.** - -```php -function testCreditCardCharge() -{ - $gateway = new PaymentGateway(/* ... */); - $cc = new CreditCard('1234567890123456', 5, 2028); - $cc->charge($gateway, 100); -} -``` - -Зверніть увагу, як раптом стають очевидними зв'язки всередині коду. Тим, що метод `charge()` декларує, що потребує `PaymentGateway`, вам не потрібно нікого питати про те, як пов'язаний код. Ви знаєте, що повинні створити його екземпляр, і коли спробуєте це зробити, зіткнетеся з тим, що повинні надати параметри доступу. Без них код навіть не запуститься. - -І головне, тепер ви можете мокувати платіжний шлюз, тож при кожному запуску тесту вам не буде нараховуватися 100 доларів. - -Глобальний стан призводить до того, що ваші об'єкти можуть таємно отримувати доступ до речей, які не задекларовані в їхньому API, і в результаті роблять ваші API патологічними брехунами. - -Можливо, ви раніше не думали про це так, але кожного разу, коли ви використовуєте глобальний стан, ви створюєте таємні бездротові канали зв'язку. Моторошна дія на відстані змушує розробників читати кожен рядок коду, щоб зрозуміти потенційні взаємодії, знижує продуктивність розробників та плутає нових членів команди. Якщо ви той, хто створив код, ви знаєте справжні залежності, але кожен, хто прийде після вас, безпорадний. - -Не пишіть код, який використовує глобальний стан, надавайте перевагу передачі залежностей. Тобто dependency injection. - - -Крихкість глобального стану ---------------------------- - -У коді, який використовує глобальний стан та singleton'и, ніколи не можна бути впевненим, коли і хто цей стан змінив. Цей ризик з'являється вже при ініціалізації. Наступний код має створити підключення до бази даних та ініціалізувати платіжний шлюз, однак постійно викидає виняток, і пошук причини є надзвичайно тривалим: - -```php -PaymentGateway::init(); -DB::init('mysql:', 'user', 'password'); -``` - -Ви повинні детально переглядати код, щоб з'ясувати, що об'єкт `PaymentGateway` бездротово звертається до інших об'єктів, деякі з яких вимагають підключення до бази даних. Отже, необхідно ініціалізувати базу даних раніше, ніж `PaymentGateway`. Однак димова завіса глобального стану це від вас приховує. Скільки часу ви б зекономили, якби API окремих класів не обманювало і декларувало свої залежності? - -```php -$db = new DB('mysql:', 'user', 'password'); -$gateway = new PaymentGateway($db, ...); -``` - -Подібна проблема виникає і при використанні глобального доступу до підключення до бази даних: - -```php -use Illuminate\Support\Facades\DB; - -class Article -{ - public function save(): void - { - DB::insert(/* ... */); - } -} -``` - -При виклику методу `save()` невідомо, чи було вже створено підключення до бази даних та хто несе відповідальність за його створення. Якщо ми хочемо, наприклад, змінювати підключення до бази даних під час виконання, наприклад, для тестів, нам, ймовірно, довелося б створити додаткові методи, такі як `DB::reconnect(...)` або `DB::reconnectForTest()`. - -Розглянемо приклад: - -```php -$article = new Article; -// ... -DB::reconnectForTest(); -Foo::doSomething(); -$article->save(); -``` - -Де ми маємо впевненість, що при виклику `$article->save()` дійсно використовується тестова база даних? Що, якщо метод `Foo::doSomething()` змінив глобальне підключення до бази даних? Щоб з'ясувати це, нам довелося б дослідити вихідний код класу `Foo` і, ймовірно, багатьох інших класів. Цей підхід, однак, дав би лише короткострокову відповідь, оскільки ситуація може змінитися в майбутньому. - -А що, якщо підключення до бази даних перемістити в статичну змінну всередині класу `Article`? - -```php -class Article -{ - private static DB $db; - - public static function setDb(DB $db): void - { - self::$db = $db; - } - - public function save(): void - { - self::$db->insert(/* ... */); - } -} -``` - -Це абсолютно нічого не змінило. Проблемою є глобальний стан, і абсолютно байдуже, в якому класі він ховається. У цьому випадку, так само як і в попередньому, ми не маємо при виклику методу `$article->save()` жодного натяку на те, до якої бази даних буде здійснено запис. Будь-хто на іншому кінці програми міг будь-коли за допомогою `Article::setDb()` змінити базу даних. Нам під носом. - -Глобальний стан робить нашу програму **надзвичайно крихкою**. - -Однак існує простий спосіб вирішити цю проблему. Достатньо дозволити API декларувати залежності, що забезпечить правильну функціональність. - -```php -class Article -{ - public function __construct( - private DB $db, - ) { - } - - public function save(): void - { - $this->db->insert(/* ... */); - } -} - -$article = new Article($db); -// ... -Foo::doSomething(); -$article->save(); -``` - -Завдяки цьому підходу зникає побоювання щодо прихованих та несподіваних змін підключення до бази даних. Тепер ми маємо впевненість, куди зберігається стаття, і жодні зміни коду всередині іншого непов'язаного класу вже не можуть змінити ситуацію. Код вже не крихкий, а стабільний. - -Не пишіть код, який використовує глобальний стан, надавайте перевагу передачі залежностей. Тобто dependency injection. - - -Singleton ---------- - -Singleton — це патерн проектування, який, згідно з "визначенням":https://en.wikipedia.org/wiki/Singleton_pattern з відомої публікації Gang of Four, обмежує клас єдиним екземпляром і пропонує до нього глобальний доступ. Реалізація цього патерну зазвичай схожа на наступний код: - -```php -class Singleton -{ - private static self $instance; - - public static function getInstance(): self - { - self::$instance ??= new self; - return self::$instance; - } - - // та інші методи, що виконують функції даного класу -} -``` - -На жаль, singleton вводить у програму глобальний стан. А як ми показали вище, глобальний стан є небажаним. Тому singleton вважається антипатерном. - -Не використовуйте у своєму коді singleton'и та замініть їх іншими механізмами. Singleton'и вам дійсно не потрібні. Однак, якщо вам потрібно гарантувати існування єдиного екземпляра класу для всієї програми, залиште це на [DI-контейнера |container]. Створіть таким чином аплікаційний singleton, тобто сервіс. Тим самим клас перестане займатися забезпеченням власної унікальності (тобто не матиме методу `getInstance()` та статичної змінної) і виконуватиме лише свої функції. Так він перестане порушувати принцип єдиної відповідальності. - - -Глобальний стан проти тестів ----------------------------- - -При написанні тестів ми припускаємо, що кожен тест є ізольованою одиницею і що до нього не входить жоден зовнішній стан. І жоден стан тести не залишає. Після завершення тесту весь пов'язаний з тестом стан повинен бути автоматично видалений збирачем сміття. Завдяки цьому тести ізольовані. Тому ми можемо запускати тести в будь-якому порядку. - -Однак, якщо присутні глобальні стани/singleton'и, всі ці приємні припущення руйнуються. Стан може входити в тест і виходити з нього. Раптом може мати значення порядок тестів. - -Щоб взагалі мати можливість тестувати singleton'и, розробники часто змушені послаблювати їхні властивості, наприклад, дозволяючи замінити екземпляр іншим. Такі рішення в кращому випадку є хаком, який створює код, що важко підтримувати та розуміти. Кожен тест або метод `tearDown()`, який впливає на будь-який глобальний стан, повинен ці зміни скасувати. - -Глобальний стан — це найбільший головний біль при юніт-тестуванні! - -Як виправити ситуацію? Легко. Не пишіть код, який використовує singleton'и, надавайте перевагу передачі залежностей. Тобто dependency injection. - - -Глобальні константи -------------------- - -Глобальний стан не обмежується лише використанням singleton'ів та статичних змінних, але може стосуватися також глобальних констант. - -Константи, значення яких не приносить нам жодної нової (`M_PI`) або корисної (`PREG_BACKTRACK_LIMIT_ERROR`) інформації, є однозначно в порядку. Навпаки, константи, які служать способом *бездротово* передати інформацію всередину коду, є нічим іншим, як прихованою залежністю. Як, наприклад, `LOG_FILE` у наступному прикладі. Використання константи `FILE_APPEND` є цілком коректним. - -```php -const LOG_FILE = '...'; - -class Foo -{ - public function doSomething() - { - // ... - file_put_contents(LOG_FILE, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -У цьому випадку ми повинні задекларувати параметр у конструкторі класу `Foo`, щоб він став частиною API: - -```php -class Foo -{ - public function __construct( - private string $logFile, - ) { - } - - public function doSomething() - { - // ... - file_put_contents($this->logFile, $message . "\n", FILE_APPEND); - // ... - } -} -``` - -Тепер ми можемо передати інформацію про шлях до файлу для логування та легко змінювати її за потребою, що полегшує тестування та підтримку коду. - - -Глобальні функції та статичні методи ------------------------------------- - -Хочемо підкреслити, що саме використання статичних методів та глобальних функцій не є проблематичним. Ми пояснювали, в чому полягає недоцільність використання `DB::insert()` та подібних методів, але завжди йшлося лише про глобальний стан, який зберігається в якійсь статичній змінній. Метод `DB::insert()` вимагає існування статичної змінної, оскільки в ній зберігається підключення до бази даних. Без цієї змінної було б неможливо реалізувати метод. - -Використання детермінованих статичних методів та функцій, таких як `DateTime::createFromFormat()`, `Closure::fromCallable`, `strlen()` та багатьох інших, є цілком сумісним з dependency injection. Ці функції завжди повертають однакові результати для однакових вхідних параметрів і тому є передбачуваними. Вони не використовують жодного глобального стану. - -Однак існують і функції в PHP, які не є детермінованими. До них належить, наприклад, функція `htmlspecialchars()`. Її третій параметр `$encoding`, якщо не вказаний, за замовчуванням має значення конфігураційної опції `ini_get('default_charset')`. Тому рекомендується цей параметр завжди вказувати, щоб уникнути можливої непередбачуваної поведінки функції. Nette це послідовно робить. - -Деякі функції, такі як `strtolower()`, `strtoupper()` та подібні, в недавньому минулому поводилися недетерміновано і залежали від налаштування `setlocale()`. Це спричиняло багато ускладнень, найчастіше при роботі з турецькою мовою. Вона розрізняє малу та велику літеру `I` з крапкою та без крапки. Отже, `strtolower('I')` повертало символ `ı`, а `strtoupper('i')` — символ `İ`, що призводило до того, що програми починали спричиняти низку загадкових помилок. Ця проблема, однак, була усунена в PHP версії 8.2, і функції вже не залежать від локалі. - -Це гарний приклад того, як глобальний стан завдав клопоту тисячам розробників у всьому світі. Рішенням було замінити його на dependency injection. - - -Коли можна використовувати глобальний стан? -------------------------------------------- - -Існують певні специфічні ситуації, коли можна використовувати глобальний стан. Наприклад, при налагодженні коду, коли потрібно вивести значення змінної або виміряти тривалість певної частини програми. У таких випадках, що стосуються тимчасових дій, які пізніше будуть видалені з коду, легітимно використовувати глобально доступний дампер або секундомір. Ці інструменти не є частиною дизайну коду. - -Іншим прикладом є функції для роботи з регулярними виразами `preg_*`, які внутрішньо зберігають скомпільовані регулярні вирази в статичному кеші в пам'яті. Коли ви викликаєте той самий регулярний вираз кілька разів у різних місцях коду, він компілюється лише один раз. Кеш економить продуктивність і водночас є для користувача абсолютно невидимим, тому таке використання можна вважати легітимним. - - -Резюме ------- - -Ми розглянули, чому має сенс: - -1) Видалити всі статичні змінні з коду -2) Декларувати залежності -3) І використовувати dependency injection - -Коли ви продумуєте дизайн коду, пам'ятайте, що кожне `static $foo` становить проблему. Щоб ваш код був середовищем, що поважає DI, необхідно повністю викорінити глобальний стан і замінити його за допомогою dependency injection. - -Під час цього процесу ви, можливо, виявите, що потрібно розділити клас, оскільки він має більше однієї відповідальності. Не бійтеся цього; прагніть до принципу єдиної відповідальності. - -*Я хотів би подякувати Мішкові Хевері, чиї статті, такі як [Flaw: Brittle Global State & Singletons |https://web.archive.org/web/20230321084133/http://misko.hevery.com/code-reviewers-guide/flaw-brittle-global-state-singletons/], є основою цього розділу.* diff --git a/dependency-injection/uk/introduction.texy b/dependency-injection/uk/introduction.texy deleted file mode 100644 index 81cdcb0fa5..0000000000 --- a/dependency-injection/uk/introduction.texy +++ /dev/null @@ -1,526 +0,0 @@ -Що таке Dependency Injection? -***************************** - -.[perex] -Цей розділ познайомить вас з основними практиками програмування, яких слід дотримуватися під час написання будь-яких застосунків. Це основи, необхідні для написання чистого, зрозумілого та підтримуваного коду. - -Якщо ви засвоїте ці правила і будете їх дотримуватися, Nette допомагатиме вам на кожному кроці. Він вирішуватиме за вас рутинні завдання та забезпечить максимальний комфорт, щоб ви могли зосередитися на самій логіці. - -Принципи, які ми тут покажемо, при цьому досить прості. Вам не потрібно нічого боятися. - - -Пам'ятаєте свою першу програму? -------------------------------- - -Ми не знаємо, якою мовою ви її написали, але якби це була PHP, вона, ймовірно, виглядала б приблизно так: - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} - -echo soucet(23, 1); // виведе 24 -``` - -Кілька тривіальних рядків коду, але в них приховано стільки ключових концепцій. Що існують змінні. Що код ділиться на менші одиниці, якими, наприклад, є функції. Що ми передаємо їм вхідні аргументи, а вони повертають результати. Не вистачає лише умов та циклів. - -Те, що ми передаємо дані у функцію, а вона повертає результат, є цілком зрозумілою концепцією, яка використовується і в інших галузях, наприклад, у математиці. - -Функція має свою сигнатуру, яка складається з її назви, переліку параметрів та їхніх типів, і, нарешті, типу значення, що повертається. Як користувачів, нас цікавить сигнатура, про внутрішню реалізацію нам зазвичай нічого знати не потрібно. - -Тепер уявіть, що сигнатура функції виглядала б так: - -```php -function soucet(float $x): float -``` - -Сума з одним параметром? Це дивно… А як щодо цього? - -```php -function soucet(): float -``` - -Це вже справді дуже дивно, чи не так? Як, напевно, використовується функція? - -```php -echo soucet(); // що вона, ймовірно, виведе? -``` - -Дивлячись на такий код, ми були б спантеличені. Його не зрозумів би не тільки початківець, такий код не зрозуміє і досвідчений програміст. - -Ви думаєте, як би така функція виглядала всередині? Звідки вона візьме доданки? Мабуть, вона *якимось чином* отримала б їх сама, наприклад, так: - -```php -function soucet(): float -{ - $a = Input::get('a'); - $b = Input::get('b'); - return $a + $b; -} -``` - -У тілі функції ми виявили приховані зв'язки з іншими глобальними функціями чи статичними методами. Щоб з'ясувати, звідки насправді беруться доданки, нам потрібно шукати далі. - - -Не сюди! --------- - -Дизайн, який ми щойно показали, є сутністю багатьох негативних рис: - -- сигнатура функції вдавала, що не потребує доданків, що нас спантеличувало -- ми взагалі не знаємо, як змусити функцію додати два інші числа -- нам довелося заглянути в код, щоб з'ясувати, звідки вона бере доданки -- ми виявили приховані зв'язки -- для повного розуміння необхідно дослідити і ці зв'язки - -А чи взагалі завданням функції додавання є отримання вхідних даних? Звісно, ні. Її відповідальність - лише саме додавання. - - -Ми не хочемо зустрічатися з таким кодом, і точно не хочемо його писати. Виправлення при цьому просте: повернутися до основ і просто використовувати параметри: - - -```php -function soucet(float $a, float $b): float -{ - return $a + $b; -} -``` - - -Правило № 1: нехай тобі це передадуть -------------------------------------- - -Найважливіше правило звучить так: **усі дані, які потрібні функції або класу, мають бути передані їм**. - -Замість того, щоб вигадувати приховані способи, за допомогою яких вони могли б якось отримати їх самі, просто передайте параметри. Ви заощадите час, необхідний для вигадування прихованих шляхів, які точно не покращать ваш код. - -Якщо ви завжди і скрізь дотримуватиметеся цього правила, ви на шляху до коду без прихованих зв'язків. До коду, який зрозумілий не лише автору, але й кожному, хто читатиме його після нього. Де все зрозуміло з сигнатур функцій та класів і не потрібно шукати прихованих таємниць у реалізації. - -Ця техніка професійно називається **dependency injection**. А ці дані називаються **залежностями.** При цьому це звичайнісінька передача параметрів, нічого більше. - -.[note] -Будь ласка, не плутайте dependency injection, що є патерном проектування, з „dependency injection container“, що є інструментом, тобто чимось діаметрально іншим. Контейнерам ми приділимо увагу пізніше. - - -Від функцій до класів ---------------------- - -А як із цим пов'язані класи? Клас - це складніша одиниця, ніж проста функція, однак правило №1 діє тут без винятку. Просто існує [більше способів передачі аргументів |passing-dependencies]. Наприклад, досить схоже на випадок з функцією: - -```php -class Matematika -{ - public function soucet(float $a, float $b): float - { - return $a + $b; - } -} - -$math = new Matematika; -echo $math->soucet(23, 1); // 24 -``` - -Або за допомогою інших методів, чи безпосередньо конструктора: - -```php -class Soucet -{ - public function __construct( - private float $a, - private float $b, - ) { - } - - public function spocti(): float - { - return $this->a + $this->b; - } - -} - -$soucet = new Soucet(23, 1); -echo $soucet->spocti(); // 24 -``` - -Обидва приклади повністю відповідають dependency injection. - - -Реальні приклади ----------------- - -У реальному світі ви не будете писати класи для додавання чисел. Перейдемо до прикладів із практики. - -Маємо клас `Article`, що представляє статтю в блозі: - -```php -class Article -{ - public int $id; - public string $title; - public string $content; - - public function save(): void - { - // збережемо статтю в базу даних - } -} -``` - -а використання буде таким: - -```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(); -``` - -Метод `save()` зберігає статтю в таблицю бази даних. Реалізувати його за допомогою [Nette Database |database:] буде легко, якби не одна заковика: де `Article` має взяти підключення до бази даних, тобто об'єкт класу `Nette\Database\Connection`? - -Здається, у нас багато варіантів. Він може взяти його звідкись зі статичної змінної. Або успадкувати від класу, який забезпечить підключення до бази даних. Або використати так званий [singleton |global-state#Singleton]. Або так звані facades, які використовуються в 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], - ); - } -} -``` - -Чудово, ми вирішили проблему. - -Чи ні? - -Нагадаємо [#правило №1: нехай тобі це передадуть |#Правило 1: нехай тобі це передадуть]: усі залежності, які потрібні класу, мають бути передані йому. Тому що якщо ми порушимо правило, ми ступили на шлях до брудного коду, повного прихованих зв'язків, незрозумілості, і результатом буде застосунок, який буде боляче підтримувати та розвивати. - -Користувач класу `Article` не знає, куди метод `save()` зберігає статтю. У таблицю бази даних? В яку, робочу чи тестову? А як це можна змінити? - -Користувач повинен подивитися, як реалізований метод `save()`, і знайде використання методу `DB::insert()`. Тож він повинен шукати далі, як цей метод отримує підключення до бази даних. А приховані зв'язки можуть утворювати досить довгий ланцюжок. - -У чистому та добре спроектованому коді ніколи не зустрічаються приховані зв'язки, фасади Laravel або статичні змінні. У чистому та добре спроектованому коді передаються аргументи: - -```php -class Article -{ - public function save(Nette\Database\Connection $db): void - { - $db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -Ще практичніше, як ми побачимо далі, це буде з конструктором: - -```php -class Article -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function save(): void - { - $this->db->query('INSERT INTO articles', [ - 'title' => $this->title, - 'content' => $this->content, - ]); - } -} -``` - -.[note] -Якщо ви досвідчений програміст, можливо, ви думаєте, що `Article` взагалі не повинен мати метод `save()`, він повинен представляти чисто компонент даних, а про збереження повинен дбати окремий репозиторій. Це має сенс. Але цим ми б вийшли далеко за рамки теми, якою є dependency injection, та прагнення наводити прості приклади. - -Якщо ви будете писати клас, який потребує для своєї роботи, наприклад, базу даних, не вигадуйте, звідки її отримати, а нехай вам її передадуть. Наприклад, як параметр конструктора або іншого методу. Визнайте залежності. Визнайте їх в API вашого класу. Ви отримаєте зрозумілий та передбачуваний код. - -А як щодо цього класу, який логує повідомлення про помилки: - -```php -class Logger -{ - public function log(string $message) - { - $file = LOG_DIR . '/log.txt'; - file_put_contents($file, $message . "\n", FILE_APPEND); - } -} -``` - -Як ви думаєте, чи дотрималися ми [#правило №1: нехай тобі це передадуть |#Правило 1: нехай тобі це передадуть]? - -Не дотрималися. - -Ключову інформацію, тобто каталог із файлом логу, клас *отримує сам* із константи. - -Подивіться на приклад використання: - -```php -$logger = new Logger; -$logger->log('Температура 23 °C'); -$logger->log('Температура 10 °C'); -``` - -Без знання реалізації, чи змогли б ви відповісти на питання, куди записуються повідомлення? Чи спало б вам на думку, що для роботи потрібна константа `LOG_DIR`? А чи змогли б ви створити другий екземпляр, який буде записувати в інше місце? Звісно, ні. - -Давайте виправимо клас: - -```php -class Logger -{ - public function __construct( - private string $file, - ) { - } - - public function log(string $message): void - { - file_put_contents($this->file, $message . "\n", FILE_APPEND); - } -} -``` - -Клас тепер набагато зрозуміліший, конфігурованіший і, отже, корисніший. - -```php -$logger = new Logger('/шлях/до/логу.txt'); -$logger->log('Температура 15 °C'); -``` - - -Але мене це не цікавить! ------------------------- - -*«Коли я створюю об'єкт Article і викликаю save(), я не хочу займатися базою даних, я просто хочу, щоб він зберігся в ту, яку я налаштував у конфігурації».* - -*«Коли я використовую Logger, я просто хочу, щоб повідомлення записалося, і не хочу думати куди. Нехай використовуються глобальні налаштування».* - -Це слушні зауваження. - -Як приклад, покажемо клас, що розсилає інформаційні бюлетені, який залогує результат: - -```php -class NewsletterDistributor -{ - public function distribute(): void - { - $logger = new Logger(/* ... */); - try { - $this->sendEmails(); - $logger->log('Електронні листи були надіслані'); - - } catch (Exception $e) { - $logger->log('Сталася помилка під час надсилання'); - throw $e; - } - } -} -``` - -Покращений `Logger`, який більше не використовує константу `LOG_DIR`, вимагає в конструкторі вказати шлях до файлу. Як це вирішити? Клас `NewsletterDistributor` зовсім не цікавить, куди записуються повідомлення, він хоче їх просто записати. - -Рішенням знову є [#правило №1: нехай тобі це передадуть |#Правило 1: нехай тобі це передадуть]: усі дані, які потрібні класу, ми йому передаємо. - -Отже, це означає, що ми передамо шлях до логу через конструктор, який потім використаємо при створенні об'єкта `Logger`? - -```php -class NewsletterDistributor -{ - public function __construct( - private string $file, // ⛔ НЕ ТАК! - ) { - } - - public function distribute(): void - { - $logger = new Logger($this->file); -``` - -Не так! Тому що шлях **не належить** до даних, які потрібні класу `NewsletterDistributor`; вони потрібні `Logger`. Ви відчуваєте різницю? Клас `NewsletterDistributor` потребує логер як такий. Тож його ми й передамо: - -```php -class NewsletterDistributor -{ - public function __construct( - private Logger $logger, // ✅ - ) { - } - - public function distribute(): void - { - try { - $this->sendEmails(); - $this->logger->log('Електронні листи були надіслані'); - - } catch (Exception $e) { - $this->logger->log('Сталася помилка під час надсилання'); - throw $e; - } - } -} -``` - -Тепер із сигнатур класу `NewsletterDistributor` зрозуміло, що частиною його функціональності є логування. А завдання замінити логер на інший, наприклад, для тестування, є абсолютно тривіальним. Крім того, якщо конструктор класу `Logger` зміниться, це ніяк не вплине на наш клас. - - -Правило № 2: бери те, що твоє ------------------------------ - -Не дозволяйте себе заплутати і не дозволяйте передавати залежності ваших залежностей. Нехай вам передають лише ваші залежності. - -Завдяки цьому код, що використовує інші об'єкти, буде повністю незалежним від змін їхніх конструкторів. Його API буде правдивішим. І головне, буде тривіально замінити ці залежності на інші. - - -Новий член родини ------------------ - -У команді розробників було прийнято рішення створити другий логер, який записує в базу даних. Тож ми створимо клас `DatabaseLogger`. Отже, у нас є два класи, `Logger` і `DatabaseLogger`, один записує у файл, інший у базу даних... вам не здається щось дивним у цій назві? Чи не краще було б перейменувати `Logger` на `FileLogger`? Звісно, так. - -Але зробимо це розумно. Під оригінальною назвою створимо інтерфейс: - -```php -interface Logger -{ - function log(string $message): void; -} -``` - -... який обидва логери будуть реалізовувати: - -```php -class FileLogger implements Logger -// ... - -class DatabaseLogger implements Logger -// ... -``` - -І завдяки цьому не потрібно буде нічого змінювати в решті коду, де використовується логер. Наприклад, конструктор класу `NewsletterDistributor` все ще буде задоволений тим, що вимагає `Logger` як параметр. І тільки від нас залежатиме, який екземпляр ми йому передамо. - -**Тому ми ніколи не додаємо до назв інтерфейсів суфікс `Interface` або префікс `I`.** Інакше неможливо було б так гарно розвивати код. - - -Х'юстоне, у нас проблема ------------------------- - -Хоча в усьому застосунку ми можемо обійтися єдиним екземпляром логера, чи то файлового, чи то базданного, і просто передавати його скрізь, де щось логується, зовсім інакше у випадку класу `Article`. Адже його екземпляри ми створюємо за потребою, навіть кілька разів. Як впоратися зі зв'язком з базою даних у його конструкторі? - -Як приклад може слугувати контролер, який після надсилання форми має зберегти статтю в базу даних: - -```php -class EditController extends Controller -{ - public function formSubmitted($data) - { - $article = new Article(/* ... */); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Можливе рішення напрошується саме собою: передамо об'єкт бази даних конструктором у `EditController` і використаємо `$article = new Article($this->db)`. - -Так само, як у попередньому випадку з `Logger` та шляхом до файлу, це неправильний підхід. База даних не є залежністю `EditController`, а `Article`. Отже, передача бази даних суперечить [правилу №2: бери те, що твоє |#Правило 2: бери те що твоє]. Коли зміниться конструктор класу `Article` (додасться новий параметр), потрібно буде також змінити код у всіх місцях, де створюються екземпляри. Уфф. - -Х'юстоне, що ти пропонуєш? - - -Правило № 3: доручи це фабриці ------------------------------- - -Скасувавши приховані зв'язки та передаючи всі залежності як аргументи, ми отримали більш конфігуровані та гнучкі класи. А отже, нам потрібно ще щось, що створить і налаштує ці гнучкіші класи. Ми будемо називати це фабриками. - -Правило звучить так: якщо клас має залежності, доручи створення їхніх екземплярів фабриці. - -Фабрики - це розумніша заміна оператора `new` у світі dependency injection. - -.[note] -Будь ласка, не плутайте з патерном проектування *factory method*, який описує специфічний спосіб використання фабрик і не пов'язаний з цією темою. - - -Фабрика -------- - -Фабрика - це метод або клас, який виробляє та конфігурує об'єкти. Клас, що виробляє `Article`, назвемо `ArticleFactory`, і він може виглядати, наприклад, так: - -```php -class ArticleFactory -{ - public function __construct( - private Nette\Database\Connection $db, - ) { - } - - public function create(): Article - { - return new Article($this->db); - } -} -``` - -Його використання в контролері буде таким: - -```php -class EditController extends Controller -{ - public function __construct( - private ArticleFactory $articleFactory, - ) { - } - - public function formSubmitted($data) - { - // доручаємо фабриці створити об'єкт - $article = $this->articleFactory->create(); - $article->title = $data->title; - $article->content = $data->content; - $article->save(); - } -} -``` - -Якщо в цей момент зміниться сигнатура конструктора класу `Article`, єдиною частиною коду, яка повинна на це реагувати, є сама фабрика `ArticleFactory`. Весь інший код, який працює з об'єктами `Article`, наприклад `EditController`, це ніяк не зачепить. - -Можливо, ви зараз стукаєте себе по лобі, чи ми взагалі собі допомогли. Кількість коду зросла, і все це починає виглядати підозріло складно. - -Не хвилюйтеся, незабаром ми дійдемо до DI-контейнера Nette. А він має низку козирів у рукаві, які надзвичайно спрощують створення застосунків, що використовують dependency injection. Наприклад, замість класу `ArticleFactory` достатньо буде [написати лише інтерфейс |factory]: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Але ми забігаємо наперед, зачекайте ще :-) - - -Підсумок --------- - -На початку цього розділу ми обіцяли показати вам, як проектувати чистий код. Достатньо класам - -1) [передавати залежності, які їм потрібні |#Правило 1: нехай тобі це передадуть] -2) [і навпаки, не передавати те, що їм безпосередньо не потрібно |#Правило 2: бери те що твоє] -3) [і що об'єкти із залежностями найкраще створювати у фабриках |#Правило 3: доручи це фабриці] - -На перший погляд це може здатися не так, але ці три правила мають далекосяжні наслідки. Вони ведуть до радикально іншого погляду на проектування коду. Чи варте воно того? Програмісти, які відкинули старі звички і почали послідовно використовувати dependency injection, вважають цей крок ключовим моментом у професійному житті. Їм відкрився світ зрозумілих та підтримуваних застосунків. - -А що, якщо код послідовно не використовує dependency injection? Що, якщо він побудований на статичних методах або синглтонах? Чи спричиняє це якісь проблеми? [Спричиняє, і дуже серйозні |global-state]. diff --git a/dependency-injection/uk/nette-container.texy b/dependency-injection/uk/nette-container.texy deleted file mode 100644 index 6959fd7129..0000000000 --- a/dependency-injection/uk/nette-container.texy +++ /dev/null @@ -1,80 +0,0 @@ -Nette DI Container -****************** - -.[perex] -Nette DI - одна з найцікавіших бібліотек Nette. Вона вміє генерувати та автоматично оновлювати скомпільовані DI-контейнери, які є надзвичайно швидкими та дивовижно легко конфігуруються. - -Вигляд сервісів, які має створювати DI-контейнер, ми зазвичай визначаємо за допомогою конфігураційних файлів у [форматі NEON |neon:format]. Контейнер, який ми вручну створили в [попередньому розділі |container], записується так: - -```neon -parameters: - db: - dsn: 'mysql:' - user: root - password: '***' - -services: - - Nette\Database\Connection(%db.dsn%, %db.user%, %db.password%) - - ArticleFactory - - UserController -``` - -Запис дійсно короткий. - -Усі залежності, оголошені в конструкторах класів `ArticleFactory` та `UserController`, Nette DI самостійно виявляє та передає завдяки так званому [autowiring |autowiring], тому в конфігураційному файлі нічого вказувати не потрібно. Тож навіть якщо параметри зміняться, вам не потрібно нічого змінювати в конфігурації. Контейнер Nette автоматично перегенерується. Ви можете зосередитися виключно на розробці застосунку. - -Якщо ми хочемо передавати залежності за допомогою сеттерів, ми використовуємо для цього секцію [setup |services#Setup]. - -Nette DI генерує безпосередньо PHP-код контейнера. Результатом є файл `.php`, який ви можете відкрити та вивчити. Завдяки цьому ви точно бачите, як працює контейнер. Ви також можете налагоджувати його в IDE та виконувати покроково. І головне: згенерований PHP надзвичайно швидкий. - -Nette DI також вміє генерувати код [фабрик |factory] на основі наданого інтерфейсу. Тому замість класу `ArticleFactory` нам достатньо буде створити в застосунку лише інтерфейс: - -```php -interface ArticleFactory -{ - function create(): Article; -} -``` - -Повний приклад ви знайдете [на GitHub |https://github.com/nette-examples/di-example-doc]. - - -Самостійне використання ------------------------ - -Впровадження бібліотеки Nette DI в застосунок дуже просте. Спочатку встановимо її за допомогою Composer (бо завантаження zip-архівів тааак застаріло): - -```shell -composer require nette/di -``` - -Наступний код створить екземпляр DI-контейнера відповідно до конфігурації, збереженої у файлі `config.neon`: - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp'); -$class = $loader->load(function ($compiler) { - $compiler->loadConfig(__DIR__ . '/config.neon'); -}); -$container = new $class; -``` - -Контейнер генерується лише один раз, його код записується в кеш (каталог `__DIR__ . '/temp'`), а при наступних запитах він просто завантажується звідти. - -Для створення та отримання сервісів служать методи `getService()` або `getByType()`. Таким чином ми створимо об'єкт `UserController`: - -```php -$controller = $container->getByType(UserController::class); -$controller->someMethod(); -``` - -Під час розробки корисно активувати режим автоматичного оновлення, коли контейнер автоматично перегенерується, якщо зміниться будь-який клас або конфігураційний файл. Достатньо в конструкторі `ContainerLoader` вказати `true` як другий аргумент. - -```php -$loader = new Nette\DI\ContainerLoader(__DIR__ . '/temp', true); -``` - - -Використання з фреймворком Nette --------------------------------- - -Як ми показали, використання Nette DI не обмежується застосунками, написаними на Nette Framework, ви можете впровадити його де завгодно за допомогою лише 3 рядків коду. Однак, якщо ви розробляєте застосунки на Nette Framework, конфігурацією та створенням контейнера займається [Bootstrap |application:bootstrapping#Конфігурація DI-контейнера]. diff --git a/dependency-injection/uk/passing-dependencies.texy b/dependency-injection/uk/passing-dependencies.texy deleted file mode 100644 index 6601427616..0000000000 --- a/dependency-injection/uk/passing-dependencies.texy +++ /dev/null @@ -1,215 +0,0 @@ -Передача залежностей -******************** - -<div class=perex> - -Аргументи, або в термінології DI «залежності», можна передавати в класи такими основними способами: - -* передача конструктором -* передача методом (так званим сеттером) -* встановленням змінної -* методом, анотацією чи атрибутом *inject* - -</div> - -Тепер розглянемо кожен варіант на конкретних прикладах. - - -Передача конструктором -====================== - -Залежності передаються в момент створення об'єкта як аргументи конструктора: - -```php -class MyClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -$obj = new MyClass($cache); -``` - -Ця форма підходить для обов'язкових залежностей, які клас неодмінно потребує для своєї роботи, оскільки без них неможливо створити екземпляр. - -Починаючи з PHP 8.0, ми можемо використовувати коротшу форму запису ([constructor property promotion |https://blog.nette.org/uk/php-8-0-complete-overview-of-news#toc-constructor-property-promotion]), яка функціонально еквівалентна: - -```php -// PHP 8.0 -class MyClass -{ - public function __construct( - private Cache $cache, - ) { - } -} -``` - -Починаючи з PHP 8.1, змінну можна позначити прапорцем `readonly`, який оголошує, що вміст змінної більше не зміниться: - -```php -// PHP 8.1 -class MyClass -{ - public function __construct( - private readonly Cache $cache, - ) { - } -} -``` - -DI-контейнер автоматично передає залежності конструктору за допомогою [autowiring |autowiring]. Аргументи, які неможливо передати таким чином (наприклад, рядки, числа, логічні значення), [ми записуємо в конфігурації |services#Аргументи]. - - -Пекло конструкторів -------------------- - -Термін *constructor hell* (пекло конструкторів) позначає ситуацію, коли нащадок успадковує від батьківського класу, конструктор якого вимагає залежностей, і водночас нащадок також вимагає залежностей. При цьому він повинен прийняти та передати також батьківські: - -```php -abstract class BaseClass -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass extends BaseClass -{ - private Database $db; - - // ⛔ ПЕКЛО КОНСТРУКТОРІВ - public function __construct(Cache $cache, Database $db) - { - parent::__construct($cache); - $this->db = $db; - } -} -``` - -Проблема виникає в той момент, коли ми хочемо змінити конструктор класу `BaseClass`, наприклад, коли додається нова залежність. Тоді необхідно також змінити всі конструктори нащадків. Що перетворює таку модифікацію на пекло. - -Як цьому запобігти? Рішенням є **надавати перевагу [композиції над успадкуванням |faq#Чому композиції надається перевага перед успадкуванням]**. - -Отже, ми спроектуємо код інакше. Ми будемо уникати [абстрактних |nette:introduction-to-object-oriented-programming#Абстрактні класи] `Base*` класів. Замість того, щоб `MyClass` отримував певну функціональність шляхом успадкування від `BaseClass`, він отримає цю функціональність як залежність: - -```php -final class SomeFunctionality -{ - private Cache $cache; - - public function __construct(Cache $cache) - { - $this->cache = $cache; - } -} - -final class MyClass -{ - private SomeFunctionality $sf; - private Database $db; - - public function __construct(SomeFunctionality $sf, Database $db) // ✅ - { - $this->sf = $sf; - $this->db = $db; - } -} -``` - - -Передача сеттером -================= - -Залежності передаються викликом методу, який зберігає їх у приватній змінній. Звичайною конвенцією іменування цих методів є форма `set*()`, тому їх називають сеттерами, але вони, звісно, можуть називатися будь-як інакше. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - $this->cache = $cache; - } -} - -$obj = new MyClass; -$obj->setCache($cache); -``` - -Цей спосіб підходить для необов'язкових залежностей, які не є необхідними для роботи класу, оскільки не гарантується, що об'єкт дійсно отримає залежність (тобто, що користувач викличе метод). - -Водночас цей спосіб дозволяє викликати сеттер повторно і таким чином змінювати залежність. Якщо це небажано, ми додамо перевірку в метод, або, починаючи з PHP 8.1, позначимо властивість `$cache` прапорцем `readonly`. - -```php -class MyClass -{ - private Cache $cache; - - public function setCache(Cache $cache): void - { - if (isset($this->cache)) { - throw new RuntimeException('Залежність вже встановлена'); - } - $this->cache = $cache; - } -} -``` - -Виклик сеттера ми визначаємо в конфігурації DI-контейнера в [ключі setup |services#Setup]. Тут також використовується автоматична передача залежностей за допомогою autowiring: - -```neon -services: - - create: MyClass - setup: - - setCache -``` - - -Встановленням змінної -===================== - -Залежності передаються записом безпосередньо в змінну-член: - -```php -class MyClass -{ - public Cache $cache; -} - -$obj = new MyClass; -$obj->cache = $cache; -``` - -Цей спосіб вважається недоречним, оскільки змінна-член повинна бути оголошена як `public`. А отже, ми не маємо контролю над тим, що передана залежність буде дійсно зазначеного типу (це було актуально до PHP 7.4), і втрачаємо можливість реагувати на новопризначену залежність власним кодом, наприклад, запобігти подальшій зміні. Водночас змінна стає частиною публічного інтерфейсу класу, що може бути небажаним. - -Встановлення змінної ми визначаємо в конфігурації DI-контейнера в [секції setup |services#Setup]: - -```neon -services: - - create: MyClass - setup: - - $cache = @\Cache -``` - - -Inject -====== - -Хоча попередні три способи застосовуються загалом у всіх об'єктно-орієнтованих мовах, ін'єкція методом, анотацією чи атрибутом *inject* є специфічною виключно для презентерів у Nette. Про них йдеться в [окремому розділі |best-practices:inject-method-attribute]. - - -Який спосіб обрати? -=================== - -- конструктор підходить для обов'язкових залежностей, які клас неодмінно потребує для своєї роботи -- сеттер, навпаки, підходить для необов'язкових залежностей або залежностей, які можна буде змінювати надалі -- публічні змінні не підходять diff --git a/dependency-injection/uk/services.texy b/dependency-injection/uk/services.texy deleted file mode 100644 index 1db05bb9d8..0000000000 --- a/dependency-injection/uk/services.texy +++ /dev/null @@ -1,458 +0,0 @@ -Визначення сервісів -******************* - -.[perex] -Конфігурація - це місце, де ми навчаємо DI-контейнер, як створювати окремі сервіси та як пов'язувати їх з іншими залежностями. Nette надає дуже зрозумілий та елегантний спосіб досягти цього. - -Секція `services` у конфігураційному файлі формату NEON - це місце, де ми визначаємо власні сервіси та їхню конфігурацію. Розглянемо простий приклад визначення сервісу під назвою `database`, який представляє екземпляр класу `PDO`: - -```neon -services: - database: PDO('sqlite::memory:') -``` - -Наведена конфігурація призведе до створення наступного фабричного методу в [DI-контейнері |container]: - -```php -public function createServiceDatabase(): PDO -{ - return new PDO('sqlite::memory:'); -} -``` - -Назви сервісів дозволяють нам посилатися на них в інших частинах конфігураційного файлу у форматі `@назваСервісу`. Якщо немає потреби називати сервіс, ми можемо просто використати маркер списку: - -```neon -services: - - PDO('sqlite::memory:') -``` - -Для отримання сервісу з DI-контейнера ми можемо використати метод `getService()` з назвою сервісу як параметром, або метод `getByType()` з типом сервісу: - -```php -$database = $container->getService('database'); -$database = $container->getByType(PDO::class); -``` - - -Створення сервісу -================= - -Зазвичай ми створюємо сервіс просто шляхом створення екземпляра певного класу. Наприклад: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Якщо нам потрібно розширити конфігурацію додатковими ключами, визначення можна розписати на кілька рядків: - -```neon -services: - database: - create: PDO('sqlite::memory:') - setup: ... -``` - -Ключ `create` має псевдонім `factory`, обидва варіанти поширені на практиці. Однак ми рекомендуємо використовувати `create`. - -Аргументи конструктора або методу створення можуть бути альтернативно записані в ключі `arguments`: - -```neon -services: - database: - create: PDO - arguments: ['mysql:host=127.0.0.1;dbname=test', root, secret] -``` - -Сервіси не обов'язково створюються лише простим створенням екземпляра класу, вони також можуть бути результатом виклику статичних методів або методів інших сервісів: - -```neon -services: - database: DatabaseFactory::create() - router: @routerFactory::create() -``` - -Зверніть увагу, що для простоти замість `->` використовується `::`, див. [##виразні засоби]. Будуть згенеровані такі фабричні методи: - -```php -public function createServiceDatabase(): PDO -{ - return DatabaseFactory::create(); -} - -public function createServiceRouter(): RouteList -{ - return $this->getService('routerFactory')->create(); -} -``` - -DI-контейнеру потрібно знати тип створеного сервісу. Якщо ми створюємо сервіс за допомогою методу, який не має вказаного типу повернення, ми повинні явно вказати цей тип у конфігурації: - -```neon -services: - database: - create: DatabaseFactory::create() - type: PDO -``` - - -Аргументи -========= - -Ми передаємо аргументи в конструктор та методи способом, дуже схожим на сам PHP: - -```neon -services: - database: PDO('mysql:host=127.0.0.1;dbname=test', root, secret) -``` - -Для кращої читабельності ми можемо розписати аргументи на окремі рядки. У такому випадку використання ком є необов'язковим: - -```neon -services: - database: PDO( - 'mysql:host=127.0.0.1;dbname=test' - root - secret - ) -``` - -Ви також можете назвати аргументи і тоді не турбуватися про їхній порядок: - -```neon -services: - database: PDO( - username: root - password: secret - dsn: 'mysql:host=127.0.0.1;dbname=test' - ) -``` - -Якщо ви хочете пропустити деякі аргументи та використати їхнє значення за замовчуванням або підставити сервіс за допомогою [autowiring |autowiring], використовуйте підкреслення: - -```neon -services: - foo: Foo(_, %appDir%) -``` - -Як аргументи можна передавати сервіси, використовувати параметри та багато іншого, див. [##виразні засоби]. - - -Setup -===== - -У секції `setup` ми визначаємо методи, які мають викликатися при створенні сервісу. - -```neon -services: - database: - create: PDO(%dsn%, %user%, %password%) - setup: - - setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION) -``` - -У PHP це виглядало б так: - -```php -public function createServiceDatabase(): PDO -{ - $service = new PDO('...', '...', '...'); - $service->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - return $service; -} -``` - -Крім виклику методів, можна також передавати значення у властивості. Також підтримується додавання елемента до масиву, що потрібно записувати в лапках, щоб не конфліктувати з синтаксисом NEON: - -```neon -services: - foo: - create: Foo - setup: - - $value = 123 - - '$onClick[]' = [@bar, clickHandler] -``` - -Що в PHP-коді виглядало б наступним чином: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - $service->value = 123; - $service->onClick[] = [$this->getService('bar'), 'clickHandler']; - return $service; -} -``` - -Однак у setup можна викликати також статичні методи або методи інших сервісів. Якщо вам потрібно передати поточний сервіс як аргумент, вкажіть його як `@self`: - -```neon -services: - foo: - create: Foo - setup: - - My\Helpers::initializeFoo(@self) - - @anotherService::setFoo(@self) -``` - -Зверніть увагу, що для простоти замість `->` використовується `::`, див. [##виразні засоби]. Буде згенеровано такий фабричний метод: - -```php -public function createServiceFoo(): Foo -{ - $service = new Foo; - My\Helpers::initializeFoo($service); - $this->getService('anotherService')->setFoo($service); - return $service; -} -``` - - -Виразні засоби -============== - -Nette DI надає нам надзвичайно багаті виразні засоби, за допомогою яких ми можемо записати майже будь-що. Таким чином, у конфігураційних файлах ми можемо використовувати [параметри |configuration#Параметри]: - -```neon -# параметр -%wwwDir% - -# значення параметра за ключем -%mailer.user% - -# параметр всередині рядка -'%wwwDir%/images' -``` - -Далі створювати об'єкти, викликати методи та функції: - -```neon -# створення об'єкта -DateTime() - -# виклик статичного методу -Collator::create(%locale%) - -# виклик функції PHP -::getenv(DB_USER) -``` - -Посилатися на сервіси або за їхньою назвою, або за типом: - -```neon -# сервіс за назвою -@database - -# сервіс за типом -@Nette\Database\Connection -``` - -Використовувати синтаксис first-class callable: .{data-version:3.2.0} - -```neon -# створення callback, аналог [@user, logout] -@user::logout(...) -``` - -Використовувати константи: - -```neon -# константа класу -FilesystemIterator::SKIP_DOTS - -# глобальну константу отримуємо функцією PHP constant() -::constant(PHP_VERSION) -``` - -Виклики методів можна ланцюгувати так само, як у PHP. Лише для простоти замість `->` використовується `::`: - -```neon -DateTime()::format('Y-m-d') -# PHP: (new DateTime())->format('Y-m-d') - -@http.request::getUrl()::getHost() -# PHP: $this->getService('http.request')->getUrl()->getHost() -``` - -Ці вирази можна використовувати будь-де, при [створенні сервісів |#Створення сервісу], в [аргументах |#Аргументи], у секції [#setup] або [параметрах |configuration#Параметри]: - -```neon -parameters: - ipAddress: @http.request::getRemoteAddress() - -services: - database: - create: DatabaseFactory::create( @anotherService::getDsn() ) - setup: - - initialize( ::getenv('DB_USER') ) -``` - - -Спеціальні функції ------------------- - -У конфігураційних файлах ви можете використовувати ці спеціальні функції: - -- `not()` заперечення значення -- `bool()`, `int()`, `float()`, `string()` перетворення типу без втрат -- `typed()` створює масив усіх сервісів зазначеного типу -- `tagged()` створює масив усіх сервісів із заданим тегом - -```neon -services: - - Foo( - id: int(::getenv('ProjectId')) - productionMode: not(%debugMode%) - ) -``` - -На відміну від класичного перетворення типів у PHP, такого як `(int)`, перетворення без втрат викине виняток для нечислових значень. - -Функція `typed()` створює масив усіх сервісів даного типу (класу або інтерфейсу). Вона пропускає сервіси, у яких вимкнено autowiring. Можна вказати кілька типів, розділених комою. - -```neon -services: - - BarsDependent( typed(Bar) ) -``` - -Масив сервісів певного типу ви також можете передавати як аргумент автоматично за допомогою [autowiring |autowiring#Масив сервісів]. - -Функція `tagged()` створює масив усіх сервісів з певним тегом. Тут також можна вказати кілька тегів, розділених комою. - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - - -Autowiring -========== - -Ключ `autowired` дозволяє впливати на поведінку autowiring для конкретного сервісу. Детальніше див. [розділ про autowiring |autowiring]. - -```neon -services: - foo: - create: Foo - autowired: false # сервіс foo виключено з autowiring -``` - - -Lazy-сервіси .{data-version:3.2.4} -================================== - -Lazy loading (ліниве завантаження) - це техніка, яка відкладає створення сервісу до моменту, коли він дійсно потрібен. У глобальній конфігурації можна [увімкнути ліниве створення |configuration#Lazy-сервіси] для всіх сервісів одночасно. Для окремих сервісів ви можете змінити цю поведінку: - -```neon -services: - foo: - create: Foo - lazy: false -``` - -Коли сервіс визначено як lazy, при його запиті з DI-контейнера ми отримуємо спеціальний об'єкт-заступник. Він виглядає і поводиться так само, як реальний сервіс, але фактична ініціалізація (виклик конструктора та setup) відбувається лише при першому виклику будь-якого його методу або властивості. - -.[note] -Lazy loading можна використовувати лише для користувацьких класів, а не для внутрішніх класів PHP. Потребує PHP 8.4 або новішої версії. - - -Теги -==== - -Теги служать для додавання додаткової інформації до сервісів. До сервісу можна додати один або кілька тегів: - -```neon -services: - foo: - create: Foo - tags: - - cached -``` - -Теги також можуть містити значення: - -```neon -services: - foo: - create: Foo - tags: - logger: monolog.logger.event -``` - -Щоб отримати всі сервіси з певними тегами, ви можете використати функцію `tagged()`: - -```neon -services: - - LoggersDependent( tagged(logger) ) -``` - -У DI-контейнері ви можете отримати назви всіх сервісів з певним тегом за допомогою методу `findByTag()`: - -```php -$names = $container->findByTag('logger'); -// $names - це масив, що містить назву сервісу та значення тегу -// напр. ['foo' => 'monolog.logger.event', ...] -``` - - -Режим Inject -============ - -За допомогою прапорця `inject: true` активується передача залежностей через публічні змінні з анотацією [inject |best-practices:inject-method-attribute#Атрибути Inject] та методи [inject*() |best-practices:inject-method-attribute#Методи inject]. - -```neon -services: - articles: - create: App\Model\Articles - inject: true -``` - -За замовчуванням `inject` активовано лише для презентерів. - - -Модифікація сервісів -==================== - -DI-контейнер містить багато сервісів, які були додані за допомогою вбудованого або [користувацького розширення |extensions]. Ви можете змінювати визначення цих сервісів безпосередньо в конфігурації. Наприклад, ви можете змінити клас сервісу `application.application`, який за замовчуванням є `Nette\Application\Application`, на інший: - -```neon -services: - application.application: - create: MyApplication - alteration: true -``` - -Прапорець `alteration` є інформативним і вказує, що ми лише модифікуємо існуючий сервіс. - -Ми також можемо доповнити setup: - -```neon -services: - application.application: - create: MyApplication - alteration: true - setup: - - '$onStartup[]' = [@resource, init] -``` - -При переписуванні сервісу ми можемо захотіти видалити початкові аргументи, елементи setup або теги, для чого служить `reset`: - -```neon -services: - application.application: - create: MyApplication - alteration: true - reset: - - arguments - - setup - - tags -``` - -Якщо ви хочете видалити сервіс, доданий розширенням, ви можете зробити це так: - -```neon -services: - cache.journal: false -``` diff --git a/forms/bg/@home.texy b/forms/bg/@home.texy deleted file mode 100644 index ca9dfebeda..0000000000 --- a/forms/bg/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Nette Forms донесоха революция в създаването на уеб форми. Изведнъж стана достатъчно да напишете няколко разбираеми реда код и имахте готова форма, включително рендиране, JavaScript и сървърна валидация, и освен това отлично защитена. Ще ви покажем как: - -- да създавате удобни за потребителя форми -- да валидирате изпратените данни -- да рендирате елементи точно според нуждите - -</div> - - -Използвайки Nette Forms, ще избегнете редица рутинни задачи, като например писане на валидация (при това двойна, на страната на сървъра и клиента), ще минимизирате вероятността от възникване на грешки и пропуски в сигурността. - -Формите можете да използвате или като част от Nette Приложение (т.е. в презентери), или напълно самостоятелно. Тъй като в двата случая използването се различава малко, подготвихме за вас два урока: - -<div class="wiki-buttons"> -<div> "Форми в презентери .[wiki-button]":in-presenter </div> -<div> "Форми самостоятелно .[wiki-button]":standalone </div> -</div> - - -Инсталация ----------- - -Изтеглете и инсталирайте библиотеката с помощта на [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/bg/@left-menu.texy b/forms/bg/@left-menu.texy deleted file mode 100644 index 489560e8be..0000000000 --- a/forms/bg/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Въведение |@home] -- [Форми в презентери|in-presenter] -- [Форми самостоятелно|standalone] -- [Формулярни елементи |controls] -- [Валидация |validation] -- [Рендиране |rendering] -- [Конфигурация |configuration] - - -Допълнително четене -******************* -- [Ръководства и процедури |best-practices:] diff --git a/forms/bg/@meta.texy b/forms/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/forms/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/forms/bg/configuration.texy b/forms/bg/configuration.texy deleted file mode 100644 index a0e73d82cc..0000000000 --- a/forms/bg/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Конфигурация на формуляри -************************* - -.[perex] -В конфигурацията могат да се променят съобщенията за грешки във формуляри по подразбиране [съобщения за грешки във формуляри|validation]. - -```neon -forms: - messages: - 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.' - 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.' -``` - -Ето превода на български език: - -```neon -forms: - messages: - Equal: 'Моля, въведете %s.' - NotEqual: 'Тази стойност не трябва да бъде %s.' - Filled: 'Това поле е задължително.' - Blank: 'Това поле трябва да бъде празно.' - MinLength: 'Моля, въведете поне %d знака.' - MaxLength: 'Моля, въведете не повече от %d знака.' - Length: 'Моля, въведете стойност с дължина между %d и %d знака.' - Email: 'Моля, въведете валиден имейл адрес.' - URL: 'Моля, въведете валиден URL адрес.' - Integer: 'Моля, въведете валидно цяло число.' - Float: 'Моля, въведете валидно число.' - Min: 'Моля, въведете стойност, по-голяма или равна на %d.' - Max: 'Моля, въведете стойност, по-малка или равна на %d.' - Range: 'Моля, въведете стойност между %d и %d.' - MaxFileSize: 'Размерът на качения файл може да бъде до %d байта.' - MaxPostSize: 'Качените данни надвишават ограничението от %d байта.' - MimeType: 'Каченият файл не е в очаквания формат.' - Image: 'Каченият файл трябва да бъде изображение във формат JPEG, GIF, PNG или WebP.' - Nette\Forms\Controls\SelectBox::Valid: 'Моля, изберете валидна опция.' - Nette\Forms\Controls\UploadControl::Valid: 'Възникна грешка при качване на файла.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Вашата сесия изтече. Моля, върнете се на началната страница и опитайте отново.' -``` - -Ако не използвате целия framework и следователно не използвате конфигурационни файлове, можете да промените съобщенията за грешки по подразбиране директно в масива `Nette\Forms\Validator::$messages`. diff --git a/forms/bg/controls.texy b/forms/bg/controls.texy deleted file mode 100644 index cf98691e43..0000000000 --- a/forms/bg/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Елементи на формуляр -******************** - -.[perex] -Преглед на стандартните елементи на формуляр. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Добавя едноредово текстово поле (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Ако потребителят не попълни полето, връща празен низ `''`, или чрез `setNullable()` може да се укаже да връща `null`. - -```php -$form->addText('name', 'Име:') - ->setRequired() - ->setNullable(); -``` - -Автоматично валидира UTF-8, премахва водещите и крайните интервали и премахва знаците за нов ред, които атакуващ би могъл да изпрати. - -Максималната дължина може да се ограничи чрез `setMaxLength()`. Промяна на въведената от потребителя стойност позволява [addFilter() |validation#Модификация на входа]. - -Чрез `setHtmlType()` може да се промени визуалният характер на текстовото поле на типове като `search`, `tel` или `url` вижте [спецификация|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Помнете, че промяната на типа е само визуална и не замества функцията за валидация. За тип `url` е препоръчително да се добави специфично [правило URL |validation#Текстови полета]. - -.[note] -За други типове входове, като `number`, `range`, `email`, `date`, `datetime-local`, `time` и `color`, използвайте специализирани методи като [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] и [#addColor], които осигуряват сървърна валидация. Типовете `month` и `week` засега не се поддържат напълно във всички браузъри. - -На елемента може да се зададе т.нар. empty-value, което е нещо като стойност по подразбиране, но ако потребителят не я промени, елементът връща празен низ или `null`. - -```php -$form->addText('phone', 'Телефон:') - ->setHtmlType('tel') - ->setEmptyValue('+359'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Добавя поле за въвеждане на многоредов текст (клас [TextArea |api:Nette\Forms\Controls\TextArea]). Ако потребителят не попълни полето, връща празен низ `''`, или чрез `setNullable()` може да се укаже да връща `null`. - -```php -$form->addTextArea('note', 'Бележка:') - ->addRule($form::MaxLength, 'Бележката е твърде дълга', 10000); -``` - -Автоматично валидира UTF-8 и нормализира разделителите на редове на `\n`. За разлика от едноредовото входно поле, не се извършва премахване на интервали. - -Максималната дължина може да се ограничи чрез `setMaxLength()`. Промяна на въведената от потребителя стойност позволява [addFilter() |validation#Модификация на входа]. Може да се зададе т.нар. empty-value чрез `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Добавя поле за въвеждане на цяло число (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Връща или integer, или `null`, ако потребителят не въведе нищо. - -```php -$form->addInteger('year', 'Година:') - ->addRule($form::Range, 'Годината трябва да бъде в диапазона от %d до %d.', [1900, 2023]); -``` - -Елементът се рендира като `<input type="number">`. Чрез използване на метода `setHtmlType()` може да се промени типът на `range` за показване под формата на плъзгач, или на `text`, ако предпочитате стандартно текстово поле без специалното поведение на тип `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Добавя поле за въвеждане на десетично число (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Връща или float, или `null`, ако потребителят не въведе нищо. - -```php -$form->addFloat('level', 'Ниво:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Нивото трябва да бъде в диапазона от %d до %d.', [0, 100]); -``` - -Елементът се рендира като `<input type="number">`. Чрез използване на метода `setHtmlType()` може да се промени типът на `range` за показване под формата на плъзгач, или на `text`, ако предпочитате стандартно текстово поле без специалното поведение на тип `number`. - -Nette и браузърът Chrome приемат като разделител на десетичните места както запетая, така и точка. За да бъде тази функционалност достъпна и във Firefox, е препоръчително да се зададе атрибутът `lang` или за дадения елемент, или за цялата страница, например `<html lang="bg">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Добавя поле за въвеждане на имейл адрес (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Ако потребителят не попълни полето, връща празен низ `''`, или чрез `setNullable()` може да се укаже да връща `null`. - -```php -$form->addEmail('email', 'Имейл:'); -``` - -Оверява дали стойността е валиден имейл адрес. Не се проверява дали домейнът действително съществува, проверява се само синтаксисът. Автоматично валидира UTF-8, премахва водещите и крайните интервали. - -Максималната дължина може да се ограничи чрез `setMaxLength()`. Промяна на въведената от потребителя стойност позволява [addFilter() |validation#Модификация на входа]. Може да се зададе т.нар. empty-value чрез `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Добавя поле за въвеждане на парола (клас [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Парола:') - ->setRequired() - ->addRule($form::MinLength, 'Паролата трябва да съдържа поне %d знака', 8) - ->addRule($form::Pattern, 'Трябва да съдържа цифра', '.*[0-9].*'); -``` - -При повторно показване на формуляра полето ще бъде празно. Автоматично валидира UTF-8, премахва водещите и крайните интервали и премахва знаците за нов ред, които атакуващ би могъл да изпрати. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Добавя чекбокс (клас [Checkbox |api:Nette\Forms\Controls\Checkbox]). Връща стойност `true` или `false`, в зависимост от това дали е отметнат. - -```php -$form->addCheckbox('agree', 'Съгласен съм с условията') - ->setRequired('Необходимо е да се съгласите с условията'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Добавя чекбоксове за избор на няколко елемента (клас [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Връща масив от ключовете на избраните елементи. Методът `getSelectedItems()` връща стойностите вместо ключовете. - -```php -$form->addCheckboxList('colors', 'Цветове:', [ - 'r' => 'червен', - 'g' => 'зелен', - 'b' => 'син', -]); -``` - -Масивът с предлаганите елементи предаваме като трети параметър или чрез метода `setItems()`. - -Чрез `setDisabled(['r', 'g'])` могат да се деактивират отделни елементи. - -Елементът автоматично проверява дали не е настъпило подправяне и дали избраните елементи са действително едни от предлаганите и не са били деактивирани. Чрез метода `getRawValue()` могат да се получат изпратените елементи без тази важна проверка. - -При задаване на избраните по подразбиране елементи също проверява дали те са едни от предлаганите, в противен случай хвърля изключение. Тази проверка може да се изключи чрез `checkDefaultValue(false)`. - -Ако изпращате формуляра с метод `GET`, можете да изберете по-компактен начин за пренос на данни, който спестява размер на query string-а. Активира се чрез задаване на HTML атрибут на формуляра: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Добавя радио бутони (клас [RadioList |api:Nette\Forms\Controls\RadioList]). Връща ключа на избрания елемент, или `null`, ако потребителят не е избрал нищо. Методът `getSelectedItem()` връща стойността вместо ключа. - -```php -$sex = [ - 'm' => 'мъж', - 'f' => 'жена', -]; -$form->addRadioList('gender', 'Пол:', $sex); -``` - -Масивът с предлаганите елементи предаваме като трети параметър или чрез метода `setItems()`. - -Чрез `setDisabled(['m', 'f'])` могат да се деактивират отделни елементи. - -Елементът автоматично проверява дали не е настъпило подправяне и дали избраният елемент е действително един от предлаганите и не е бил деактивиран. Чрез метода `getRawValue()` може да се получи изпратеният елемент без тази важна проверка. - -При задаване на избрания по подразбиране елемент също проверява дали той е един от предлаганите, в противен случай хвърля изключение. Тази проверка може да се изключи чрез `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Добавя селект бокс (клас [SelectBox |api:Nette\Forms\Controls\SelectBox]). Връща ключа на избрания елемент, или `null`, ако потребителят не е избрал нищо. Методът `getSelectedItem()` връща стойността вместо ключа. - -```php -$countries = [ - 'BG' => 'България', - 'CZ' => 'Чешка република', - 'SK' => 'Словакия', -]; - -$form->addSelect('country', 'Държава:', $countries) - ->setDefaultValue('BG'); -``` - -Масивът с предлаганите елементи предаваме като трети параметър или чрез метода `setItems()`. Елементите могат да бъдат и двумерен масив: - -```php -$countries = [ - 'Европа' => [ - 'BG' => 'България', - 'CZ' => 'Чешка република', - 'SK' => 'Словакия', - ], - 'CA' => 'Канада', - 'US' => 'САЩ', - '?' => 'друга', -]; -``` - -При селект боксовете често първият елемент има специално значение, служи като призив за действие. За добавяне на такъв елемент служи методът `setPrompt()`. - -```php -$form->addSelect('country', 'Държава:', $countries) - ->setPrompt('Изберете държава'); -``` - -Чрез `setDisabled(['CZ', 'SK'])` могат да се деактивират отделни елементи. - -Елементът автоматично проверява дали не е настъпило подправяне и дали избраният елемент е действително един от предлаганите и не е бил деактивиран. Чрез метода `getRawValue()` може да се получи изпратеният елемент без тази важна проверка. - -При задаване на избрания по подразбиране елемент също проверява дали той е един от предлаганите, в противен случай хвърля изключение. Тази проверка може да се изключи чрез `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Добавя селект бокс за избор на няколко елемента (клас [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Връща масив от ключовете на избраните елементи. Методът `getSelectedItems()` връща стойностите вместо ключовете. - -```php -$form->addMultiSelect('countries', 'Държави:', $countries); -``` - -Масивът с предлаганите елементи предаваме като трети параметър или чрез метода `setItems()`. Елементите могат да бъдат и двумерен масив. - -Чрез `setDisabled(['CZ', 'SK'])` могат да се деактивират отделни елементи. - -Елементът автоматично проверява дали не е настъпило подправяне и дали избраните елементи са действително едни от предлаганите и не са били деактивирани. Чрез метода `getRawValue()` могат да се получат изпратените елементи без тази важна проверка. - -При задаване на избраните по подразбиране елементи също проверява дали те са едни от предлаганите, в противен случай хвърля изключение. Тази проверка може да се изключи чрез `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Добавя поле за качване на файл (клас [UploadControl |api:Nette\Forms\Controls\UploadControl]). Връща обект [FileUpload |http:request#FileUpload] и то дори в случай, че потребителят не е изпратил никакъв файл, което може да се установи чрез метода `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Аватар:') - ->addRule($form::Image, 'Аватарът трябва да е JPEG, PNG, GIF, WebP или AVIF.') - ->addRule($form::MaxFileSize, 'Максималният размер е 1 MB.', 1024 * 1024); -``` - -Ако файлът не успее да се качи коректно, формулярът не е успешно изпратен и се показва грешка. Т.е. при успешно изпращане не е необходимо да се проверява методът `FileUpload::isOk()`. - -Никога не вярвайте на оригиналното име на файла, върнато от метода `FileUpload::getName()`, клиентът може да е изпратил злонамерено име на файл с намерение да повреди или хакне вашето приложение. - -Правилата `MimeType` и `Image` откриват изисквания тип въз основа на сигнатурата на файла и не проверяват неговата цялост. Дали изображението не е повредено може да се установи например чрез опит за неговото [зареждане |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Добавя поле за качване на няколко файла едновременно (клас [UploadControl |api:Nette\Forms\Controls\UploadControl]). Връща масив от обекти [FileUpload |http:request#FileUpload]. Методът `FileUpload::hasFile()` при всеки от тях ще връща `true`. - -```php -$form->addMultiUpload('files', 'Файлове:') - ->addRule($form::MaxLength, 'Могат да бъдат качени максимум %d файла', 10); -``` - -Ако някой файл не успее да се качи коректно, формулярът не е успешно изпратен и се показва грешка. Т.е. при успешно изпращане не е необходимо да се проверява методът `FileUpload::isOk()`. - -Никога не вярвайте на оригиналните имена на файловете, върнати от метода `FileUpload::getName()`, клиентът може да е изпратил злонамерено име на файл с намерение да повреди или хакне вашето приложение. - -Правилата `MimeType` и `Image` откриват изисквания тип въз основа на сигнатурата на файла и не проверяват неговата цялост. Дали изображението не е повредено може да се установи например чрез опит за неговото [зареждане |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Добавя поле, което позволява на потребителя лесно да въведе дата, състояща се от година, месец и ден (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Като стойност по подразбиране приема или обекти, имплементиращи интерфейса `DateTimeInterface`, низ с време, или число, представляващо UNIX timestamp. Същото важи и за аргументите на правилата `Min`, `Max` или `Range`, които дефинират минималната и максималната разрешена дата. - -```php -$form->addDate('date', 'Дата:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Датата трябва да е поне преди един месец.', new DateTime('-1 month')); -``` - -Стандартно връща обект `DateTimeImmutable`, чрез метода `setFormat()` можете да специфицирате [текстов формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] или timestamp: - -```php -$form->addDate('date', 'Дата:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Добавя поле, което позволява на потребителя лесно да въведе час, състоящ се от часове, минути и по избор и секунди (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Като стойност по подразбиране приема или обекти, имплементиращи интерфейса `DateTimeInterface`, низ с време, или число, представляващо UNIX timestamp. От тези входове се използва само информацията за времето, датата се игнорира. Същото важи и за аргументите на правилата `Min`, `Max` или `Range`, които дефинират минималния и максималния разрешен час. Ако зададената минимална стойност е по-висока от максималната, се създава времеви диапазон, преминаващ през полунощ. - -```php -$form->addTime('time', 'Час:', withSeconds: true) - ->addRule($form::Range, 'Часът трябва да бъде в диапазона от %s до %s.', ['12:30', '13:30']); -``` - -Стандартно връща обект `DateTimeImmutable` (с дата 1 януари година 1), чрез метода `setFormat()` можете да специфицирате [текстов формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Час:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Добавя поле, което позволява на потребителя лесно да въведе дата и час, състоящи се от година, месец, ден, часове, минути и по избор и секунди (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Като стойност по подразбиране приема или обекти, имплементиращи интерфейса `DateTimeInterface`, низ с време, или число, представляващо UNIX timestamp. Същото важи и за аргументите на правилата `Min`, `Max` или `Range`, които дефинират минималната и максималната разрешена дата. - -```php -$form->addDateTime('datetime', 'Дата и час:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Датата трябва да е поне преди един месец.', new DateTime('-1 month')); -``` - -Стандартно връща обект `DateTimeImmutable`, чрез метода `setFormat()` можете да специфицирате [текстов формат|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] или timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Добавя поле за избор на цвят (клас [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Цветът е низ във формата `#rrggbb`. Ако потребителят не направи избор, се връща черен цвят `#000000`. - -```php -$form->addColor('color', 'Цвят:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Добавя скрито поле (клас [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -Чрез `setNullable()` може да се настрои да връща `null` вместо празен низ. Промяна на изпратената стойност позволява [addFilter() |validation#Модификация на входа]. - -Въпреки че елементът е скрит, е **важно да се осъзнае**, че стойността все още може да бъде модифицирана или подправена от атакуващ. Винаги щателно проверявайте и валидирайте всички получени стойности на сървърна страна, за да се предотвратят рискове за сигурността, свързани с манипулиране на данни. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Добавя бутон за изпращане (клас [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Изпрати'); -``` - -Във формуляра е възможно да има и няколко бутона за изпращане: - -```php -$form->addSubmit('register', 'Регистрирай се'); -$form->addSubmit('cancel', 'Отказ'); -``` - -За да установите кой от тях е бил кликнат, използвайте: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Ако не искате да валидирате целия формуляр при натискане на бутона (например при бутони *Отказ* или *Преглед*), използвайте [setValidationScope() |validation#Изключване на валидацията]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Добавя бутон (клас [Button |api:Nette\Forms\Controls\Button]), който няма функция за изпращане. Може следователно да се използва за някаква друга функция, напр. извикване на JavaScript функция при кликване. - -```php -$form->addButton('raise', 'Увеличи заплатата') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Добавя бутон за изпращане под формата на изображение (клас [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image'); -``` - -При използване на няколко бутона за изпращане може да се установи кой е бил кликнат, чрез `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Добавя подформуляр (клас [Container|api:Nette\Forms\Container]), или контейнер, в който могат да се добавят други елементи по същия начин, както ги добавяме към формуляра. Работят и методите `setDefaults()` или `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Вашето име:'); -$sub1->addEmail('email', 'Имейл:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Вашето име:'); -$sub2->addEmail('email', 'Имейл:'); -``` - -Изпратените данни след това връща като многомерна структура: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Преглед на настройките -====================== - -При всички елементи можем да извикваме следните методи (пълен преглед в [API документация|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | задава стойност по подразбиране -| `getValue()` | получава текущата стойност -| `setOmitted()` | [#пропускане на стойност] -| `setDisabled()` | [#деактивиране на елементи] - -Рендиране: -.[table-form-methods language-php] -| `setCaption($caption)` | променя етикета на елемента -| `setTranslator($translator)` | задава [преводач |rendering#Превод] -| `setHtmlAttribute($name, $value)` | задава [HTML атрибут |rendering#HTML атрибути] на елемента -| `setHtmlId($id)` | задава HTML атрибут `id` -| `setHtmlType($type)` | задава HTML атрибут `type` -| `setHtmlName($name)` | задава HTML атрибут `name` -| `setOption($key, $value)` | [настройка за рендиране |rendering#Options] - -Валидация: -.[table-form-methods language-php] -| `setRequired()` | [задължителен елемент |validation] -| `addRule()` | задава [правило за валидация |validation#Правила] -| `addCondition()`, `addConditionOn()` | задава [условие за валидация |validation#Условия] -| `addError($message)` | [предаване на съобщение за грешка |validation#Грешки при обработка] - -При елементите `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` могат да се извикват следните методи: - -.[table-form-methods language-php] -| `setNullable()` | задава дали getValue() да връща `null` вместо празен низ -| `setEmptyValue($value)` | задава специална стойност, която се счита за празен низ -| `setMaxLength($length)` | задава максималния брой разрешени знаци -| `addFilter($filter)` | [редактиране на въведеното |validation#Модификация на входа] - - -Пропускане на стойност -====================== - -Ако попълнената от потребителя стойност не ни интересува, можем чрез `setOmitted()` да я пропуснем от резултата на метода `$form->getValues()` или от данните, предавани на хендлърите. Това е полезно за различни пароли за проверка, антиспам елементи и т.н. - -```php -$form->addPassword('passwordVerify', 'Парола за проверка:') - ->setRequired('Моля, въведете паролата отново за проверка') - ->addRule($form::Equal, 'Паролите не съвпадат', $form['password']) - ->setOmitted(); -``` - - -Деактивиране на елементи -======================== - -Елементите могат да се деактивират чрез `setDisabled()`. Такъв елемент потребителят не може да редактира. - -```php -$form->addText('username', 'Потребителско име:') - ->setDisabled(); -``` - -Деактивираните елементи браузърът изобщо не изпраща на сървъра, т.е. няма да ги намерите и в данните, върнати от функцията `$form->getValues()`. Ако обаче зададете `setOmitted(false)`, Nette ще включи в тези данни тяхната стойност по подразбиране. - -При извикване на `setDisabled()` от съображения за сигурност **се изтрива стойността на елемента**. Ако задавате стойност по подразбиране, е необходимо да го направите след неговото деактивиране: - -```php -$form->addText('username', 'Потребителско име:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Алтернатива на деактивираните елементи са елементите с HTML атрибут `readonly`, които браузърът изпраща на сървъра. Въпреки че елементът е само за четене, е **важно да се осъзнае**, че неговата стойност все още може да бъде модифицирана или подправена от атакуващ. - - -Персонализирани елементи -======================== - -Освен широката гама от вградени елементи на формуляр, можете да добавяте към формуляра собствени елементи по следния начин: - -```php -$form->addComponent(new DateInput('Дата:'), 'date'); -// алтернативен синтаксис: $form['date'] = new DateInput('Дата:'); -``` - -.[note] -Формулярът е наследник на класа [Container |component-model:#Container], а отделните елементи са наследници на [Component |component-model:#Component]. - -Съществува начин да се дефинират нови методи на формуляра, служещи за добавяне на собствени елементи (напр. `$form->addZip()`). Това са т.нар. extension methods. Недостатъкът е, че за тях няма да работи подсказването в редакторите. - -```php -use Nette\Forms\Container; - -// добавяме метод 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, 'Поне 5 цифри', '[0-9]{5}'); -}); - -// използване -$form->addZip('zip', 'Пощенски код:'); -``` - - -Елементи на ниско ниво -====================== - -Могат да се използват и елементи, които записваме само в шаблона и не ги добавяме към формуляра с някой от методите `$form->addXyz()`. Когато например изписваме записи от база данни и предварително не знаем колко ще бъдат и какви ще бъдат техните ID, и искаме при всеки ред да покажем чекбокс или радио бутон, е достатъчно да го кодираме в шаблона: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -А след изпращане стойността установяваме: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -където първият параметър е типът на елемента (`DataFile` за `type=file`, `DataLine` за едноредови входове като `text`, `password`, `email` и др. и `DataText` за всички останали), а вторият параметър `sel[]` съответства на HTML атрибута name. Типът на елемента можем да комбинираме със стойността `DataKeys`, която запазва ключовете на елементите. Това е полезно особено за `select`, `radioList` и `checkboxList`. - -Същественото е, че `getHttpData()` връща санирана стойност, в този случай това винаги ще бъде масив от валидни UTF-8 низове, независимо какво би се опитал да подхвърли атакуващ на сървъра. Това е аналог на директната работа с `$_POST` или `$_GET`, но със съществената разлика, че винаги връща чисти данни, така както сте свикнали при стандартните елементи на Nette формулярите. diff --git a/forms/bg/in-presenter.texy b/forms/bg/in-presenter.texy deleted file mode 100644 index 1778ed84f5..0000000000 --- a/forms/bg/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Форми в презентерите -******************** - -.[perex] -Nette Forms значително улесняват създаването и обработката на уеб форми. В тази глава ще се запознаете с използването на форми в презентерите. - -Ако се интересувате как да ги използвате напълно самостоятелно без останалата част от framework-а, ръководството за [самостоятелна употреба |standalone] е за вас. - - -Първа форма -=========== - -Нека опитаме да напишем проста форма за регистрация. Кодът ѝ ще бъде следният: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Име:'); -$form->addPassword('password', 'Парола:'); -$form->addSubmit('send', 'Регистрирай се'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -и ще се покаже в браузъра по следния начин: - -[* form-cs.webp *] - -Формата в презентера е обект от класа `Nette\Application\UI\Form`, неговият предшественик `Nette\Forms\Form` е предназначен за самостоятелна употреба. Добавихме към нея така наречените елементи име, парола и бутон за изпращане. И накрая, редът с `$form->onSuccess` казва, че след изпращане и успешна валидация трябва да се извика методът `$this->formSucceeded()`. - -От гледна точка на презентера, формата е обикновен компонент. Затова се третира като компонент и се включва в презентера чрез [фабричен метод |application:components#Фабрични методи]. Ще изглежда така: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Име:'); - $form->addPassword('password', 'Парола:'); - $form->addSubmit('send', 'Регистрирай се'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // тук обработваме данните, изпратени от формата - // $data->name съдържа името - // $data->password съдържа паролата - $this->flashMessage('Бяхте успешно регистриран.'); - $this->redirect('Home:'); - } -} -``` - -И в шаблона рендираме формата с тага `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Регистрация</h1> - -{control registrationForm} -``` - -И това всъщност е всичко :-) Имаме функционална и перфектно [защитена |#Защита от уязвимости] форма. - -И сега вероятно си мислите, че това беше твърде прибързано, чудите се как е възможно да се извика методът `formSucceeded()` и какви са параметрите, които получава. Разбира се, прави сте, това заслужава обяснение. - -Nette всъщност идва със свеж механизъм, който наричаме [Холивудски стил |application:components#Hollywood style]. Вместо вие като разработчик постоянно да питате дали нещо се е случило („формата изпратена ли е?“, „изпратена ли е валидно?“, „фалшифицирана ли е?“), казвате на framework-а „когато формата е валидно попълнена, извикай този метод“ и оставяте останалата работа на него. Ако програмирате на JavaScript, този стил на програмиране ви е добре познат. Пишете функции, които се извикват, когато настъпи определено [събитие |nette:glossary#Събития events]. И езикът им предава съответните аргументи. - -Точно така е изграден и горният код на презентера. Масивът `$form->onSuccess` представлява списък от PHP callback-ове, които Nette извиква в момента, когато формата е изпратена и правилно попълнена (т.е. е валидна). В рамките на [жизнения цикъл на презентера |application:presenters#Жизнен цикъл на презентера] това е така нареченият сигнал, така че те се извикват след метода `action*` и преди метода `render*`. И на всеки callback предава като първи параметър самата форма, а като втори - изпратените данни под формата на обект [ArrayHash |utils:arrays#ArrayHash]. Можете да пропуснете първия параметър, ако не се нуждаете от обекта на формата. А вторият параметър може да бъде по-хитър, но за това [по-късно |#Мапване към класове]. - -Обектът `$data` съдържа ключовете `name` и `password` с данните, които потребителят е попълнил. Обикновено данните се изпращат директно за по-нататъшна обработка, което може да бъде например вмъкване в база данни. По време на обработката обаче може да възникне грешка, например потребителското име вече е заето. В такъв случай предаваме грешката обратно към формата чрез `addError()` и я оставяме да се рендира отново, заедно със съобщението за грешка. - -```php -$form->addError('Извиняваме се, потребителското име вече се използва.'); -``` - -Освен `onSuccess` съществува и `onSubmit`: callback-овете се извикват винаги след изпращане на формата, дори ако тя не е попълнена правилно. И също `onError`: callback-овете се извикват само ако изпращането не е валидно. Те се извикват дори ако във `onSuccess` или `onSubmit` направим формата невалидна чрез `addError()`. - -След обработка на формата пренасочваме към следващата страница. Това предотвратява нежелано повторно изпращане на формата чрез бутона *обнови*, *назад* или чрез движение в историята на браузъра. - -Опитайте да добавите и други [елементи на формата |controls]. - - -Достъп до елементите -==================== - -Формата е компонент на презентера, в нашия случай наречена `registrationForm` (според името на фабричния метод `createComponentRegistrationForm`), така че навсякъде в презентера можете да получите достъп до формата чрез: - -```php -$form = $this->getComponent('registrationForm'); -// алтернативен синтаксис: $form = $this['registrationForm']; -``` - -Отделните елементи на формата също са компоненти, така че можете да получите достъп до тях по същия начин: - -```php -$input = $form->getComponent('name'); // или $input = $form['name']; -$button = $form->getComponent('send'); // или $button = $form['send']; -``` - -Елементите се премахват с помощта на unset: - -```php -unset($form['name']); -``` - - -Правила за валидация -==================== - -Споменахме думата *валидна*, но формата все още няма правила за валидация. Нека поправим това. - -Името ще бъде задължително, затова го маркираме с метода `setRequired()`, чийто аргумент е текстът на съобщението за грешка, което ще се покаже, ако потребителят не попълни името. Ако не посочим аргумент, ще се използва съобщението за грешка по подразбиране. - -```php -$form->addText('name', 'Име:') - ->setRequired('Моля, въведете име'); -``` - -Опитайте да изпратите формата без попълнено име и ще видите, че ще се покаже съобщение за грешка и браузърът или сървърът ще я отхвърлят, докато не попълните полето. - -В същото време няма да измамите системата, като напишете само интервали в полето. Няма начин. Nette автоматично премахва водещите и крайните интервали. Опитайте. Това е нещо, което винаги трябва да правите с всеки едноредов вход, но често се забравя. Nette го прави автоматично. (Можете да опитате да измамите формата и да изпратите многоредов низ като име. Дори тук Nette няма да се обърка и ще преобразува новите редове в интервали.) - -Формата винаги се валидира от страна на сървъра, но също така се генерира JavaScript валидация, която се извършва мигновено и потребителят научава за грешката веднага, без да е необходимо да изпраща формата до сървъра. За това отговаря скриптът `netteForms.js`. Вмъкнете го в шаблона на лейаута: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ако погледнете изходния код на страницата с формата, може да забележите, че Nette вмъква задължителните елементи в елементи с CSS клас `required`. Опитайте да добавите следния стил в шаблона и етикетът „Име“ ще стане червен. Така елегантно ще маркираме задължителните елементи за потребителите: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Добавяме допълнителни правила за валидация с метода `addRule()`. Първият параметър е правилото, вторият отново е текстът на съобщението за грешка, а може да последва и аргумент на правилото за валидация. Какво означава това? - -Ще разширим формата с ново незадължително поле „възраст“, което трябва да бъде цяло число (`addInteger()`) и освен това в допустим диапазон (`$form::Range`). И тук ще използваме третия параметър на метода `addRule()`, с който ще предадем на валидатора необходимия диапазон като двойка `[от, до]`: - -```php -$form->addInteger('age', 'Възраст:') - ->addRule($form::Range, 'Възрастта трябва да е между 18 и 120', [18, 120]); -``` - -.[tip] -Ако потребителят не попълни полето, правилата за валидация няма да бъдат проверени, тъй като елементът е незадължителен. - -Тук възниква възможност за малък рефакторинг. В съобщението за грешка и в третия параметър числата са посочени дублирано, което не е идеално. Ако създавахме [многоезични форми |rendering#Превод] и съобщението, съдържащо числа, беше преведено на няколко езика, евентуалната промяна на стойностите би била затруднена. Поради тази причина е възможно да се използват плейсхолдъри `%d` и Nette ще допълни стойностите: - -```php - ->addRule($form::Range, 'Възрастта трябва да бъде от %d до %d години', [18, 120]); -``` - -Да се върнем към елемента `password`, който също ще направим задължителен и ще проверим минималната дължина на паролата (`$form::MinLength`), отново с използване на плейсхолдър: - -```php -$form->addPassword('password', 'Парола:') - ->setRequired('Изберете парола') - ->addRule($form::MinLength, 'Паролата трябва да съдържа поне %d знака', 8); -``` - -Ще добавим към формата и поле `passwordVerify`, където потребителят ще въведе паролата още веднъж, за проверка. С помощта на правилата за валидация ще проверим дали двете пароли са еднакви (`$form::Equal`). И като параметър ще дадем препратка към първата парола с помощта на [квадратни скоби |#Достъп до елементите]: - -```php -$form->addPassword('passwordVerify', 'Парола за проверка:') - ->setRequired('Моля, въведете паролата отново за проверка') - ->addRule($form::Equal, 'Паролите не съвпадат', $form['password']) - ->setOmitted(); -``` - -С помощта на `setOmitted()` маркирахме елемент, чиято стойност всъщност не ни интересува и който съществува само с цел валидация. Стойността не се предава на `$data`. - -С това имаме напълно функционална форма с валидация както в PHP, така и в JavaScript. Възможностите за валидация на Nette са много по-широки, могат да се създават условия, според тях да се показват и скриват части от страницата и т.н. Всичко ще научите в главата за [валидация на форми |validation]. - - -Стойности по подразбиране -========================= - -Обикновено задаваме стойности по подразбиране на елементите на формата: - -```php -$form->addEmail('email', 'Имейл') - ->setDefaultValue($lastUsedEmail); -``` - -Често е полезно да се зададат стойности по подразбиране на всички елементи едновременно. Например, когато формата се използва за редактиране на записи. Прочитаме записа от базата данни и задаваме стойностите по подразбиране: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Извиквайте `setDefaults()` след дефинирането на елементите. - - -Рендиране на формата -==================== - -По подразбиране формата се рендира като таблица. Отделните елементи отговарят на основното правило за достъпност - всички етикети са написани като `<label>` и са свързани със съответния елемент на формата. При кликване върху етикета курсорът автоматично се появява в полето на формата. - -На всеки елемент можем да задаваме произволни HTML атрибути. Например, да добавим placeholder: - -```php -$form->addInteger('age', 'Възраст:') - ->setHtmlAttribute('placeholder', 'Моля, попълнете възрастта'); -``` - -Има наистина много начини за рендиране на форма, така че на това е посветена [отделна глава за рендиране |rendering]. - - -Мапване към класове -=================== - -Да се върнем към метода `formSucceeded()`, който във втория параметър `$data` получава изпратените данни като обект `ArrayHash`. Тъй като това е генеричен клас, нещо като `stdClass`, при работа с него ще ни липсва известно удобство, като например подсказване на свойствата в редакторите или статичен анализ на кода. Това може да се реши, като за всяка форма имаме конкретен клас, чиито свойства представляват отделните елементи. Напр.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Алтернативно можете да използвате конструктор: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Свойствата на класа с данни могат да бъдат и enum-и и те ще бъдат автоматично мапнати. .{data-version:3.2.4} - -Как да кажем на Nette да ни връща данните като обекти от този клас? По-лесно, отколкото си мислите. Достатъчно е само да посочите класа като тип на параметъра `$data` в обработващия метод: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $data е инстанция на RegistrationFormData - $name = $data->name; - // ... -} -``` - -Като тип може да се посочи и `array` и тогава данните ще бъдат предадени като масив. - -По подобен начин може да се използва и функцията `getValues()`, на която предаваме името на класа или обекта за хидратиране като параметър: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Ако формите образуват многостепенна структура, съставена от контейнери, създайте отделен клас за всеки: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Мапването след това разпознава от типа на свойството `$person`, че трябва да мапне контейнера към класа `PersonFormData`. Ако свойството съдържа масив от контейнери, посочете типа `array` и предайте класа за мапване директно на контейнера: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Можете да генерирате дизайна на класа с данни за формата с помощта на метода `Nette\Forms\Blueprint::dataClass($form)`, който ще го изведе на страницата на браузъра. След това е достатъчно да маркирате кода с кликване и да го копирате в проекта. .{data-version:3.1.15} - - -Множество бутони -================ - -Ако формата има повече от един бутон, обикновено трябва да разграничим кой от тях е бил натиснат. Можем да създадем собствена обработваща функция за всеки бутон. Ще я зададем като хендлър за [събитието |nette:glossary#Събития events] `onClick`: - -```php -$form->addSubmit('save', 'Запази') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Изтрий') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Тези хендлъри се извикват само в случай на валидно попълнена форма, точно както при събитието `onSuccess`. Разликата е, че като първи параметър вместо формата може да се предаде изпращащият бутон, в зависимост от типа, който посочите: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Когато формата се изпрати с бутона <kbd>Enter</kbd>, се счита, че е изпратена с първия бутон. - - -Събитие onAnchor -================ - -Когато изграждаме формата във фабричния метод (като например `createComponentRegistrationForm`), тя все още не знае дали е била изпратена, нито с какви данни. Но има случаи, когато трябва да знаем изпратените стойности, например по-нататъшният вид на формата зависи от тях, или ни трябват за зависими selectbox-ове и т.н. - -Затова можете да оставите частта от кода, която изгражда формата, да бъде извикана едва в момента, когато тя е т. нар. „закотвена“, т.е. вече е свързана с презентера и знае своите изпратени данни. Предаваме такъв код в масива `$onAnchor`: - -```php -$country = $form->addSelect('country', 'Държава:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Град:'); - -$form->onAnchor[] = function () use ($country, $city) { - // тази функция ще се извика, когато формата знае дали е била изпратена и с какви данни - // следователно може да се използва методът getValue() - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Защита от уязвимости -==================== - -Nette Framework поставя голям акцент върху сигурността и затова стриктно се грижи за добрата защита на формите. Прави го напълно прозрачно и не изисква ръчна настройка. - -Освен че защитава формите от атаки [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] и [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], той извършва много малки защити, за които вече не е нужно да мислите. - -Например, филтрира всички контролни знаци от входовете и проверява валидността на UTF-8 кодирането, така че данните от формата винаги ще бъдат чисти. При select box-ове и radio list-ове проверява дали избраните елементи са наистина от предлаганите и не е имало фалшификация. Вече споменахме, че при едноредови текстови входове премахва знаците за край на ред, които нападателят може да е изпратил. При многоредови входове пък нормализира знаците за край на ред. И така нататък. - -Nette решава вместо вас рисковете за сигурността, за които много програмисти дори не подозират, че съществуват. - -Споменатата CSRF атака се състои в това, че нападателят примамва жертвата към страница, която незабелязано в браузъра на жертвата изпълнява заявка към сървъра, на който жертвата е влязла, и сървърът смята, че заявката е изпълнена от жертвата по нейна воля. Затова Nette предотвратява изпращането на POST форма от друг домейн. Ако по някаква причина искате да изключите защитата и да позволите изпращането на формата от друг домейн, използвайте: - -```php -$form->allowCrossOrigin(); // ВНИМАНИЕ! Изключва защитата! -``` - -Тази защита използва SameSite cookie, наречена `_nss`. Защитата чрез SameSite cookie може да не е 100% надеждна, затова е препоръчително да включите и защита чрез токен: - -```php -$form->addProtection(); -``` - -Препоръчваме да защитавате по този начин формите в административната част на сайта, които променят чувствителни данни в приложението. Framework-ът се защитава срещу CSRF атака чрез генериране и проверка на оторизационен токен, който се съхранява в сесията. Затова е необходимо да имате отворена сесия преди показването на формата. В административната част на сайта обикновено сесията вече е стартирана поради влизането на потребителя. В противен случай стартирайте сесията с метода `Nette\Http\Session::start()`. - - -Същата форма в множество презентери -=================================== - -Ако трябва да използвате една и съща форма в множество презентери, препоръчваме да създадете фабрика за нея, която след това да предадете на презентера. Подходящо място за такъв клас е например директорията `app/Forms`. - -Фабричният клас може да изглежда например така: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Име:'); - $form->addSubmit('send', 'Вход'); - return $form; - } -} -``` - -Искаме от класа да произведе формата във фабричния метод за компоненти в презентера: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // можем да променим формата, тук например променяме етикета на бутона - $form['send']->setCaption('Продължи'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // и добавяме хендлър - return $form; -} -``` - -Хендлърът за обработка на формата може да бъде предоставен и от фабриката: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Име:'); - $form->addSubmit('send', 'Вход'); - $form->onSuccess[] = function (Form $form, $data): void { - // тук извършваме обработката на формата - }; - return $form; - } -} -``` - -И така, направихме бързо въведение във формите в Nette. Опитайте да разгледате и директорията [examples |https://github.com/nette/forms/tree/master/examples] в дистрибуцията, където ще намерите още вдъхновение. diff --git a/forms/bg/rendering.texy b/forms/bg/rendering.texy deleted file mode 100644 index 953d4618f1..0000000000 --- a/forms/bg/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Рендиране на форми -****************** - -Външният вид на формите може да бъде много разнообразен. На практика можем да срещнем две крайности. От една страна, стои нуждата в приложението да се рендират редица форми, които визуално си приличат като две капки вода, и ще оценим лесното рендиране без шаблон с помощта на `$form->render()`. Обикновено това е случаят с административните интерфейси. - -От друга страна, има разнообразни форми, за които важи: всяка е оригинал. Техният вид най-добре се описва с HTML език в шаблона на формата. И разбира се, освен двете споменати крайности, ще срещнем много форми, които се намират някъде по средата. - - -Рендиране с помощта на Latte -============================ - -[Шаблонната система Latte|latte:] значително улеснява рендирането на форми и техните елементи. Първо ще покажем как да рендираме формите ръчно, елемент по елемент, и така да получим пълен контрол над кода. По-късно ще покажем как такова рендиране може да бъде [автоматизирано |#Автоматично рендиране]. - -Можете да генерирате дизайна на Latte шаблона за формата с помощта на метода `Nette\Forms\Blueprint::latte($form)`, който го извежда на страницата на браузъра. След това просто маркирайте кода с кликване и го копирайте в проекта си. .{data-version:3.1.15} - - -`{control}` ------------ - -Най-лесният начин да рендирате форма е да напишете в шаблона: - -```latte -{control signInForm} -``` - -Можете да повлияете на външния вид на така рендираната форма чрез конфигуриране на [#Renderer] и [отделните елементи |#HTML атрибути]. - - -`n:name` --------- - -Дефиницията на формата в PHP кода може да бъде изключително лесно свързана с HTML кода. Достатъчно е само да добавите атрибутите `n:name`. Толкова е лесно! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Потребителско име: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Парола: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Имате пълен контрол над вида на получения HTML код. Ако използвате атрибута `n:name` при елементите `<select>`, `<button>` или `<textarea>`, тяхното вътрешно съдържание ще се попълни автоматично. Тагът `<form n:name>` освен това създава локална променлива `$form` с обекта на рендираната форма, а затварящият `</form>` рендира всички нерендирани скрити елементи (същото важи и за `{form} ... {/form}`). - -Не трябва обаче да забравяме да рендираме възможните съобщения за грешки. Както тези, които са добавени към отделните елементи с метода `addError()` (с помощта на `{inputError}`), така и тези, добавени директно към формата (връщат се от `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Потребителско име: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Парола: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -По-сложни елементи на формата, като RadioList или CheckboxList, могат да се рендират по този начин, поотделно: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Не искате да мислите за всеки елемент какъв HTML елемент да използвате за него в шаблона, дали `<input>`, `<textarea>` и т.н.? Решението е универсалният таг `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Потребителско име: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Парола: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Ако формата използва преводач, текстът вътре в таговете `{label}` ще бъде преведен. - -И в този случай по-сложни елементи на формата, като RadioList или CheckboxList, могат да се рендират поотделно: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -За да рендирате само `<input>` в елемента Checkbox, използвайте `{input myCheckbox:}`. В този случай винаги разделяйте HTML атрибутите със запетая `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Извежда съобщение за грешка за елемента на формата, ако има такова. Обикновено обвиваме съобщението в HTML елемент за стилизиране. Можете елегантно да предотвратите рендирането на празен елемент, ако няма съобщение, с помощта на `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -Можем да проверим наличието на грешка с метода `hasErrors()` и съответно да зададем клас на родителския елемент: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -Таговете `{form signInForm}...{/form}` са алтернатива на `<form n:name="signInForm">...</form>`. - - -Автоматично рендиране ---------------------- - -Благодарение на таговете `{input}` и `{label}` можем лесно да създадем общ шаблон за всяка форма. Той ще итерира последователно и ще рендира всички нейни елементи, с изключение на скритите елементи, които ще се рендират автоматично при затваряне на формата с тага `</form>`. Името на рендираната форма ще се очаква в променливата `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -Използваните самозатварящи се двойни тагове `{label .../}` показват етикети, идващи от дефиницията на формата в PHP кода. - -Запазете този общ шаблон например във файл `basic-form.latte` и за да рендирате формата, е достатъчно да го включите и да предадете името (или инстанцията) на формата в параметъра `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Ако при рендирането на една конкретна форма искате да се намесите във вида й и например да рендирате един елемент по различен начин, тогава най-лесният начин е да подготвите предварително блокове в шаблона, които след това ще могат да бъдат презаписани. Блоковете могат да имат и [динамични имена |latte:template-inheritance#Динамични имена на блокове], така че в тях може да се вмъкне и името на рендирания елемент. Например: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -За елемент, напр. `username`, така се създава блок `input-username`, който може лесно да бъде презаписан с помощта на тага [{embed} |latte:template-inheritance#Единично наследяване]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Алтернативно, цялото съдържание на шаблона `basic-form.latte` може да бъде [дефинирано |latte:template-inheritance#Дефиниции] като блок, включително параметъра `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Благодарение на това извикването му ще бъде малко по-лесно: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -При това е достатъчно блокът да се импортира само на едно място, и то в началото на шаблона на лейаута: - -```latte -{import basic-form.latte} -``` - - -Специални случаи ----------------- - -Ако трябва да рендирате само вътрешната част на формата без HTML таговете `<form>`, например при изпращане на снипети, скрийте ги с помощта на атрибута `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Потребителско име: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -С рендирането на елементи вътре във формулярния контейнер ще помогне тагът `{formContainer}`. - -```latte -<p>Кои новини желаете да получавате:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Рендиране без Latte -=================== - -Най-лесният начин да рендирате форма е да извикате: - -```php -$form->render(); -``` - -Можете да повлияете на външния вид на така рендираната форма чрез конфигуриране на [#Renderer] и [отделните елементи |#HTML атрибути]. - - -Ръчно рендиране ---------------- - -Всеки елемент на формата разполага с методи, които генерират HTML код за полето на формата и етикетите. Те могат да го връщат или като низ, или като обект [Nette\Utils\Html|utils:html-elements]: - -- `getControl(): Html|string` връща HTML кода на елемента -- `getLabel($caption = null): Html|string|null` връща HTML кода на етикета, ако съществува - -Така формата може да се рендира елемент по елемент: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -Докато при някои елементи `getControl()` връща единствен HTML елемент (напр. `<input>`, `<select>` и т.н.), при други връща цял фрагмент HTML код (CheckboxList, RadioList). В такъв случай можете да използвате методи, които генерират отделни input-и и етикети, за всеки елемент поотделно: - -- `getControlPart($key = null): ?Html` връща HTML кода на един елемент -- `getLabelPart($key = null): ?Html` връща HTML кода на етикета на един елемент - -.[note] -Тези методи имат префикс `get` по исторически причини, но `generate` би бил по-добър, тъй като при всяко извикване създава и връща нов елемент `Html`. - - -Renderer -======== - -Това е обект, осигуряващ рендирането на формата. Той може да бъде зададен с метода `$form->setRenderer`. Контролът му се предава при извикване на метода `$form->render()`. - -Ако не зададем собствен renderer, ще бъде използван renderer-ът по подразбиране [api:Nette\Forms\Rendering\DefaultFormRenderer]. Той рендира елементите на формата под формата на HTML таблица. Изходът изглежда така: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Име:</label></th> - - <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">Възраст:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Пол:</label></th> - ... -``` - -Дали да се използва или не таблица за скелета на формата е спорно и много уеб дизайнери предпочитат друг markup. Например дефиниционен списък. Затова ще преконфигурираме `DefaultFormRenderer` така, че да рендира формата под формата на списък. Конфигурацията се извършва чрез редактиране на масива [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Първият индекс винаги представлява областта, а вторият - нейния атрибут. Отделните области са показани на изображението: - -[* defaultformrenderer.webp *] - -Стандартно групата елементи `controls` е обвита в таблица `<table>`, всеки `pair` представлява ред на таблицата `<tr>`, а двойката `label` и `control` са клетки `<th>` и `<td>`. Сега ще променим обвиващите елементи. Ще вмъкнем областта `controls` в контейнер `<dl>`, ще оставим областта `pair` без контейнер, ще вмъкнем `label` в `<dt>` и накрая ще обвием `control` с тагове `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Резултатът е следният HTML код: - -```latte -<dl> - <dt><label class="required" for="frm-name">Име:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Възраст:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Пол:</label></dt> - ... -</dl> -``` - -В масива wrappers може да се повлияе на редица други атрибути: - -- добавяне на CSS класове към отделните типове елементи на формата -- разграничаване на четни и нечетни редове с CSS клас -- визуално разграничаване на задължителни и незадължителни елементи -- определяне дали съобщенията за грешки да се показват директно при елементите или над формата - - -Options -------- - -Поведението на Renderer-а може да се контролира и чрез задаване на *options* на отделните елементи на формата. По този начин може да се зададе описание, което ще се изведе до входното поле: - -```php -$form->addText('phone', 'Номер:') - ->setOption('description', 'Този номер ще остане скрит'); -``` - -Ако искаме да поставим HTML съдържание в него, ще използваме класа [Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Номер:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Условия за съхранение на Вашия номер</a>') - ); -``` - -.[tip] -Html елементът може да се използва и вместо етикет: `$form->addCheckbox('conditions', $label)`. - - -Групиране на елементи ---------------------- - -Renderer-ът позволява групиране на елементи във визуални групи (fieldset-и): - -```php -$form->addGroup('Лични данни'); -``` - -След създаване на нова група, тя става активна и всеки новодобавен елемент се добавя и към нея. Така че формата може да се изгражда по този начин: - -```php -$form = new Form; -$form->addGroup('Лични данни'); -$form->addText('name', 'Вашето име:'); -$form->addInteger('age', 'Вашата възраст:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Адрес за доставка'); -$form->addCheckbox('send', 'Изпрати на адрес'); -$form->addText('street', 'Улица:'); -$form->addText('city', 'Град:'); -$form->addSelect('country', 'Държава:', $countries); -``` - -Renderer-ът първо рендира групите и едва след това елементите, които не принадлежат към никоя група. - - -Поддръжка за Bootstrap ----------------------- - -[В примерите |https://github.com/nette/forms/tree/master/examples] ще намерите примери как да конфигурирате Renderer за [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] и [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -HTML атрибути -============= - -За задаване на произволни HTML атрибути на елементите на формата използваме метода `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Номер:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Сортиране по:', ['цена', 'име']) - ->setHtmlAttribute('onchange', 'submit()'); // изпрати при промяна - - -// За задаване на атрибути на самия <form> -$form->setHtmlAttribute('id', 'myForm'); -``` - -Спецификация на типа на елемента: - -```php -$form->addText('tel', 'Вашият телефон:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'въведете телефон'); -``` - -.[warning] -Задаването на типа и други атрибути служи само за визуални цели. Проверката на коректността на входовете трябва да се извършва на сървъра, което се осигурява чрез избор на подходящ [елемент на формата|controls] и посочване на [правила за валидация|validation]. - -На отделните елементи в radio или checkbox списъци можем да зададем HTML атрибут с различни стойности за всеки от тях. Обърнете внимание на двоеточието след `style:`, което осигурява избор на стойност според ключа: - -```php -$colors = ['r' => 'червен', 'g' => 'зелен', 'b' => 'син']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Цветове:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Извежда: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">червен</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">зелен</label> -<label><input type="checkbox" name="colors[]" value="b">син</label> -``` - -За задаване на логически атрибути, като `readonly`, можем да използваме запис с въпросителен знак: - -```php -$form->addCheckboxList('colors', 'Цветове:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // за повече ключове използвайте масив, напр. ['r', 'g'] -``` - -Извежда: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">червен</label> -<label><input type="checkbox" name="colors[]" value="g">зелен</label> -<label><input type="checkbox" name="colors[]" value="b">син</label> -``` - -В случай на selectbox-ове методът `setHtmlAttribute()` задава атрибути на елемента `<select>`. Ако искаме да зададем атрибути на отделните `<option>`, използваме метода `setOptionAttribute()`. Записите с двоеточие и въпросителен знак, посочени по-горе, също работят: - -```php -$form->addSelect('colors', 'Цветове:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Извежда: - -```latte -<select name="colors"> - <option value="r" style="background:red">червен</option> - <option value="g" style="background:green">зелен</option> - <option value="b">син</option> -</select> -``` - - -Прототипи ---------- - -Алтернативен начин за задаване на HTML атрибути е чрез модифициране на шаблона, от който се генерира HTML елементът. Шаблонът е обект `Html` и се връща от метода `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Номер:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -По този начин може да се модифицира и шаблонът на етикета, който се връща от `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -При елементите Checkbox, CheckboxList и RadioList можете да повлияете на шаблона на елемента, който обвива целия елемент. Той се връща от `getContainerPrototype()`. В състояние по подразбиране това е „празен“ елемент, така че нищо не се рендира, но като му зададем име, той ще се рендира: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -В случай на CheckboxList и RadioList може да се повлияе и на шаблона на разделителя на отделните елементи, който се връща от метода `getSeparatorPrototype()`. В състояние по подразбиране това е елементът `<br>`. Ако го промените на двоен елемент, той ще обвива отделните елементи, вместо да ги разделя. Освен това може да се повлияе на шаблона на HTML елемента на етикета при отделните елементи, който се връща от `getItemLabelPrototype()`. - - -Превод -====== - -Ако програмирате многоезично приложение, вероятно ще трябва да рендирате формата в различни езикови версии. За тази цел Nette Framework дефинира интерфейс за превод [api:Nette\Localization\Translator]. В Nette няма имплементация по подразбиране, можете да избирате според нуждите си от няколко готови решения, които ще намерите на [Componette |https://componette.org/search/localization]. В тяхната документация ще научите как да конфигурирате преводача. - -Формите поддържат извеждане на текстове чрез преводач. Предаваме им го с помощта на метода `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -От този момент нататък не само всички етикети, но и всички съобщения за грешки или елементи на select box-ове ще бъдат преведени на друг език. - -При отделните елементи на формата е възможно да се зададе друг преводач или преводът да се изключи напълно със стойност `null`: - -```php -$form->addSelect('carModel', 'Модел:', $cars) - ->setTranslator(null); -``` - -При [правилата за валидация|validation] на преводача се предават и специфични параметри, например при правилото: - -```php -$form->addPassword('password', 'Парола:') - ->addRule($form::MinLength, 'Паролата трябва да съдържа поне %d знака', 8); -``` - -се извиква преводачът с тези параметри: - -```php -$translator->translate('Паролата трябва да съдържа поне %d знака', 8); -``` - -и следователно може да избере правилната форма за множествено число на думата `знака` според броя. - - -Събитие onRender -================ - -Точно преди формата да се рендира, можем да извикаме наш код. Той може например да добави HTML класове към елементите на формата за правилно показване. Добавяме кода към масива `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/bg/standalone.texy b/forms/bg/standalone.texy deleted file mode 100644 index 6a4e89a8a9..0000000000 --- a/forms/bg/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Форми, използвани самостоятелно -******************************* - -.[perex] -Nette Forms значително улесняват създаването и обработката на уеб форми. Можете да ги използвате във вашите приложения напълно самостоятелно, без останалата част от framework-а, което ще покажем в тази глава. - -Но ако използвате Nette Application и презентери, за вас е предназначено ръководството за [използване в презентери|in-presenter]. - - -Първа форма -=========== - -Нека опитаме да напишем проста форма за регистрация. Кодът й ще бъде следният ("целия код":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Име:'); -$form->addPassword('password', 'Парола:'); -$form->addSubmit('send', 'Регистриране'); -``` - -Много лесно можем да я рендираме: - -```php -$form->render(); -``` - -и в браузъра ще се покаже така: - -[* form-cs.webp *] - -Формата е обект от класа `Nette\Forms\Form` (класът `Nette\Application\UI\Form` се използва в презентери). Добавихме към нея т.нар. елементи име, парола и бутон за изпращане. - -А сега да оживим формата. С проверка на `$form->isSuccess()` ще разберем дали формата е била изпратена и дали е била попълнена валидно. Ако да, ще изведем данните. Следователно, след дефиницията на формата добавяме: - -```php -if ($form->isSuccess()) { - echo 'Формата беше правилно попълнена и изпратена'; - $data = $form->getValues(); - // $data->name съдържа името - // $data->password съдържа паролата - var_dump($data); -} -``` - -Методът `getValues()` връща изпратените данни под формата на обект [ArrayHash |utils:arrays#ArrayHash]. Как да променим това, ще покажем [по-късно |#Мапиране към класове]. Обектът `$data` съдържа ключове `name` и `password` с данните, които е попълнил потребителят. - -Обикновено данните веднага се изпращат за по-нататъшна обработка, което може да бъде например вмъкване в база данни. По време на обработката обаче може да възникне грешка, например потребителското име вече е заето. В такъв случай предаваме грешката обратно към формата с помощта на `addError()` и я оставяме да се рендира отново, заедно със съобщението за грешка. - -```php -$form->addError('Извиняваме се, това потребителско име вече се използва.'); -``` - -След обработката на формата пренасочваме към следващата страница. Това предотвратява нежеланото повторно изпращане на формата с бутона *обнови*, *назад* или чрез движение в историята на браузъра. - -Формата стандартно се изпраща с метод POST и то към същата страница. И двете могат да се променят: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -И това всъщност е всичко :-) Имаме функционална и перфектно [защитена |#Защита от уязвимости] форма. - -Опитайте да добавите и други [елементи на формата|controls]. - - -Достъп до елементи -================== - -Формата и нейните отделни елементи наричаме компоненти. Те образуват дърво от компоненти, където коренът е именно формата. До отделните елементи на формата можем да достигнем по следния начин: - -```php -$input = $form->getComponent('name'); -// алтернативен синтаксис: $input = $form['name']; - -$button = $form->getComponent('send'); -// алтернативен синтаксис: $button = $form['send']; -``` - -Елементите се премахват с помощта на unset: - -```php -unset($form['name']); -``` - - -Правила за валидация -==================== - -Споменахме думата *валидна*, но формата засега няма никакви правила за валидация. Нека поправим това. - -Името ще бъде задължително, затова го маркираме с метода `setRequired()`, чийто аргумент е текстът на съобщението за грешка, което ще се покаже, ако потребителят не попълни името. Ако не посочим аргумент, ще се използва съобщението за грешка по подразбиране. - -```php -$form->addText('name', 'Име:') - ->setRequired('Моля, въведете име'); -``` - -Опитайте да изпратите формата без попълнено име и ще видите, че ще се покаже съобщение за грешка и браузърът или сървърът ще я отхвърлят, докато не попълните полето. - -Същевременно системата няма да ви измами, като напишете в полето например само интервали. Не. Nette автоматично премахва левите и десните интервали. Опитайте. Това е нещо, което винаги трябва да правите с всеки едноредов input, но често се забравя. Nette го прави автоматично. (Можете да опитате да измамите формата и да изпратите многоредов низ като име. И тук Nette няма да се обърка и ще промени новите редове на интервали.) - -Формата винаги се валидира от страна на сървъра, но също така се генерира JavaScript валидация, която протича мигновено и потребителят научава за грешката веднага, без да е необходимо да изпраща формата на сървъра. За това се грижи скриптът `netteForms.js`. Вмъкнете го в страницата: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ако погледнете в изходния код на страницата с формата, можете да забележите, че Nette вмъква задължителните елементи в елементи с CSS клас `required`. Опитайте да добавите следния стил в шаблона и надписът „Име“ ще бъде червен. Така елегантно маркираме задължителните елементи за потребителите: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Други правила за валидация добавяме с метода `addRule()`. Първият параметър е правилото, вторият е отново текстът на съобщението за грешка и може да последва още аргумент на правилото за валидация. Какво се има предвид? - -Ще разширим формата с ново незадължително поле „възраст“, което трябва да бъде цяло число (`addInteger()`) и освен това в разрешен диапазон (`$form::Range`). И тук именно ще използваме третия параметър на метода `addRule()`, с който ще предадем на валидатора изисквания диапазон като двойка `[от, до]`: - -```php -$form->addInteger('age', 'Възраст:') - ->addRule($form::Range, 'Възрастта трябва да е между 18 и 120', [18, 120]); -``` - -.[tip] -Ако потребителят не попълни полето, правилата за валидация няма да се проверяват, тъй като елементът е незадължителен. - -Тук възниква възможност за дребен рефакторинг. В съобщението за грешка и в третия параметър числата са посочени дублирано, което не е идеално. Ако създавахме [многоезични форми |rendering#Превод] и съобщението, съдържащо числа, беше преведено на няколко езика, евентуалната промяна на стойностите би се затруднила. Поради тази причина е възможно да се използват заместващи знаци `%d` и Nette ще допълни стойностите: - -```php - ->addRule($form::Range, 'Възрастта трябва да е между %d и %d години', [18, 120]); -``` - -Да се върнем към елемента `password`, който също ще направим задължителен и ще проверим минималната дължина на паролата (`$form::MinLength`), отново с използване на заместващ знак: - -```php -$form->addPassword('password', 'Парола:') - ->setRequired('Изберете парола') - ->addRule($form::MinLength, 'Паролата трябва да има поне %d знака', 8); -``` - -Ще добавим към формата още поле `passwordVerify`, където потребителят ще въведе паролата още веднъж, за проверка. С помощта на правилата за валидация ще проверим дали двете пароли са еднакви (`$form::Equal`). И като параметър ще дадем препратка към първата парола с помощта на [квадратни скоби |#Достъп до елементи]: - -```php -$form->addPassword('passwordVerify', 'Парола за проверка:') - ->setRequired('Моля, въведете паролата отново за проверка') - ->addRule($form::Equal, 'Паролите не съвпадат', $form['password']) - ->setOmitted(); -``` - -С помощта на `setOmitted()` маркирахме елемент, чиято стойност всъщност не ни интересува и който съществува само поради валидация. Стойността не се предава в `$data`. - -С това имаме готова напълно функционална форма с валидация в PHP и JavaScript. Валидационните способности на Nette са далеч по-широки, могат да се създават условия, според тях да се показват и скриват части от страницата и т.н. Всичко ще научите в главата за [валидация на форми|validation]. - - -Стойности по подразбиране -========================= - -На елементите на формата обикновено задаваме стойности по подразбиране: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Често е полезно да се зададат стойности по подразбиране на всички елементи едновременно. Например, когато формата служи за редактиране на записи. Прочитаме записа от базата данни и задаваме стойностите по подразбиране: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Извиквайте `setDefaults()` след дефинирането на елементите. - - -Рендиране на формата -==================== - -Стандартно формата се рендира като таблица. Отделните елементи отговарят на основното правило за достъпност - всички надписи са записани като `<label>` и са свързани със съответния елемент на формата. При кликване върху надписа курсорът автоматично се появява в полето на формата. - -На всеки елемент можем да задаваме произволни HTML атрибути. Например да добавим placeholder: - -```php -$form->addInteger('age', 'Възраст:') - ->setHtmlAttribute('placeholder', 'Моля, попълнете възрастта'); -``` - -Начините за рендиране на форма са наистина много, затова на това е посветена [самостоятелна глава за рендиране|rendering]. - - -Мапиране към класове -==================== - -Да се върнем към обработката на данните от формата. Методът `getValues()` ни връщаше изпратените данни като обект `ArrayHash`. Тъй като това е генеричен клас, нещо като `stdClass`, при работа с него ще ни липсва определен комфорт, като например подсказване на свойствата в редакторите или статичен анализ на кода. Това би могло да се реши, като за всяка форма имаме конкретен клас, чиито свойства представляват отделните елементи. Напр.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Алтернативно можете да използвате конструктор: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Свойствата на класа с данни могат да бъдат и enum-и и те ще бъдат автоматично мапирани. .{data-version:3.2.4} - -Как да кажем на Nette да ни връща данните като обекти от този клас? По-лесно, отколкото си мислите. Достатъчно е само името на класа или обектът за хидратиране да се посочи като параметър: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Като параметър може да се посочи също `'array'` и тогава данните ще се върнат като масив. - -Ако формите образуват многостепенна структура, съставена от контейнери, създайте за всеки отделен клас: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Мапирането след това от типа на свойството `$person` ще разбере, че трябва да мапира контейнера към класа `PersonFormData`. Ако свойството съдържа масив от контейнери, посочете тип `array` и предайте класа за мапиране директно на контейнера: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Можете да генерирате дизайна на класа с данни на формата с помощта на метода `Nette\Forms\Blueprint::dataClass($form)`, който го извежда на страницата на браузъра. След това е достатъчно да маркирате кода с кликване и да го копирате в проекта. .{data-version:3.1.15} - - -Повече бутони -============= - -Ако формата има повече от един бутон, обикновено трябва да разграничим кой от тях е бил натиснат. Тази информация ни връща методът `isSubmittedBy()` на бутона: - -```php -$form->addSubmit('save', 'Запазване'); -$form->addSubmit('delete', 'Изтриване'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Не пропускайте проверката `$form->isSuccess()`, с нея ще проверите валидността на данните. - -Когато формата се изпрати с бутона <kbd>Enter</kbd>, се счита, че е изпратена с първия бутон. - - -Защита от уязвимости -==================== - -Nette Framework поставя голям акцент върху сигурността и затова стриктно се грижи за доброто обезопасяване на формите. - -Освен че формите защитават от атаки [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] и [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], той прави много дребни защити, за които вие вече не трябва да мислите. - -Например, филтрира от входовете всички контролни знаци и проверява валидността на UTF-8 кодирането, така че данните от формата винаги ще бъдат чисти. При select кутиите и radio списъците проверява дали избраните елементи са били действително от предлаганите и дали не е имало подправяне. Вече споменахме, че при едноредовите текстови входове премахва знаците за край на ред, които нападателят е могъл да изпрати. При многоредовите входове пък нормализира знаците за край на ред. И така нататък. - -Nette решава вместо вас рисковете за сигурността, за които много програмисти дори не подозират, че съществуват. - -Споменатата CSRF атака се състои в това, че нападателят примамва жертвата на страница, която незабележимо в браузъра на жертвата изпълнява заявка към сървъра, на който жертвата е влязла, и сървърът смята, че заявката е била изпълнена от жертвата по нейна воля. Затова Nette предотвратява изпращането на POST форма от друг домейн. Ако по някаква причина искате да изключите защитата и да позволите изпращането на формата от друг домейн, използвайте: - -```php -$form->allowCrossOrigin(); // ВНИМАНИЕ! Изключва защитата! -``` - -Тази защита използва SameSite бисквитка с име `_nss`. Затова създавайте обекта на формата преди изпращането на първия изход, за да може бисквитката да бъде изпратена. - -Защитата с помощта на SameSite бисквитка може да не е 100% надеждна, затова е препоръчително да включите и защита с помощта на токен: - -```php -$form->addProtection(); -``` - -Препоръчваме да защитавате по този начин формите в административната част на сайта, които променят чувствителни данни в приложението. Framework-ът се защитава срещу CSRF атака чрез генериране и проверка на оторизационен токен, който се съхранява в сесията. Затова е необходимо преди показването на формата да има отворена сесия. В административната част на сайта обикновено сесията вече е стартирана поради влизането на потребителя. В противен случай стартирайте сесията с метода `Nette\Http\Session::start()`. - -Така, преминахме през бързо въведение във формите в Nette. Опитайте да разгледате още директорията [examples|https://github.com/nette/forms/tree/master/examples] в дистрибуцията, където ще намерите повече вдъхновение. diff --git a/forms/bg/validation.texy b/forms/bg/validation.texy deleted file mode 100644 index 2342c274de..0000000000 --- a/forms/bg/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Валидация на форми -****************** - - -Задължителни елементи -===================== - -Задължителните елементи маркираме с метода `setRequired()`, чийто аргумент е текстът на [#Съобщения за грешки], който ще се покаже, ако потребителят не попълни елемента. Ако не посочим аргумент, ще се използва съобщението за грешка по подразбиране. - -```php -$form->addText('name', 'Име:') - ->setRequired('Моля, въведете име'); -``` - - -Правила -======= - -Правилата за валидация добавяме към елементите с метода `addRule()`. Първият параметър е правилото, вторият е текстът на [#Съобщения за грешки] и третият е аргументът на правилото за валидация. - -```php -$form->addPassword('password', 'Парола:') - ->addRule($form::MinLength, 'Паролата трябва да има поне %d знака', 8); -``` - -**Правилата за валидация се проверяват само в случай, че потребителят е попълнил елемента.** - -Nette идва с цяла редица предварително дефинирани правила, чиито имена са константи на класа `Nette\Forms\Form`. При всички елементи можем да използваме тези правила: - -| константа | описание | тип аргумент -|------- -| `Required` | задължителен елемент, псевдоним за `setRequired()` | - -| `Filled` | задължителен елемент, псевдоним за `setRequired()` | - -| `Blank` | елементът не трябва да бъде попълнен | - -| `Equal` | стойността е равна на параметъра | `mixed` -| `NotEqual` | стойността не е равна на параметъра | `mixed` -| `IsIn` | стойността е равна на някой елемент в масива | `array` -| `IsNotIn` | стойността не е равна на никой елемент в масива | `array` -| `Valid` | елементът попълнен ли е правилно? (за [#Условия]) | - - - -Текстови полета ---------------- - -При елементите `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` могат да се използват и някои от следните правила: - -| `MinLength` | минимална дължина на текста | `int` -| `MaxLength` | максимална дължина на текста | `int` -| `Length` | дължина в диапазон или точна дължина | двойка `[int, int]` или `int` -| `Email` | валиден имейл адрес | - -| `URL` | абсолютен URL | - -| `Pattern` | съответства на регулярен израз | `string` -| `PatternInsensitive` | като `Pattern`, но независимо от големината на буквите | `string` -| `Integer` | целочислена стойност | - -| `Numeric` | псевдоним за `Integer` | - -| `Float` | число | - -| `Min` | минимална стойност на числов елемент | `int\|float` -| `Max` | максимална стойност на числов елемент | `int\|float` -| `Range` | стойност в диапазон | двойка `[int\|float, int\|float]` - -Правилата за валидация `Integer`, `Numeric` и `Float` веднага преобразуват стойността в integer съответно float. Освен това правилото `URL` приема и адрес без схема (напр. `nette.org`) и допълва схемата (`https://nette.org`). Изразът в `Pattern` и `PatternIcase` трябва да важи за цялата стойност, т.е. сякаш е обгърнат със знаците `^` и `$`. - - -Брой елементи -------------- - -При елементите `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` могат да се използват и следните правила за ограничаване на броя на избраните елементи съответно качените файлове: - -| `MinLength` | минимален брой | `int` -| `MaxLength` | максимален брой | `int` -| `Length` | брой в диапазон или точен брой | двойка `[int, int]` или `int` - - -Качване на файлове ------------------- - -При елементите `addUpload()`, `addMultiUpload()` могат да се използват и следните правила: - -| `MaxFileSize` | максимален размер на файла в байтове | `int` -| `MimeType` | MIME тип, разрешени са заместващи знаци (`'video/*'`) | `string\|string[]` -| `Image` | изображение JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | името на файла съответства на регулярен израз | `string` -| `PatternInsensitive` | като `Pattern`, но независимо от големината на буквите | `string` - -`MimeType` и `Image` изискват PHP разширението `fileinfo`. Дали файлът или изображението е от изисквания тип се открива въз основа на неговата сигнатура и **не се проверява целостта на целия файл.** Дали изображението не е повредено може да се установи например чрез опит за неговото [зареждане |http:request#toImage]. - - -Съобщения за грешки -=================== - -Всички предварително дефинирани правила с изключение на `Pattern` и `PatternInsensitive` имат съобщение за грешка по подразбиране, така че то може да бъде пропуснато. Въпреки това, като посочите и формулирате всички съобщения по мярка, ще направите формата по-удобна за потребителя. - -Можете да промените съобщенията по подразбиране в [конфигурацията|forms:configuration], като редактирате текстовете в масива `Nette\Forms\Validator::$messages` или като използвате [преводач |rendering#Превод]. - -В текста на съобщенията за грешки могат да се използват следните заместващи низове: - -| `%d` | заменя последователно с аргументите на правилото -| `%n$d` | заменя с n-тия аргумент на правилото -| `%label` | заменя с надписа на елемента (без двоеточие) -| `%name` | заменя с името на елемента (напр. `name`) -| `%value` | заменя с въведената от потребителя стойност - -```php -$form->addText('name', 'Име:') - ->setRequired('Моля, попълнете %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'поне %d и най-много %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'най-много %2$d и поне %1$d', [5, 10]); -``` - - -Условия -======= - -Освен правила могат да се добавят и условия. Те се записват подобно на правилата, само че вместо `addRule()` използваме метода `addCondition()` и разбира се не посочваме никакво съобщение за грешка (условието само пита): - -```php -$form->addPassword('password', 'Парола:') - // ако паролата не е по-дълга от 8 знака - ->addCondition($form::MaxLength, 8) - // тогава трябва да съдържа цифра - ->addRule($form::Pattern, 'Трябва да съдържа цифра', '.*[0-9].*'); -``` - -Условието може да бъде обвързано и с друг елемент освен текущия с помощта на `addConditionOn()`. Като първи параметър посочваме референция към елемента. В този пример имейлът ще бъде задължителен само тогава, когато се отметне чекбоксът (неговата стойност ще бъде true): - -```php -$form->addCheckbox('newsletters', 'изпращайте ми бюлетини'); - -$form->addEmail('email', 'Имейл:') - // ако чекбоксът е отметнат - ->addConditionOn($form['newsletters'], $form::Equal, true) - // тогава изисквай имейл - ->setRequired('Въведете имейл адрес'); -``` - -От условията могат да се създават комплексни структури с помощта на `elseCondition()` и `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // ако е изпълнено първото условие - ->addConditionOn(/* ... */) // и второто условие на друг елемент - ->addRule(/* ... */) // изисквай това правило - ->elseCondition() // ако второто условие не е изпълнено - ->addRule(/* ... */) // изисквай тези правила - ->addRule(/* ... */) - ->endCondition() // връщаме се към първото условие - ->addRule(/* ... */); -``` - -В Nette може много лесно да се реагира на изпълнението или неизпълнението на условие и от страна на JavaScript с помощта на метода `toggle()`, виж [#Динамичен JavaScript]. - - -Референция към друг елемент -=========================== - -Като аргумент на правило или условие може да се предаде и друг елемент на формата. Правилото тогава ще използва стойността, въведена по-късно от потребителя в браузъра. Така може например динамично да се валидира, че елементът `password` съдържа същия низ като елемента `password_confirm`: - -```php -$form->addPassword('password', 'Парола'); -$form->addPassword('password_confirm', 'Потвърдете паролата') - ->addRule($form::Equal, 'Въведените пароли не съвпадат', $form['password']); -``` - - -Персонализирани правила и условия -================================= - -Понякога се озоваваме в ситуация, когато вградените правила за валидация в Nette не са достатъчни и трябва да валидираме данните от потребителя по свой начин. В Nette това е много лесно! - -На методите `addRule()` или `addCondition()` може да се предаде като първи параметър произволен callback. Той приема като първи параметър самия елемент и връща булева стойност, определяща дали валидацията е преминала успешно. При добавяне на правило с `addRule()` е възможно да се зададат и други аргументи, те след това се предават като втори параметър. - -Така можем да създадем собствен набор от валидатори като клас със статични методи: - -```php -class MyValidators -{ - // проверява дали стойността се дели на аргумента - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // други валидатори - } -} -``` - -Използването след това е много лесно: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Стойността трябва да е кратна на %d', - 8, - ); -``` - -Персонализирани правила за валидация могат да се добавят и в JavaScript. Условието е правилото да бъде статичен метод. Неговото име за JavaScript валидатора се образува чрез свързване на името на класа без обратни наклонени черти `\`, долна черта `_` и името на метода. Напр. `App\MyValidators::validateDivisibility` записваме като `AppMyValidators_validateDivisibility` и добавяме към обекта `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Събитие onValidate -================== - -След изпращане на формата се извършва валидация, при която се проверяват отделните правила, добавени с `addRule()`, и след това се извиква [събитие |nette:glossary#Събития events] `onValidate`. Неговият handler може да се използва за допълнителна валидация, типично проверка на правилната комбинация от стойности в няколко елемента на формата. - -Ако се открие грешка, я предаваме на формата с метода `addError()`. Той може да се извика или на конкретен елемент, или директно на формата. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Тази комбинация не е възможна.'); - } -} -``` - - -Грешки при обработка -==================== - -В много случаи научаваме за грешката едва когато обработваме валидната форма, например записваме нов елемент в базата данни и се натъкваме на дублиране на ключове. В такъв случай отново предаваме грешката на формата с метода `addError()`. Той може да се извика или на конкретен елемент, или директно на формата: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Невалидна парола.'); - } -} -``` - -Ако е възможно, препоръчваме да прикачите грешката директно към елемента на формата, тъй като тя ще се покаже до него при използване на рендеръра по подразбиране. - -```php -$form['date']->addError('Извиняваме се, но тази дата вече е заета.'); -``` - -Можете да извиквате `addError()` многократно и така да предавате на формата или елемента повече съобщения за грешки. Получавате ги с `getErrors()`. - -Внимание, `$form->getErrors()` връща резюме на всички съобщения за грешки, включително тези, които са били предадени директно на отделни елементи, не само директно на формата. Съобщенията за грешки, предадени само на формата, получавате чрез `$form->getOwnErrors()`. - - -Модификация на входа -==================== - -С помощта на метода `addFilter()` можем да променим въведената от потребителя стойност. В този пример ще толерираме и премахваме интервали в пощенския код: - -```php -$form->addText('zip', 'Пощенски код:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // премахваме интервалите от пощенския код - }) - ->addRule($form::Pattern, 'Пощенският код не е във формат от пет цифри', '\d{5}'); -``` - -Филтърът се интегрира между правилата за валидация и условията и следователно редът на методите има значение, т.е. филтърът и правилото се извикват в такъв ред, какъвто е редът на методите `addFilter()` и `addRule()`. - - -JavaScript валидация -==================== - -Езикът за формулиране на условия и правила е много мощен. Всички конструкции при това работят както от страна на сървъра, така и от страна на JavaScript. Пренасят се в HTML атрибути `data-nette-rules` като JSON. Самата валидация след това се извършва от скрипт, който прихваща събитието `submit` на формата, преминава през отделните елементи и извършва съответната валидация. - -Този скрипт е `netteForms.js` и е достъпен от няколко възможни източника: - -Можете да вмъкнете скрипта директно в HTML страницата от CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Или да го копирате локално в публичната папка на проекта (напр. от `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Или да го инсталирате чрез [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -И след това да го заредите и стартирате: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Алтернативно можете да го заредите директно от папката `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -Динамичен JavaScript -==================== - -Искате да покажете полетата за въвеждане на адрес само ако потребителят избере стоката да бъде изпратена по пощата? Няма проблем. Ключът е двойката методи `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Този код казва, че когато условието е изпълнено, т.е. когато чекбоксът е отметнат, ще бъде видим HTML елементът `#address-container`. И обратното. Елементите на формата с адреса на получателя така поставяме в контейнер с това ID и при кликване върху чекбокса те се скриват или показват. Това осигурява скриптът `netteForms.js`. - -Като аргумент на метода `toggle()` може да се предаде произволен селектор. По исторически причини буквено-цифров низ без други специални знаци се разбира като ID на елемент, т.е. същото, като че ли му предхожда знакът `#`. Вторият незадължителен параметър позволява да се обърне поведението, т.е. ако използваме `toggle('#address-container', false)`, елементът ще се покаже обратно само тогава, ако чекбоксът не е отметнат. - -Имплементацията по подразбиране в JavaScript променя свойството `hidden` на елементите. Поведението обаче можем лесно да променим, например да добавим анимация. Достатъчно е в JavaScript да презапишем метода `Nette.toggle` със собствено решение: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // скриваме или показваме 'el' според стойността на 'visible' - }); -}; -``` - - -Изключване на валидацията -========================= - -Понякога може да се наложи да изключите валидацията. Ако натискането на бутона за изпращане не трябва да извършва валидация (подходящо за бутони *Cancel* или *Preview*), я изключваме с метода `$submit->setValidationScope([])`. Ако трябва да извършва само частична валидация, можем да определим кои полета или контейнери на формата трябва да се валидират. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Валидира цялата форма -$form->addSubmit('send2') - ->setValidationScope([]); // Не валидира изобщо -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Валидира само елемента name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Валидира само елемента age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Валидира контейнера details -``` - -`setValidationScope` не влияе на [#Събитие onValidate] на формата, която ще бъде извикана винаги. Събитието `onValidate` на контейнера ще бъде извикано само ако този контейнер е маркиран за частична валидация. diff --git a/forms/el/@home.texy b/forms/el/@home.texy deleted file mode 100644 index 1250a801a3..0000000000 --- a/forms/el/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Το Nette Forms έφερε επανάσταση στη δημιουργία φορμών web. Ξαφνικά, αρκούσε να γράψετε μερικές κατανοητές γραμμές κώδικα και είχατε έτοιμη μια φόρμα, συμπεριλαμβανομένης της απόδοσης, της επικύρωσης JavaScript και server-side, και επιπλέον εξαιρετικά ασφαλισμένη. Θα δείξουμε πώς - -- να δημιουργείτε φιλικές φόρμες -- να επικυρώνετε τα υποβληθέντα δεδομένα -- να αποδίδετε τα στοιχεία ακριβώς όπως χρειάζεται - -</div> - - -Χρησιμοποιώντας το Nette Forms αποφεύγετε μια ολόκληρη σειρά από ρουτίνες εργασίες, όπως η συγγραφή επικύρωσης (επιπλέον διπλής, από την πλευρά του server και του client), ελαχιστοποιείτε την πιθανότητα εμφάνισης σφαλμάτων και κενών ασφαλείας. - -Μπορείτε να χρησιμοποιήσετε τις φόρμες είτε ως μέρος της Εφαρμογής Nette (δηλαδή σε presenters), είτε εντελώς αυτόνομα. Επειδή και στις δύο περιπτώσεις η χρήση διαφέρει λίγο, ετοιμάσαμε για εσάς δύο οδηγούς: - -<div class="wiki-buttons"> -<div> "Φόρμες σε presenters .[wiki-button]":in-presenter </div> -<div> "Φόρμες αυτόνομα .[wiki-button]":standalone </div> -</div> - - -Εγκατάσταση ------------ - -Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας το εργαλείο [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/el/@left-menu.texy b/forms/el/@left-menu.texy deleted file mode 100644 index bbf4c1020e..0000000000 --- a/forms/el/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Εισαγωγή |@home] -- [Φόρμες σε presenters|in-presenter] -- [Φόρμες αυτόνομα|standalone] -- [Στοιχεία φόρμας |controls] -- [Επικύρωση |validation] -- [Απόδοση |rendering] -- [Διαμόρφωση |configuration] - - -Περαιτέρω ανάγνωση -****************** -- [Οδηγοί και διαδικασίες |best-practices:] diff --git a/forms/el/@meta.texy b/forms/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/forms/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/forms/el/configuration.texy b/forms/el/configuration.texy deleted file mode 100644 index 44fcbe1719..0000000000 --- a/forms/el/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Διαμόρφωση φορμών -***************** - -.[perex] -Στη διαμόρφωση, μπορείτε να αλλάξετε τα προεπιλεγμένα [μηνύματα σφάλματος φόρμας|validation]. - -```neon -forms: - messages: - 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.' - 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.' -``` - -Εδώ είναι η ελληνική μετάφραση: - -```neon -forms: - messages: - Equal: 'Παρακαλώ εισάγετε %s.' - NotEqual: 'Αυτή η τιμή δεν πρέπει να είναι %s.' - Filled: 'Αυτό το πεδίο είναι υποχρεωτικό.' - Blank: 'Αυτό το πεδίο πρέπει να είναι κενό.' - MinLength: 'Παρακαλώ εισάγετε τουλάχιστον %d χαρακτήρες.' - MaxLength: 'Παρακαλώ εισάγετε το πολύ %d χαρακτήρες.' - Length: 'Παρακαλώ εισάγετε μια τιμή μήκους μεταξύ %d και %d χαρακτήρων.' - Email: 'Παρακαλώ εισάγετε μια έγκυρη διεύθυνση email.' - URL: 'Παρακαλώ εισάγετε ένα έγκυρο URL.' - Integer: 'Παρακαλώ εισάγετε έναν έγκυρο ακέραιο αριθμό.' - Float: 'Παρακαλώ εισάγετε έναν έγκυρο αριθμό.' - Min: 'Παρακαλώ εισάγετε μια τιμή μεγαλύτερη ή ίση με %d.' - Max: 'Παρακαλώ εισάγετε μια τιμή μικρότερη ή ίση με %d.' - Range: 'Παρακαλώ εισάγετε μια τιμή μεταξύ %d και %d.' - MaxFileSize: 'Το μέγεθος του ανεβασμένου αρχείου μπορεί να είναι έως %d bytes.' - MaxPostSize: 'Τα ανεβασμένα δεδομένα υπερβαίνουν το όριο των %d bytes.' - MimeType: 'Το ανεβασμένο αρχείο δεν είναι στην αναμενόμενη μορφή.' - Image: 'Το ανεβασμένο αρχείο πρέπει να είναι εικόνα σε μορφή JPEG, GIF, PNG, WebP ή AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Παρακαλώ επιλέξτε μια έγκυρη επιλογή.' - Nette\Forms\Controls\UploadControl::Valid: 'Παρουσιάστηκε σφάλμα κατά τη μεταφόρτωση του αρχείου.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Η συνεδρία σας έχει λήξει. Παρακαλώ επιστρέψτε στην αρχική σελίδα και προσπαθήστε ξανά.' -``` - -Εάν δεν χρησιμοποιείτε ολόκληρο το framework και επομένως ούτε τα αρχεία διαμόρφωσης, μπορείτε να αλλάξετε τα προεπιλεγμένα μηνύματα σφάλματος απευθείας στον πίνακα `Nette\Forms\Validator::$messages`. diff --git a/forms/el/controls.texy b/forms/el/controls.texy deleted file mode 100644 index faa1fdb363..0000000000 --- a/forms/el/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Στοιχεία φόρμας -*************** - -.[perex] -Επισκόπηση των τυπικών στοιχείων φόρμας. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Προσθέτει ένα πεδίο κειμένου μίας γραμμής (κλάση [TextInput |api:Nette\Forms\Controls\TextInput]). Εάν ο χρήστης δεν συμπληρώσει το πεδίο, επιστρέφει ένα κενό string `''`, ή με τη χρήση του `setNullable()` μπορεί να οριστεί να επιστρέφει `null`. - -```php -$form->addText('name', 'Όνομα:') - ->setRequired() - ->setNullable(); -``` - -Επικυρώνει αυτόματα το UTF-8, αφαιρεί τα κενά στην αρχή και στο τέλος και αφαιρεί τις αλλαγές γραμμής που θα μπορούσε να στείλει ένας εισβολέας. - -Το μέγιστο μήκος μπορεί να περιοριστεί με τη χρήση του `setMaxLength()`. Η τροποποίηση της τιμής που εισήγαγε ο χρήστης είναι δυνατή με το [addFilter() |validation#Τροποποίηση Εισόδου]. - -Με τη χρήση του `setHtmlType()` μπορεί να αλλάξει ο οπτικός χαρακτήρας του πεδίου κειμένου σε τύπους όπως `search`, `tel` ή `url` δείτε τις [προδιαγραφές|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Να θυμάστε ότι η αλλαγή τύπου είναι μόνο οπτική και δεν αντικαθιστά τη λειτουργία επικύρωσης. Για τον τύπο `url` είναι σκόπιμο να προστεθεί ένας συγκεκριμένος [κανόνας επικύρωσης URL |validation#Είσοδοι Κειμένου]. - -.[note] -Για άλλους τύπους εισόδου, όπως `number`, `range`, `email`, `date`, `datetime-local`, `time` και `color`, χρησιμοποιήστε τις εξειδικευμένες μεθόδους όπως [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] και [#addColor], οι οποίες εξασφαλίζουν την επικύρωση από την πλευρά του διακομιστή. Οι τύποι `month` και `week` δεν υποστηρίζονται ακόμη πλήρως σε όλα τα προγράμματα περιήγησης. - -Στο στοιχείο μπορεί να οριστεί η λεγόμενη empty-value, η οποία είναι κάτι σαν προεπιλεγμένη τιμή, αλλά αν ο χρήστης δεν την αλλάξει, το στοιχείο επιστρέφει κενό string ή `null`. - -```php -$form->addText('phone', 'Τηλέφωνο:') - ->setHtmlType('tel') - ->setEmptyValue('+30'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Προσθέτει ένα πεδίο για την εισαγωγή κειμένου πολλαπλών γραμμών (κλάση [TextArea |api:Nette\Forms\Controls\TextArea]). Εάν ο χρήστης δεν συμπληρώσει το πεδίο, επιστρέφει ένα κενό string `''`, ή με τη χρήση του `setNullable()` μπορεί να οριστεί να επιστρέφει `null`. - -```php -$form->addTextArea('note', 'Σημείωση:') - ->addRule($form::MaxLength, 'Η σημείωση είναι πολύ μεγάλη', 10000); -``` - -Επικυρώνει αυτόματα το UTF-8 και κανονικοποιεί τους διαχωριστές γραμμών σε `\n`. Σε αντίθεση με το πεδίο εισόδου μίας γραμμής, δεν γίνεται καμία αφαίρεση κενών. - -Το μέγιστο μήκος μπορεί να περιοριστεί με τη χρήση του `setMaxLength()`. Η τροποποίηση της τιμής που εισήγαγε ο χρήστης είναι δυνατή με το [addFilter() |validation#Τροποποίηση Εισόδου]. Μπορεί να οριστεί η λεγόμενη empty-value με τη χρήση του `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Προσθέτει ένα πεδίο για την εισαγωγή ακέραιου αριθμού (κλάση [TextInput |api:Nette\Forms\Controls\TextInput]). Επιστρέφει είτε integer, είτε `null`, εάν ο χρήστης δεν εισάγει τίποτα. - -```php -$form->addInteger('year', 'Έτος:') - ->addRule($form::Range, 'Το έτος πρέπει να είναι στο εύρος από %d έως %d.', [1900, 2023]); -``` - -Το στοιχείο αποδίδεται ως `<input type="number">`. Με τη χρήση της μεθόδου `setHtmlType()` μπορεί να αλλάξει ο τύπος σε `range` για εμφάνιση με τη μορφή ολισθητήρα, ή σε `text`, εάν προτιμάτε ένα τυπικό πεδίο κειμένου χωρίς την ειδική συμπεριφορά του τύπου `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Προσθέτει ένα πεδίο για την εισαγωγή δεκαδικού αριθμού (κλάση [TextInput |api:Nette\Forms\Controls\TextInput]). Επιστρέφει είτε float, είτε `null`, εάν ο χρήστης δεν εισάγει τίποτα. - -```php -$form->addFloat('level', 'Επίπεδο:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Το επίπεδο πρέπει να είναι στο εύρος από %d έως %d.', [0, 100]); -``` - -Το στοιχείο αποδίδεται ως `<input type="number">`. Με τη χρήση της μεθόδου `setHtmlType()` μπορεί να αλλάξει ο τύπος σε `range` για εμφάνιση με τη μορφή ολισθητήρα, ή σε `text`, εάν προτιμάτε ένα τυπικό πεδίο κειμένου χωρίς την ειδική συμπεριφορά του τύπου `number`. - -Το Nette και ο περιηγητής Chrome αποδέχονται τόσο το κόμμα όσο και την τελεία ως διαχωριστικό δεκαδικών ψηφίων. Για να είναι διαθέσιμη αυτή η λειτουργικότητα και στον Firefox, συνιστάται να ορίσετε το χαρακτηριστικό `lang` είτε για το συγκεκριμένο στοιχείο είτε για ολόκληρη τη σελίδα, για παράδειγμα `<html lang="el">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Προσθέτει ένα πεδίο για την εισαγωγή διεύθυνσης email (κλάση [TextInput |api:Nette\Forms\Controls\TextInput]). Εάν ο χρήστης δεν συμπληρώσει το πεδίο, επιστρέφει ένα κενό string `''`, ή με τη χρήση του `setNullable()` μπορεί να οριστεί να επιστρέφει `null`. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Επαληθεύει εάν η τιμή είναι έγκυρη διεύθυνση email. Δεν επαληθεύεται εάν ο τομέας υπάρχει πραγματικά, επαληθεύεται μόνο η σύνταξη. Επικυρώνει αυτόματα το UTF-8, αφαιρεί τα κενά στην αρχή και στο τέλος. - -Το μέγιστο μήκος μπορεί να περιοριστεί με τη χρήση του `setMaxLength()`. Η τροποποίηση της τιμής που εισήγαγε ο χρήστης είναι δυνατή με το [addFilter() |validation#Τροποποίηση Εισόδου]. Μπορεί να οριστεί η λεγόμενη empty-value με τη χρήση του `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Προσθέτει ένα πεδίο για την εισαγωγή κωδικού πρόσβασης (κλάση [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - ->setRequired() - ->addRule($form::MinLength, 'Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον %d χαρακτήρες', 8) - ->addRule($form::Pattern, 'Πρέπει να περιέχει αριθμό', '.*[0-9].*'); -``` - -Κατά την εκ νέου εμφάνιση της φόρμας, το πεδίο θα είναι κενό. Επικυρώνει αυτόματα το UTF-8, αφαιρεί τα κενά στην αρχή και στο τέλος και αφαιρεί τις αλλαγές γραμμής που θα μπορούσε να στείλει ένας εισβολέας. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Προσθέτει ένα πλαίσιο ελέγχου (κλάση [Checkbox |api:Nette\Forms\Controls\Checkbox]). Επιστρέφει την τιμή είτε `true` είτε `false`, ανάλογα με το αν είναι επιλεγμένο. - -```php -$form->addCheckbox('agree', 'Συμφωνώ με τους όρους') - ->setRequired('Είναι απαραίτητο να συμφωνήσετε με τους όρους'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Προσθέτει πλαίσια ελέγχου για την επιλογή πολλαπλών στοιχείων (κλάση [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Επιστρέφει έναν πίνακα με τα κλειδιά των επιλεγμένων στοιχείων. Η μέθοδος `getSelectedItems()` επιστρέφει τις τιμές αντί για τα κλειδιά. - -```php -$form->addCheckboxList('colors', 'Χρώματα:', [ - 'r' => 'κόκκινο', - 'g' => 'πράσινο', - 'b' => 'μπλε', -]); -``` - -Τον πίνακα των προσφερόμενων στοιχείων τον παραδίδουμε ως τρίτη παράμετρο ή με τη μέθοδο `setItems()`. - -Με τη χρήση του `setDisabled(['r', 'g'])` μπορούν να απενεργοποιηθούν μεμονωμένα στοιχεία. - -Το στοιχείο ελέγχει αυτόματα ότι δεν έχει γίνει πλαστογράφηση και ότι τα επιλεγμένα στοιχεία είναι πράγματι ένα από τα προσφερόμενα και δεν έχουν απενεργοποιηθεί. Με τη μέθοδο `getRawValue()` μπορούν να ληφθούν τα υποβληθέντα στοιχεία χωρίς αυτόν τον σημαντικό έλεγχο. - -Κατά τον ορισμό των προεπιλεγμένων επιλεγμένων στοιχείων, ελέγχει επίσης ότι είναι ένα από τα προσφερόμενα, διαφορετικά προκαλεί εξαίρεση. Αυτός ο έλεγχος μπορεί να απενεργοποιηθεί με τη χρήση του `checkDefaultValue(false)`. - -Εάν υποβάλλετε τη φόρμα με τη μέθοδο `GET`, μπορείτε να επιλέξετε έναν πιο συμπαγή τρόπο μετάδοσης δεδομένων, ο οποίος εξοικονομεί μέγεθος στο query string. Ενεργοποιείται ορίζοντας το HTML attribute της φόρμας: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Προσθέτει κουμπιά επιλογής (κλάση [RadioList |api:Nette\Forms\Controls\RadioList]). Επιστρέφει το κλειδί του επιλεγμένου στοιχείου, ή `null`, εάν ο χρήστης δεν επέλεξε τίποτα. Η μέθοδος `getSelectedItem()` επιστρέφει την τιμή αντί για το κλειδί. - -```php -$sex = [ - 'm' => 'άνδρας', - 'f' => 'γυναίκα', -]; -$form->addRadioList('gender', 'Φύλο:', $sex); -``` - -Τον πίνακα των προσφερόμενων στοιχείων τον παραδίδουμε ως τρίτη παράμετρο ή με τη μέθοδο `setItems()`. - -Με τη χρήση του `setDisabled(['m', 'f'])` μπορούν να απενεργοποιηθούν μεμονωμένα στοιχεία. - -Το στοιχείο ελέγχει αυτόματα ότι δεν έχει γίνει πλαστογράφηση και ότι το επιλεγμένο στοιχείο είναι πράγματι ένα από τα προσφερόμενα και δεν έχει απενεργοποιηθεί. Με τη μέθοδο `getRawValue()` μπορεί να ληφθεί το υποβληθέν στοιχείο χωρίς αυτόν τον σημαντικό έλεγχο. - -Κατά τον ορισμό του προεπιλεγμένου επιλεγμένου στοιχείου, ελέγχει επίσης ότι είναι ένα από τα προσφερόμενα, διαφορετικά προκαλεί εξαίρεση. Αυτός ο έλεγχος μπορεί να απενεργοποιηθεί με τη χρήση του `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Προσθέτει ένα select box (κλάση [SelectBox |api:Nette\Forms\Controls\SelectBox]). Επιστρέφει το κλειδί του επιλεγμένου στοιχείου, ή `null`, εάν ο χρήστης δεν επέλεξε τίποτα. Η μέθοδος `getSelectedItem()` επιστρέφει την τιμή αντί για το κλειδί. - -```php -$countries = [ - 'CZ' => 'Τσεχία', - 'SK' => 'Σλοβακία', - 'GR' => 'Ελλάδα', -]; - -$form->addSelect('country', 'Χώρα:', $countries) - ->setDefaultValue('GR'); -``` - -Τον πίνακα των προσφερόμενων στοιχείων τον παραδίδουμε ως τρίτη παράμετρο ή με τη μέθοδο `setItems()`. Τα στοιχεία μπορούν να είναι και δισδιάστατος πίνακας: - -```php -$countries = [ - 'Europe' => [ - 'CZ' => 'Τσεχία', - 'SK' => 'Σλοβακία', - 'GR' => 'Ελλάδα', - ], - 'CA' => 'Καναδάς', - 'US' => 'ΗΠΑ', - '?' => 'άλλη', -]; -``` - -Στα select boxes, συχνά το πρώτο στοιχείο έχει ειδική σημασία, χρησιμεύει ως προτροπή για δράση. Για την προσθήκη ενός τέτοιου στοιχείου χρησιμοποιείται η μέθοδος `setPrompt()`. - -```php -$form->addSelect('country', 'Χώρα:', $countries) - ->setPrompt('Επιλέξτε χώρα'); -``` - -Με τη χρήση του `setDisabled(['CZ', 'SK'])` μπορούν να απενεργοποιηθούν μεμονωμένα στοιχεία. - -Το στοιχείο ελέγχει αυτόματα ότι δεν έχει γίνει πλαστογράφηση και ότι το επιλεγμένο στοιχείο είναι πράγματι ένα από τα προσφερόμενα και δεν έχει απενεργοποιηθεί. Με τη μέθοδο `getRawValue()` μπορεί να ληφθεί το υποβληθέν στοιχείο χωρίς αυτόν τον σημαντικό έλεγχο. - -Κατά τον ορισμό του προεπιλεγμένου επιλεγμένου στοιχείου, ελέγχει επίσης ότι είναι ένα από τα προσφερόμενα, διαφορετικά προκαλεί εξαίρεση. Αυτός ο έλεγχος μπορεί να απενεργοποιηθεί με τη χρήση του `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Προσθέτει ένα select box για την επιλογή πολλαπλών στοιχείων (κλάση [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Επιστρέφει έναν πίνακα με τα κλειδιά των επιλεγμένων στοιχείων. Η μέθοδος `getSelectedItems()` επιστρέφει τις τιμές αντί για τα κλειδιά. - -```php -$form->addMultiSelect('countries', 'Χώρες:', $countries); -``` - -Τον πίνακα των προσφερόμενων στοιχείων τον παραδίδουμε ως τρίτη παράμετρο ή με τη μέθοδο `setItems()`. Τα στοιχεία μπορούν να είναι και δισδιάστατος πίνακας. - -Με τη χρήση του `setDisabled(['CZ', 'SK'])` μπορούν να απενεργοποιηθούν μεμονωμένα στοιχεία. - -Το στοιχείο ελέγχει αυτόματα ότι δεν έχει γίνει πλαστογράφηση και ότι τα επιλεγμένα στοιχεία είναι πράγματι ένα από τα προσφερόμενα και δεν έχουν απενεργοποιηθεί. Με τη μέθοδο `getRawValue()` μπορούν να ληφθούν τα υποβληθέντα στοιχεία χωρίς αυτόν τον σημαντικό έλεγχο. - -Κατά τον ορισμό των προεπιλεγμένων επιλεγμένων στοιχείων, ελέγχει επίσης ότι είναι ένα από τα προσφερόμενα, διαφορετικά προκαλεί εξαίρεση. Αυτός ο έλεγχος μπορεί να απενεργοποιηθεί με τη χρήση του `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Προσθέτει ένα πεδίο για τη μεταφόρτωση αρχείου (κλάση [UploadControl |api:Nette\Forms\Controls\UploadControl]). Επιστρέφει ένα αντικείμενο [FileUpload |http:request#FileUpload] ακόμα και στην περίπτωση που ο χρήστης δεν υπέβαλε κανένα αρχείο, κάτι που μπορεί να διαπιστωθεί με τη μέθοδο `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Το Avatar πρέπει να είναι JPEG, PNG, GIF, WebP ή AVIF.') - ->addRule($form::MaxFileSize, 'Το μέγιστο μέγεθος είναι 1 MB.', 1024 * 1024); -``` - -Εάν το αρχείο δεν μεταφορτωθεί σωστά, η φόρμα δεν υποβάλλεται επιτυχώς και εμφανίζεται σφάλμα. Δηλαδή, κατά την επιτυχή υποβολή δεν χρειάζεται να επαληθεύσετε τη μέθοδο `FileUpload::isOk()`. - -Ποτέ μην εμπιστεύεστε το αρχικό όνομα του αρχείου που επιστρέφεται από τη μέθοδο `FileUpload::getName()`, ο πελάτης θα μπορούσε να έχει στείλει ένα κακόβουλο όνομα αρχείου με σκοπό να βλάψει ή να χακάρει την εφαρμογή σας. - -Οι κανόνες `MimeType` και `Image` ανιχνεύουν τον απαιτούμενο τύπο βάσει της υπογραφής του αρχείου και δεν επαληθεύουν την ακεραιότητά του. Το αν μια εικόνα είναι κατεστραμμένη μπορεί να διαπιστωθεί, για παράδειγμα, προσπαθώντας να τη [φορτώσετε |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Προσθέτει ένα πεδίο για τη μεταφόρτωση πολλαπλών αρχείων ταυτόχρονα (κλάση [UploadControl |api:Nette\Forms\Controls\UploadControl]). Επιστρέφει έναν πίνακα αντικειμένων [FileUpload |http:request#FileUpload]. Η μέθοδος `FileUpload::hasFile()` σε καθένα από αυτά θα επιστρέφει `true`. - -```php -$form->addMultiUpload('files', 'Αρχεία:') - ->addRule($form::MaxLength, 'Μπορούν να μεταφορτωθούν το πολύ %d αρχεία', 10); -``` - -Εάν κάποιο αρχείο δεν μεταφορτωθεί σωστά, η φόρμα δεν υποβάλλεται επιτυχώς και εμφανίζεται σφάλμα. Δηλαδή, κατά την επιτυχή υποβολή δεν χρειάζεται να επαληθεύσετε τη μέθοδο `FileUpload::isOk()`. - -Ποτέ μην εμπιστεύεστε τα αρχικά ονόματα των αρχείων που επιστρέφονται από τη μέθοδο `FileUpload::getName()`, ο πελάτης θα μπορούσε να έχει στείλει ένα κακόβουλο όνομα αρχείου με σκοπό να βλάψει ή να χακάρει την εφαρμογή σας. - -Οι κανόνες `MimeType` και `Image` ανιχνεύουν τον απαιτούμενο τύπο βάσει της υπογραφής του αρχείου και δεν επαληθεύουν την ακεραιότητά του. Το αν μια εικόνα είναι κατεστραμμένη μπορεί να διαπιστωθεί, για παράδειγμα, προσπαθώντας να τη [φορτώσετε |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Προσθέτει ένα πεδίο που επιτρέπει στον χρήστη να εισάγει εύκολα μια ημερομηνία που αποτελείται από έτος, μήνα και ημέρα (κλάση [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ως προεπιλεγμένη τιμή δέχεται είτε αντικείμενα που υλοποιούν το interface `DateTimeInterface`, ένα string με χρόνο, είτε έναν αριθμό που αντιπροσωπεύει UNIX timestamp. Το ίδιο ισχύει για τα ορίσματα των κανόνων `Min`, `Max` ή `Range`, τα οποία ορίζουν την ελάχιστη και μέγιστη επιτρεπόμενη ημερομηνία. - -```php -$form->addDate('date', 'Ημερομηνία:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Η ημερομηνία πρέπει να είναι τουλάχιστον ενός μηνός παλιά.', new DateTime('-1 month')); -``` - -Συνήθως επιστρέφει ένα αντικείμενο `DateTimeImmutable`, με τη μέθοδο `setFormat()` μπορείτε να καθορίσετε τη [μορφή κειμένου|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ή timestamp: - -```php -$form->addDate('date', 'Ημερομηνία:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Προσθέτει ένα πεδίο που επιτρέπει στον χρήστη να εισάγει εύκολα έναν χρόνο που αποτελείται από ώρες, λεπτά και προαιρετικά δευτερόλεπτα (κλάση [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ως προεπιλεγμένη τιμή δέχεται είτε αντικείμενα που υλοποιούν το interface `DateTimeInterface`, ένα string με χρόνο, είτε έναν αριθμό που αντιπροσωπεύει UNIX timestamp. Από αυτές τις εισόδους χρησιμοποιείται μόνο η πληροφορία του χρόνου, η ημερομηνία αγνοείται. Το ίδιο ισχύει για τα ορίσματα των κανόνων `Min`, `Max` ή `Range`, τα οποία ορίζουν τον ελάχιστο και μέγιστο επιτρεπόμενο χρόνο. Εάν η καθορισμένη ελάχιστη τιμή είναι υψηλότερη από τη μέγιστη, δημιουργείται ένα χρονικό εύρος που υπερβαίνει τα μεσάνυχτα. - -```php -$form->addTime('time', 'Ώρα:', withSeconds: true) - ->addRule($form::Range, 'Η ώρα πρέπει να είναι στο εύρος από %s έως %s.', ['12:30', '13:30']); -``` - -Συνήθως επιστρέφει ένα αντικείμενο `DateTimeImmutable` (με ημερομηνία 1 Ιανουαρίου του έτους 1), με τη μέθοδο `setFormat()` μπορείτε να καθορίσετε τη [μορφή κειμένου|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Ώρα:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Προσθέτει ένα πεδίο που επιτρέπει στον χρήστη να εισάγει εύκολα ημερομηνία και ώρα που αποτελείται από έτος, μήνα, ημέρα, ώρες, λεπτά και προαιρετικά δευτερόλεπτα (κλάση [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ως προεπιλεγμένη τιμή δέχεται είτε αντικείμενα που υλοποιούν το interface `DateTimeInterface`, ένα string με χρόνο, είτε έναν αριθμό που αντιπροσωπεύει UNIX timestamp. Το ίδιο ισχύει για τα ορίσματα των κανόνων `Min`, `Max` ή `Range`, τα οποία ορίζουν την ελάχιστη και μέγιστη επιτρεπόμενη ημερομηνία. - -```php -$form->addDateTime('datetime', 'Ημερομηνία και ώρα:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Η ημερομηνία πρέπει να είναι τουλάχιστον ενός μηνός παλιά.', new DateTime('-1 month')); -``` - -Συνήθως επιστρέφει ένα αντικείμενο `DateTimeImmutable`, με τη μέθοδο `setFormat()` μπορείτε να καθορίσετε τη [μορφή κειμένου|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ή timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Προσθέτει ένα πεδίο για την επιλογή χρώματος (κλάση [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Το χρώμα είναι ένα string στη μορφή `#rrggbb`. Εάν ο χρήστης δεν κάνει επιλογή, επιστρέφεται το μαύρο χρώμα `#000000`. - -```php -$form->addColor('color', 'Χρώμα:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Προσθέτει ένα κρυφό πεδίο (κλάση [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -Με τη χρήση του `setNullable()` μπορεί να οριστεί να επιστρέφει `null` αντί για κενό string. Η τροποποίηση της υποβληθείσας τιμής είναι δυνατή με το [addFilter() |validation#Τροποποίηση Εισόδου]. - -Παρόλο που το στοιχείο είναι κρυφό, είναι **σημαντικό να συνειδητοποιήσετε** ότι η τιμή μπορεί ακόμα να τροποποιηθεί ή να πλαστογραφηθεί από έναν εισβολέα. Πάντα επαληθεύετε και επικυρώνετε διεξοδικά όλες τις λαμβανόμενες τιμές στην πλευρά του διακομιστή για να αποφύγετε κινδύνους ασφαλείας που σχετίζονται με τη χειραγώγηση δεδομένων. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Προσθέτει ένα κουμπί υποβολής (κλάση [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Υποβολή'); -``` - -Στη φόρμα είναι δυνατόν να υπάρχουν και περισσότερα κουμπιά υποβολής: - -```php -$form->addSubmit('register', 'Εγγραφή'); -$form->addSubmit('cancel', 'Ακύρωση'); -``` - -Για να διαπιστώσετε σε ποιο από αυτά έγινε κλικ, χρησιμοποιήστε: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Εάν δεν θέλετε να επικυρώσετε ολόκληρη τη φόρμα κατά το πάτημα του κουμπιού (για παράδειγμα, στα κουμπιά *Ακύρωση* ή *Προεπισκόπηση*), χρησιμοποιήστε το [setValidationScope() |validation#Απενεργοποίηση Επικύρωσης]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Προσθέτει ένα κουμπί (κλάση [Button |api:Nette\Forms\Controls\Button]), το οποίο δεν έχει λειτουργία υποβολής. Μπορεί επομένως να χρησιμοποιηθεί για κάποια άλλη λειτουργία, π.χ. κλήση μιας συνάρτησης JavaScript κατά το κλικ. - -```php -$form->addButton('raise', 'Αύξηση μισθού') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Προσθέτει ένα κουμπί υποβολής με τη μορφή εικόνας (κλάση [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image'); -``` - -Κατά τη χρήση πολλαπλών κουμπιών υποβολής, μπορείτε να διαπιστώσετε σε ποιο έγινε κλικ, χρησιμοποιώντας το `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Προσθέτει μια υποφόρμα (κλάση [Container|api:Nette\Forms\Container]), ή αλλιώς container, στο οποίο μπορούν να προστεθούν άλλα στοιχεία με τον ίδιο τρόπο που τα προσθέτουμε στη φόρμα. Λειτουργούν επίσης οι μέθοδοι `setDefaults()` ή `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Το όνομά σας:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Το όνομά σας:'); -$sub2->addEmail('email', 'Email:'); -``` - -Τα υποβληθέντα δεδομένα επιστρέφονται στη συνέχεια ως πολυδιάστατη δομή: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Επισκόπηση ρυθμίσεων -==================== - -Σε όλα τα στοιχεία μπορούμε να καλέσουμε τις ακόλουθες μεθόδους (πλήρης επισκόπηση στην [τεκμηρίωση API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | ορίζει την προεπιλεγμένη τιμή -| `getValue()` | λαμβάνει την τρέχουσα τιμή -| `setOmitted()` | [#Παράλειψη τιμής] -| `setDisabled()` | [#Απενεργοποίηση στοιχείων] - -Απόδοση: -.[table-form-methods language-php] -| `setCaption($caption)` | αλλάζει την ετικέτα του στοιχείου -| `setTranslator($translator)` | ορίζει τον [μεταφραστή |rendering#Μετάφραση] -| `setHtmlAttribute($name, $value)` | ορίζει το [HTML attribute |rendering#Χαρακτηριστικά HTML] του στοιχείου -| `setHtmlId($id)` | ορίζει το HTML attribute `id` -| `setHtmlType($type)` | ορίζει το HTML attribute `type` -| `setHtmlName($name)` | ορίζει το HTML attribute `name` -| `setOption($key, $value)` | [ρυθμίσεις για απόδοση |rendering#Options] - -Επικύρωση: -.[table-form-methods language-php] -| `setRequired()` | [υποχρεωτικό στοιχείο |validation] -| `addRule()` | ορίζει τον [κανόνα επικύρωσης |validation#Κανόνες] -| `addCondition()`, `addConditionOn()` | ορίζει τη [συνθήκη επικύρωσης |validation#Συνθήκες] -| `addError($message)` | [παράδοση μηνύματος σφάλματος |validation#Σφάλματα κατά την Επεξεργασία] - -Στα στοιχεία `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` μπορούν να κληθούν οι ακόλουθες μέθοδοι: - -.[table-form-methods language-php] -| `setNullable()` | ορίζει αν το getValue() θα επιστρέψει `null` αντί για κενό string -| `setEmptyValue($value)` | ορίζει μια ειδική τιμή που θεωρείται κενό string -| `setMaxLength($length)` | ορίζει τον μέγιστο αριθμό επιτρεπόμενων χαρακτήρων -| `addFilter($filter)` | [επεξεργασία εισόδου |validation#Τροποποίηση Εισόδου] - - -Παράλειψη τιμής -=============== - -Εάν η τιμή που συμπλήρωσε ο χρήστης δεν μας ενδιαφέρει, μπορούμε να την παραλείψουμε από το αποτέλεσμα της μεθόδου `$form->getValues()` ή από τα δεδομένα που παραδίδονται στους handlers με τη χρήση του `setOmitted()`. Αυτό είναι χρήσιμο για διάφορους κωδικούς ελέγχου, στοιχεία antispam κ.λπ. - -```php -$form->addPassword('passwordVerify', 'Κωδικός για έλεγχο:') - ->setRequired('Παρακαλώ εισάγετε τον κωδικό ξανά για έλεγχο') - ->addRule($form::Equal, 'Οι κωδικοί δεν ταιριάζουν', $form['password']) - ->setOmitted(); -``` - - -Απενεργοποίηση στοιχείων -======================== - -Τα στοιχεία μπορούν να απενεργοποιηθούν με τη χρήση του `setDisabled()`. Ένα τέτοιο στοιχείο δεν μπορεί να επεξεργαστεί ο χρήστης. - -```php -$form->addText('username', 'Όνομα χρήστη:') - ->setDisabled(); -``` - -Τα απενεργοποιημένα στοιχεία ο περιηγητής δεν τα στέλνει καθόλου στον διακομιστή, επομένως δεν θα τα βρείτε ούτε στα δεδομένα που επιστρέφει η συνάρτηση `$form->getValues()`. Ωστόσο, εάν ορίσετε `setOmitted(false)`, το Nette θα συμπεριλάβει την προεπιλεγμένη τους τιμή σε αυτά τα δεδομένα. - -Κατά την κλήση του `setDisabled()`, για λόγους ασφαλείας **διαγράφεται η τιμή του στοιχείου**. Εάν ορίζετε μια προεπιλεγμένη τιμή, είναι απαραίτητο να το κάνετε μετά την απενεργοποίησή του: - -```php -$form->addText('username', 'Όνομα χρήστη:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Μια εναλλακτική λύση στα απενεργοποιημένα στοιχεία είναι τα στοιχεία με το HTML attribute `readonly`, τα οποία ο περιηγητής στέλνει στον διακομιστή. Παρόλο που το στοιχείο είναι μόνο για ανάγνωση, είναι **σημαντικό να συνειδητοποιήσετε** ότι η τιμή του μπορεί ακόμα να τροποποιηθεί ή να πλαστογραφηθεί από έναν εισβολέα. - - -Προσαρμοσμένα στοιχεία -====================== - -Εκτός από την ευρεία γκάμα ενσωματωμένων στοιχείων φόρμας, μπορείτε να προσθέσετε προσαρμοσμένα στοιχεία στη φόρμα με αυτόν τον τρόπο: - -```php -$form->addComponent(new DateInput('Ημερομηνία:'), 'date'); -// εναλλακτική σύνταξη: $form['date'] = new DateInput('Ημερομηνία:'); -``` - -.[note] -Η φόρμα είναι απόγονος της κλάσης [Container |component-model:#Container] και τα επιμέρους στοιχεία είναι απόγονοι του [Component |component-model:#Component]. - -Υπάρχει ένας τρόπος να ορίσετε νέες μεθόδους φόρμας που χρησιμεύουν για την προσθήκη προσαρμοσμένων στοιχείων (π.χ. `$form->addZip()`). Πρόκειται για τις λεγόμενες extension methods. Το μειονέκτημα είναι ότι η αυτόματη συμπλήρωση στους επεξεργαστές δεν θα λειτουργεί για αυτές. - -```php -use Nette\Forms\Container; - -// προσθέτουμε τη μέθοδο 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, 'Τουλάχιστον 5 αριθμοί', '[0-9]{5}'); -}); - -// χρήση -$form->addZip('zip', 'ZIP code:'); -``` - - -Στοιχεία χαμηλού επιπέδου -========================= - -Μπορούν να χρησιμοποιηθούν και στοιχεία που γράφουμε μόνο στο template και δεν τα προσθέτουμε στη φόρμα με κάποια από τις μεθόδους `$form->addXyz()`. Για παράδειγμα, όταν εμφανίζουμε εγγραφές από τη βάση δεδομένων και δεν ξέρουμε εκ των προτέρων πόσες θα είναι και ποια θα είναι τα ID τους, και θέλουμε σε κάθε γραμμή να εμφανίσουμε ένα checkbox ή ένα radio button, αρκεί να το κωδικοποιήσουμε στο template: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -Και μετά την υποβολή, βρίσκουμε την τιμή: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -όπου η πρώτη παράμετρος είναι ο τύπος του στοιχείου (`DataFile` για `type=file`, `DataLine` για εισόδους μίας γραμμής όπως `text`, `password`, `email` κ.λπ. και `DataText` για όλα τα υπόλοιπα) και η δεύτερη παράμετρος `sel[]` αντιστοιχεί στο HTML attribute name. Μπορούμε να συνδυάσουμε τον τύπο του στοιχείου με την τιμή `DataKeys`, η οποία διατηρεί τα κλειδιά των στοιχείων. Αυτό είναι ιδιαίτερα χρήσιμο για `select`, `radioList` και `checkboxList`. - -Το ουσιαστικό είναι ότι το `getHttpData()` επιστρέφει μια απολυμασμένη τιμή, σε αυτή την περίπτωση θα είναι πάντα ένας πίνακας έγκυρων UTF-8 strings, ανεξάρτητα από το τι θα προσπαθούσε να υποβάλει ένας εισβολέας στον διακομιστή. Πρόκειται για μια αναλογία της άμεσης εργασίας με το `$_POST` ή το `$_GET`, αλλά με τη σημαντική διαφορά ότι επιστρέφει πάντα καθαρά δεδομένα, όπως είστε συνηθισμένοι με τα τυπικά στοιχεία των φορμών Nette. diff --git a/forms/el/in-presenter.texy b/forms/el/in-presenter.texy deleted file mode 100644 index 7cc037f5d3..0000000000 --- a/forms/el/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Φόρμες στους presenters -*********************** - -.[perex] -Οι Nette Forms διευκολύνουν κατά πολύ τη δημιουργία και την επεξεργασία φορμών ιστού. Σε αυτό το κεφάλαιο, θα μάθετε πώς να χρησιμοποιείτε φόρμες μέσα στους presenters. - -Αν ενδιαφέρεστε για το πώς να τις χρησιμοποιήσετε εντελώς αυτόνομα χωρίς το υπόλοιπο framework, ο οδηγός για [αυτόνομη χρήση |standalone] είναι για εσάς. - - -Η πρώτη φόρμα -============= - -Ας δοκιμάσουμε να γράψουμε μια απλή φόρμα εγγραφής. Ο κώδικάς της θα είναι ο εξής: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Όνομα:'); -$form->addPassword('password', 'Κωδικός πρόσβασης:'); -$form->addSubmit('send', 'Εγγραφή'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -και στον περιηγητή θα εμφανιστεί έτσι: - -[* form-cs.webp *] - -Μια φόρμα σε έναν presenter είναι ένα αντικείμενο της κλάσης `Nette\Application\UI\Form`, ο προκάτοχός της `Nette\Forms\Form` προορίζεται για αυτόνομη χρήση. Προσθέσαμε σε αυτήν τα λεγόμενα στοιχεία όνομα, κωδικό πρόσβασης και ένα κουμπί υποβολής. Και τέλος, η γραμμή με το `$form->onSuccess` λέει ότι μετά την υποβολή και την επιτυχή επικύρωση, πρέπει να κληθεί η μέθοδος `$this->formSucceeded()`. - -Από την οπτική γωνία του presenter, η φόρμα είναι ένα συνηθισμένο component. Επομένως, αντιμετωπίζεται ως component και την ενσωματώνουμε στον presenter χρησιμοποιώντας [factory methods |application:components#Μέθοδοι Εργοστασίου]. Θα μοιάζει κάπως έτσι: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Όνομα:'); - $form->addPassword('password', 'Κωδικός πρόσβασης:'); - $form->addSubmit('send', 'Εγγραφή'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // εδώ επεξεργαζόμαστε τα δεδομένα που υποβλήθηκαν από τη φόρμα - // το $data->name περιέχει το όνομα - // το $data->password περιέχει τον κωδικό πρόσβασης - $this->flashMessage('Εγγραφήκατε με επιτυχία.'); - $this->redirect('Home:'); - } -} -``` - -Και στο template, αποδίδουμε τη φόρμα με την ετικέτα `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Εγγραφή</h1> - -{control registrationForm} -``` - -Και αυτό είναι όλο :-) Έχουμε μια λειτουργική και τέλεια [ασφαλή |#Προστασία από ευπάθειες] φόρμα. - -Και τώρα πιθανότατα σκέφτεστε ότι αυτό ήταν πολύ γρήγορο, αναρωτιέστε πώς είναι δυνατόν να κληθεί η μέθοδος `formSucceeded()` και ποιες είναι οι παράμετροι που λαμβάνει. Σίγουρα, έχετε δίκιο, αυτό αξίζει εξήγηση. - -Η Nette εισάγει έναν φρέσκο μηχανισμό, τον οποίο ονομάζουμε [Hollywood style |application:components#Hollywood Style]. Αντί εσείς, ως προγραμματιστής, να πρέπει συνεχώς να ρωτάτε αν συνέβη κάτι («υποβλήθηκε η φόρμα;», «υποβλήθηκε έγκυρα;» και «δεν παραποιήθηκε;»), λέτε στο framework «όταν η φόρμα συμπληρωθεί έγκυρα, κάλεσε αυτή τη μέθοδο» και αφήνετε την υπόλοιπη δουλειά σε αυτό. Αν προγραμματίζετε σε JavaScript, αυτό το στυλ προγραμματισμού σας είναι πολύ οικείο. Γράφετε συναρτήσεις που καλούνται όταν συμβεί ένα συγκεκριμένο [γεγονός |nette:glossary#Events]. Και η γλώσσα τους περνά τα κατάλληλα ορίσματα. - -Ακριβώς έτσι είναι δομημένος και ο παραπάνω κώδικας του presenter. Ο πίνακας `$form->onSuccess` αντιπροσωπεύει μια λίστα από PHP callbacks που η Nette καλεί τη στιγμή που η φόρμα υποβάλλεται και συμπληρώνεται σωστά (δηλαδή είναι έγκυρη). Στο πλαίσιο του [κύκλου ζωής του presenter |application:presenters#Κύκλος ζωής του presenter] πρόκειται για ένα λεγόμενο σήμα, οπότε καλούνται μετά τη μέθοδο `action*` και πριν από τη μέθοδο `render*`. Και σε κάθε callback, περνά ως πρώτη παράμετρο την ίδια τη φόρμα και ως δεύτερη τα υποβληθέντα δεδομένα με τη μορφή ενός αντικειμένου [ArrayHash |utils:arrays#ArrayHash]. Μπορείτε να παραλείψετε την πρώτη παράμετρο αν δεν χρειάζεστε το αντικείμενο της φόρμας. Και η δεύτερη παράμετρος μπορεί να είναι πιο έξυπνη, αλλά γι' αυτό θα μιλήσουμε [αργότερα |#Αντιστοίχιση σε κλάσεις]. - -Το αντικείμενο `$data` περιέχει τα κλειδιά `name` και `password` με τα δεδομένα που συμπλήρωσε ο χρήστης. Συνήθως, στέλνουμε αμέσως τα δεδομένα για περαιτέρω επεξεργασία, η οποία μπορεί να είναι, για παράδειγμα, η εισαγωγή στη βάση δεδομένων. Ωστόσο, κατά την επεξεργασία μπορεί να προκύψει σφάλμα, για παράδειγμα, το όνομα χρήστη είναι ήδη κατειλημμένο. Σε αυτή την περίπτωση, επιστρέφουμε το σφάλμα στη φόρμα χρησιμοποιώντας την `addError()` και την αφήνουμε να αποδοθεί ξανά, μαζί με το μήνυμα σφάλματος. - -```php -$form->addError('Λυπούμαστε, το όνομα χρήστη χρησιμοποιείται ήδη.'); -``` - -Εκτός από το `onSuccess`, υπάρχει επίσης το `onSubmit`: τα callbacks καλούνται πάντα μετά την υποβολή της φόρμας, ακόμη και αν δεν έχει συμπληρωθεί σωστά. Και επίσης το `onError`: τα callbacks καλούνται μόνο αν η υποβολή δεν είναι έγκυρη. Καλούνται ακόμη και αν στο `onSuccess` ή στο `onSubmit` ακυρώσουμε την εγκυρότητα της φόρμας χρησιμοποιώντας την `addError()`. - -Μετά την επεξεργασία της φόρμας, ανακατευθύνουμε σε άλλη σελίδα. Αυτό αποτρέπει την ακούσια επανυποβολή της φόρμας με το κουμπί *ανανέωση*, *πίσω* ή με την κίνηση στο ιστορικό του περιηγητή. - -Δοκιμάστε να προσθέσετε και άλλα [στοιχεία φόρμας|controls]. - - -Πρόσβαση στα στοιχεία -===================== - -Η φόρμα είναι ένα component του presenter, στην περίπτωσή μας ονομάζεται `registrationForm` (από το όνομα της factory method `createComponentRegistrationForm`), οπότε οπουδήποτε στον presenter μπορείτε να αποκτήσετε πρόσβαση στη φόρμα χρησιμοποιώντας: - -```php -$form = $this->getComponent('registrationForm'); -// εναλλακτική σύνταξη: $form = $this['registrationForm']; -``` - -Τα μεμονωμένα στοιχεία της φόρμας είναι επίσης components, επομένως μπορείτε να αποκτήσετε πρόσβαση σε αυτά με τον ίδιο τρόπο: - -```php -$input = $form->getComponent('name'); // ή $input = $form['name']; -$button = $form->getComponent('send'); // ή $button = $form['send']; -``` - -Τα στοιχεία αφαιρούνται χρησιμοποιώντας το unset: - -```php -unset($form['name']); -``` - - -Κανόνες επικύρωσης -================== - -Αναφέρθηκε η λέξη *έγκυρη*, αλλά η φόρμα δεν έχει ακόμη κανόνες επικύρωσης. Ας το διορθώσουμε αυτό. - -Το όνομα θα είναι υποχρεωτικό, γι' αυτό το επισημαίνουμε με τη μέθοδο `setRequired()`, το όρισμα της οποίας είναι το κείμενο του μηνύματος σφάλματος που θα εμφανιστεί εάν ο χρήστης δεν συμπληρώσει το όνομα. Εάν δεν παρέχουμε όρισμα, θα χρησιμοποιηθεί το προεπιλεγμένο μήνυμα σφάλματος. - -```php -$form->addText('name', 'Όνομα:') - ->setRequired('Παρακαλώ εισάγετε ένα όνομα'); -``` - -Δοκιμάστε να υποβάλετε τη φόρμα χωρίς να συμπληρώσετε το όνομα και θα δείτε ότι θα εμφανιστεί ένα μήνυμα σφάλματος και ο περιηγητής ή ο διακομιστής θα την απορρίπτει μέχρι να συμπληρώσετε το πεδίο. - -Ταυτόχρονα, δεν μπορείτε να ξεγελάσετε το σύστημα γράφοντας, για παράδειγμα, μόνο κενά στο πεδίο. Όχι. Η Nette αφαιρεί αυτόματα τα κενά στην αρχή και στο τέλος. Δοκιμάστε το. Είναι κάτι που πρέπει πάντα να κάνετε με κάθε input μίας γραμμής, αλλά συχνά ξεχνιέται. Η Nette το κάνει αυτόματα. (Μπορείτε να δοκιμάσετε να ξεγελάσετε τη φόρμα και να στείλετε μια συμβολοσειρά πολλών γραμμών ως όνομα. Ούτε εδώ η Nette δεν θα μπερδευτεί και θα αλλάξει τις αλλαγές γραμμής σε κενά.) - -Η φόρμα επικυρώνεται πάντα από την πλευρά του διακομιστή, αλλά δημιουργείται επίσης επικύρωση JavaScript, η οποία εκτελείται αστραπιαία και ο χρήστης ενημερώνεται για το σφάλμα αμέσως, χωρίς να χρειάζεται να υποβάλει τη φόρμα στον διακομιστή. Αυτό το χειρίζεται το script `netteForms.js`. Εισαγάγετέ το στο template του layout: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Αν κοιτάξετε τον πηγαίο κώδικα της σελίδας με τη φόρμα, μπορείτε να παρατηρήσετε ότι η Nette εισάγει τα υποχρεωτικά στοιχεία σε στοιχεία με την κλάση CSS `required`. Δοκιμάστε να προσθέσετε το ακόλουθο φύλλο στυλ στο template και η ετικέτα "Όνομα" θα γίνει κόκκινη. Με αυτόν τον κομψό τρόπο, επισημαίνουμε τα υποχρεωτικά στοιχεία στους χρήστες: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Προσθέτουμε περαιτέρω κανόνες επικύρωσης με τη μέθοδο `addRule()`. Η πρώτη παράμετρος είναι ο κανόνας, η δεύτερη είναι πάλι το κείμενο του μηνύματος σφάλματος και μπορεί να ακολουθήσει ένα όρισμα του κανόνα επικύρωσης. Τι σημαίνει αυτό; - -Θα επεκτείνουμε τη φόρμα με ένα νέο προαιρετικό πεδίο "ηλικία", το οποίο πρέπει να είναι ακέραιος αριθμός (`addInteger()`) και επιπλέον εντός επιτρεπτού εύρους (`$form::Range`). Και εδώ ακριβώς θα χρησιμοποιήσουμε την τρίτη παράμετρο της μεθόδου `addRule()`, με την οποία περνάμε στον επικυρωτή το απαιτούμενο εύρος ως ζεύγος `[από, έως]`: - -```php -$form->addInteger('age', 'Ηλικία:') - ->addRule($form::Range, 'Η ηλικία πρέπει να είναι από 18 έως 120', [18, 120]); -``` - -.[tip] -Εάν ο χρήστης δεν συμπληρώσει το πεδίο, οι κανόνες επικύρωσης δεν θα ελεγχθούν, καθώς το στοιχείο είναι προαιρετικό. - -Εδώ υπάρχει χώρος για μια μικρή αναδιάρθρωση (refactoring). Στο μήνυμα σφάλματος και στην τρίτη παράμετρο, οι αριθμοί αναφέρονται διπλά, πράγμα που δεν είναι ιδανικό. Αν δημιουργούσαμε [πολύγλωσσες φόρμες |rendering#Μετάφραση] και το μήνυμα που περιέχει αριθμούς μεταφραζόταν σε πολλές γλώσσες, θα δυσκόλευε μια πιθανή αλλαγή των τιμών. Για το λόγο αυτό, είναι δυνατόν να χρησιμοποιηθούν οι χαρακτήρες υποκατάστασης `%d` και η Nette θα συμπληρώσει τις τιμές: - -```php - ->addRule($form::Range, 'Η ηλικία πρέπει να είναι από %d έως %d ετών', [18, 120]); -``` - -Ας επιστρέψουμε στο στοιχείο `password`, το οποίο θα καταστήσουμε επίσης υποχρεωτικό και θα ελέγξουμε επιπλέον το ελάχιστο μήκος του κωδικού πρόσβασης (`$form::MinLength`), χρησιμοποιώντας πάλι τον χαρακτήρα υποκατάστασης: - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - ->setRequired('Επιλέξτε έναν κωδικό πρόσβασης') - ->addRule($form::MinLength, 'Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον %d χαρακτήρες', 8); -``` - -Θα προσθέσουμε στη φόρμα ένα ακόμη πεδίο `passwordVerify`, όπου ο χρήστης θα εισάγει τον κωδικό πρόσβασης ξανά, για έλεγχο. Χρησιμοποιώντας κανόνες επικύρωσης, θα ελέγξουμε αν οι δύο κωδικοί πρόσβασης είναι ίδιοι (`$form::Equal`). Και ως παράμετρο θα δώσουμε μια αναφορά στον πρώτο κωδικό πρόσβασης χρησιμοποιώντας [αγκύλες |#Πρόσβαση στα στοιχεία]: - -```php -$form->addPassword('passwordVerify', 'Κωδικός πρόσβασης για έλεγχο:') - ->setRequired('Παρακαλώ εισάγετε τον κωδικό πρόσβασης ξανά για έλεγχο') - ->addRule($form::Equal, 'Οι κωδικοί πρόσβασης δεν ταιριάζουν', $form['password']) - ->setOmitted(); -``` - -Με τη χρήση της `setOmitted()`, επισημάναμε το στοιχείο του οποίου η τιμή στην πραγματικότητα δεν μας ενδιαφέρει και το οποίο υπάρχει μόνο για λόγους επικύρωσης. Η τιμή δεν θα περάσει στο `$data`. - -Με αυτό, έχουμε μια πλήρως λειτουργική φόρμα με επικύρωση σε PHP και JavaScript. Οι δυνατότητες επικύρωσης της Nette είναι πολύ ευρύτερες, μπορούν να δημιουργηθούν συνθήκες, να εμφανίζονται και να αποκρύπτονται τμήματα της σελίδας βάσει αυτών, κ.λπ. Όλα θα τα μάθετε στο κεφάλαιο για την [επικύρωση φορμών|validation]. - - -Προεπιλεγμένες τιμές -==================== - -Συνήθως ορίζουμε προεπιλεγμένες τιμές για τα στοιχεία της φόρμας: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Συχνά είναι χρήσιμο να ορίσουμε προεπιλεγμένες τιμές για όλα τα στοιχεία ταυτόχρονα. Για παράδειγμα, όταν η φόρμα χρησιμοποιείται για την επεξεργασία εγγραφών. Διαβάζουμε την εγγραφή από τη βάση δεδομένων και ορίζουμε τις προεπιλεγμένες τιμές: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Καλέστε την `setDefaults()` μετά τον ορισμό των στοιχείων. - - -Απόδοση φόρμας -============== - -Από προεπιλογή, η φόρμα αποδίδεται ως πίνακας. Τα μεμονωμένα στοιχεία πληρούν τον βασικό κανόνα προσβασιμότητας - όλες οι ετικέτες γράφονται ως `<label>` και συνδέονται με το αντίστοιχο στοιχείο της φόρμας. Όταν κάνετε κλικ στην ετικέτα, ο δρομέας εμφανίζεται αυτόματα στο πεδίο της φόρμας. - -Μπορούμε να ορίσουμε οποιαδήποτε HTML attributes για κάθε στοιχείο. Για παράδειγμα, να προσθέσουμε ένα placeholder: - -```php -$form->addInteger('age', 'Ηλικία:') - ->setHtmlAttribute('placeholder', 'Παρακαλώ συμπληρώστε την ηλικία'); -``` - -Υπάρχουν πραγματικά πολλοί τρόποι για να αποδοθεί μια φόρμα, γι' αυτό υπάρχει ένα [ξεχωριστό κεφάλαιο για την απόδοση|rendering]. - - -Αντιστοίχιση σε κλάσεις -======================= - -Ας επιστρέψουμε στη μέθοδο `formSucceeded()`, η οποία στη δεύτερη παράμετρο `$data` λαμβάνει τα υποβληθέντα δεδομένα ως αντικείμενο `ArrayHash`. Επειδή πρόκειται για μια γενική κλάση, κάτι σαν `stdClass`, θα μας λείψει κάποια άνεση κατά την εργασία μαζί της, όπως η πρόταση properties στους επεξεργαστές ή η στατική ανάλυση κώδικα. Αυτό θα μπορούσε να λυθεί έχοντας μια συγκεκριμένη κλάση για κάθε φόρμα, της οποίας οι properties αντιπροσωπεύουν τα μεμονωμένα στοιχεία. Π.χ.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Εναλλακτικά, μπορείτε να χρησιμοποιήσετε τον κατασκευαστή: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Οι ιδιότητες της κλάσης δεδομένων μπορούν επίσης να είναι enum και θα αντιστοιχιστούν αυτόματα. .{data-version:3.2.4} - -Πώς να πούμε στη Nette να μας επιστρέφει τα δεδομένα ως αντικείμενα αυτής της κλάσης; Πιο εύκολα από ό,τι νομίζετε. Αρκεί απλώς να δηλώσετε την κλάση ως τύπο της παραμέτρου `$data` στη μέθοδο χειρισμού: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // το $data είναι μια παρουσία του RegistrationFormData - $name = $data->name; - // ... -} -``` - -Ως τύπος μπορεί επίσης να δηλωθεί το `array` και τότε τα δεδομένα θα περάσουν ως πίνακας. - -Με παρόμοιο τρόπο μπορεί να χρησιμοποιηθεί και η συνάρτηση `getValues()`, στην οποία περνάμε το όνομα της κλάσης ή το αντικείμενο προς ενυδάτωση ως παράμετρο: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Εάν οι φόρμες σχηματίζουν μια πολυεπίπεδη δομή αποτελούμενη από containers, δημιουργήστε μια ξεχωριστή κλάση για καθένα: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Η αντιστοίχιση τότε από τον τύπο της property `$person` καταλαβαίνει ότι πρέπει να αντιστοιχίσει το container στην κλάση `PersonFormData`. Εάν η property περιείχε έναν πίνακα από containers, δηλώστε τον τύπο `array` και περάστε την κλάση για αντιστοίχιση απευθείας στο container: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Μπορείτε να ζητήσετε τη δημιουργία του σχεδίου της κλάσης δεδομένων της φόρμας χρησιμοποιώντας τη μέθοδο `Nette\Forms\Blueprint::dataClass($form)`, η οποία θα το εκτυπώσει στη σελίδα του περιηγητή. Στη συνέχεια, αρκεί να επιλέξετε τον κώδικα με κλικ και να τον αντιγράψετε στο έργο σας. .{data-version:3.1.15} - - -Πολλαπλά κουμπιά -================ - -Εάν η φόρμα έχει περισσότερα από ένα κουμπιά, συνήθως χρειαζόμαστε να διακρίνουμε ποιο από αυτά πατήθηκε. Μπορούμε να δημιουργήσουμε τη δική μας συνάρτηση χειρισμού για κάθε κουμπί. Θα την ορίσουμε ως handler για το [γεγονός |nette:glossary#Events] `onClick`: - -```php -$form->addSubmit('save', 'Αποθήκευση') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Διαγραφή') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Αυτοί οι handlers καλούνται μόνο στην περίπτωση έγκυρα συμπληρωμένης φόρμας, όπως και στην περίπτωση του συμβάντος `onSuccess`. Η διαφορά είναι ότι ως πρώτη παράμετρος, αντί για τη φόρμα, μπορεί να περάσει το κουμπί υποβολής, ανάλογα με τον τύπο που θα δηλώσετε: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Όταν η φόρμα υποβάλλεται με το πλήκτρο <kbd>Enter</kbd>, θεωρείται σαν να υποβλήθηκε με το πρώτο κουμπί. - - -Το συμβάν onAnchor -================== - -Όταν στη factory method (όπως π.χ. η `createComponentRegistrationForm`) κατασκευάζουμε τη φόρμα, αυτή δεν γνωρίζει ακόμη αν υποβλήθηκε, ούτε με ποια δεδομένα. Υπάρχουν όμως περιπτώσεις όπου χρειαζόμαστε να γνωρίζουμε τις υποβληθείσες τιμές, για παράδειγμα, η περαιτέρω μορφή της φόρμας εξαρτάται από αυτές, ή τις χρειαζόμαστε για εξαρτώμενα selectboxes κ.λπ. - -Μπορείτε λοιπόν να αφήσετε ένα μέρος του κώδικα που κατασκευάζει τη φόρμα να κληθεί μόνο τη στιγμή που είναι, όπως λέγεται, αγκυρωμένη, δηλαδή είναι ήδη συνδεδεμένη με τον presenter και γνωρίζει τα υποβληθέντα δεδομένα της. Τέτοιο κώδικα τον περνάμε στον πίνακα `$onAnchor`: - -```php -$country = $form->addSelect('country', 'Χώρα:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Πόλη:'); - -$form->onAnchor[] = function () use ($country, $city) { - // αυτή η συνάρτηση καλείται όταν η φόρμα γνωρίζει αν έχει υποβληθεί και με ποια δεδομένα - // επομένως, η μέθοδος getValue() μπορεί να χρησιμοποιηθεί - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Προστασία από ευπάθειες -======================= - -Το Nette Framework δίνει μεγάλη έμφαση στην ασφάλεια και γι' αυτό φροντίζει σχολαστικά για την καλή ασφάλεια των φορμών. Το κάνει εντελώς διαφανώς και δεν απαιτεί χειροκίνητη ρύθμιση. - -Εκτός από την προστασία των φορμών από επιθέσεις [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] και [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], κάνει πολλές μικρές ασφαλιστικές δικλείδες για τις οποίες εσείς δεν χρειάζεται πλέον να σκέφτεστε. - -Για παράδειγμα, φιλτράρει από τις εισόδους όλους τους χαρακτήρες ελέγχου και ελέγχει την εγκυρότητα της κωδικοποίησης UTF-8, έτσι ώστε τα δεδομένα από τη φόρμα να είναι πάντα καθαρά. Στα select boxes και radio lists, ελέγχει ότι τα επιλεγμένα στοιχεία ήταν πραγματικά από τα προσφερόμενα και ότι δεν υπήρξε παραποίηση. Έχουμε ήδη αναφέρει ότι στα text inputs μίας γραμμής αφαιρεί τους χαρακτήρες τέλους γραμμής που θα μπορούσε να στείλει ένας εισβολέας. Στα inputs πολλών γραμμών, κανονικοποιεί τους χαρακτήρες τέλους γραμμής. Και ούτω καθεξής. - -Η Nette λύνει για εσάς κινδύνους ασφαλείας που πολλοί προγραμματιστές ούτε καν υποψιάζονται ότι υπάρχουν. - -Η αναφερόμενη επίθεση CSRF συνίσταται στο ότι ο εισβολέας προσελκύει το θύμα σε μια σελίδα που εκτελεί διακριτικά στον περιηγητή του θύματος ένα αίτημα προς τον διακομιστή στον οποίο το θύμα είναι συνδεδεμένο, και ο διακομιστής πιστεύει ότι το αίτημα εκτελέστηκε από το θύμα με τη θέλησή του. Γι' αυτό η Nette αποτρέπει την υποβολή φορμών POST από άλλο domain. Εάν για κάποιο λόγο θέλετε να απενεργοποιήσετε την προστασία και να επιτρέψετε την υποβολή της φόρμας από άλλο domain, χρησιμοποιήστε: - -```php -$form->allowCrossOrigin(); // ΠΡΟΣΟΧΗ! Απενεργοποιεί την προστασία! -``` - -Αυτή η προστασία χρησιμοποιεί ένα SameSite cookie με το όνομα `_nss`. Η προστασία μέσω SameSite cookie μπορεί να μην είναι 100% αξιόπιστη, γι' αυτό είναι σκόπιμο να ενεργοποιήσετε επιπλέον την προστασία μέσω token: - -```php -$form->addProtection(); -``` - -Συνιστούμε να προστατεύετε με αυτόν τον τρόπο τις φόρμες στο διαχειριστικό τμήμα του ιστότοπου που αλλάζουν ευαίσθητα δεδομένα στην εφαρμογή. Το framework αμύνεται έναντι της επίθεσης CSRF δημιουργώντας και επαληθεύοντας ένα token εξουσιοδότησης, το οποίο αποθηκεύεται στο session. Επομένως, είναι απαραίτητο να έχετε ανοιχτό το session πριν από την εμφάνιση της φόρμας. Στο διαχειριστικό τμήμα του ιστότοπου, το session είναι συνήθως ήδη ενεργοποιημένο λόγω της σύνδεσης του χρήστη. Διαφορετικά, ξεκινήστε το session με τη μέθοδο `Nette\Http\Session::start()`. - - -Η ίδια φόρμα σε πολλούς presenters -================================== - -Εάν χρειάζεστε να χρησιμοποιήσετε την ίδια φόρμα σε πολλούς presenters, συνιστούμε να δημιουργήσετε ένα factory για αυτήν, το οποίο στη συνέχεια θα περάσετε στον presenter. Μια κατάλληλη τοποθεσία για μια τέτοια κλάση είναι, για παράδειγμα, ο κατάλογος `app/Forms`. - -Η κλάση factory μπορεί να μοιάζει κάπως έτσι: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Όνομα:'); - $form->addSubmit('send', 'Σύνδεση'); - return $form; - } -} -``` - -Ζητάμε από την κλάση να κατασκευάσει τη φόρμα στη factory method για components στον presenter: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // μπορούμε να τροποποιήσουμε τη φόρμα, εδώ για παράδειγμα αλλάζουμε την ετικέτα στο κουμπί - $form['send']->setCaption('Συνέχεια'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // και προσθέτουμε τον handler - return $form; -} -``` - -Ο handler για την επεξεργασία της φόρμας μπορεί επίσης να παρασχεθεί ήδη από το factory: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Όνομα:'); - $form->addSubmit('send', 'Σύνδεση'); - $form->onSuccess[] = function (Form $form, $data): void { - // εδώ εκτελούμε την επεξεργασία της φόρμας - }; - return $form; - } -} -``` - -Λοιπόν, έχουμε πίσω μας μια γρήγορη εισαγωγή στις φόρμες στη Nette. Δοκιμάστε να ρίξετε μια ματιά στον κατάλογο [examples |https://github.com/nette/forms/tree/master/examples] στη διανομή, όπου θα βρείτε περαιτέρω έμπνευση. diff --git a/forms/el/rendering.texy b/forms/el/rendering.texy deleted file mode 100644 index 56c525c592..0000000000 --- a/forms/el/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Απόδοση φορμών -************** - -Η εμφάνιση των φορμών μπορεί να είναι πολύ διαφορετική. Στην πράξη, μπορούμε να συναντήσουμε δύο άκρα. Από τη μία πλευρά, υπάρχει η ανάγκη να αποδοθούν στην εφαρμογή πολλές φόρμες που είναι οπτικά παρόμοιες σαν δύο σταγόνες νερό, και εκτιμούμε την εύκολη απόδοση χωρίς πρότυπο χρησιμοποιώντας την `$form->render()`. Αυτή είναι συνήθως η περίπτωση των διαχειριστικών διεπαφών. - -Από την άλλη πλευρά, υπάρχουν ποικίλες φόρμες όπου ισχύει: κάθε κομμάτι, ένα πρωτότυπο. Η μορφή τους περιγράφεται καλύτερα με τη γλώσσα HTML στο πρότυπο της φόρμας. Και φυσικά, εκτός από τα δύο αναφερόμενα άκρα, θα συναντήσουμε πολλές φόρμες που κινούνται κάπου στη μέση. - - -Απόδοση με χρήση Latte -====================== - -Το [σύστημα προτύπων Latte |latte:] διευκολύνει σημαντικά την απόδοση των φορμών και των στοιχείων τους. Πρώτα θα δείξουμε πώς να αποδίδετε τις φόρμες χειροκίνητα, στοιχείο προς στοιχείο, αποκτώντας έτσι πλήρη έλεγχο του κώδικα. Αργότερα θα δείξουμε πώς μπορεί αυτή η απόδοση να [αυτοματοποιηθεί |#Αυτόματη απόδοση]. - -Μπορείτε να ζητήσετε τη δημιουργία του σχεδίου του προτύπου Latte της φόρμας χρησιμοποιώντας τη μέθοδο `Nette\Forms\Blueprint::latte($form)`, η οποία θα το εκτυπώσει στη σελίδα του προγράμματος περιήγησης. Στη συνέχεια, αρκεί να επιλέξετε τον κώδικα με κλικ και να τον αντιγράψετε στο έργο σας. .{data-version:3.1.15} - - -`{control}` ------------ - -Ο απλούστερος τρόπος για να αποδοθεί μια φόρμα είναι να γράψετε στο πρότυπο: - -```latte -{control signInForm} -``` - -Μπορείτε να επηρεάσετε την εμφάνιση της φόρμας που αποδίδεται με αυτόν τον τρόπο διαμορφώνοντας τον [#Renderer] και τα [μεμονωμένα στοιχεία ελέγχου |#Χαρακτηριστικά HTML]. - - -`n:name` --------- - -Ο ορισμός της φόρμας στον κώδικα PHP μπορεί να συνδεθεί εξαιρετικά εύκολα με τον κώδικα HTML. Αρκεί απλώς να προσθέσετε τα χαρακτηριστικά `n:name`. Είναι τόσο εύκολο! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Όνομα χρήστη: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Κωδικός πρόσβασης: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Έχετε τον πλήρη έλεγχο της μορφής του τελικού κώδικα HTML. Εάν χρησιμοποιήσετε το χαρακτηριστικό `n:name` στα στοιχεία `<select>`, `<button>` ή `<textarea>`, το εσωτερικό τους περιεχόμενο θα συμπληρωθεί αυτόματα. Επιπλέον, η ετικέτα `<form n:name>` δημιουργεί μια τοπική μεταβλητή `$form` με το αντικείμενο της αποδιδόμενης φόρμας και η κλείνουσα `</form>` αποδίδει όλα τα μη αποδοθέντα κρυφά στοιχεία (το ίδιο ισχύει και για `{form} ... {/form}`). - -Ωστόσο, δεν πρέπει να ξεχάσουμε να αποδώσουμε τα πιθανά μηνύματα σφάλματος. Και αυτά που προστέθηκαν με τη μέθοδο `addError()` στα μεμονωμένα στοιχεία (χρησιμοποιώντας `{inputError}`), και αυτά που προστέθηκαν απευθείας στη φόρμα (τα επιστρέφει η `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Όνομα χρήστη: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Κωδικός πρόσβασης: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Πιο σύνθετα στοιχεία φόρμας, όπως το RadioList ή το CheckboxList, μπορούν να αποδοθούν με αυτόν τον τρόπο ανά μεμονωμένο στοιχείο: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Δεν θέλετε να σκέφτεστε για κάθε στοιχείο ποιο στοιχείο HTML να χρησιμοποιήσετε στο πρότυπο, αν `<input>`, `<textarea>` κ.λπ.; Η λύση είναι η καθολική ετικέτα `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Όνομα χρήστη: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Κωδικός πρόσβασης: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Εάν η φόρμα χρησιμοποιεί μεταφραστή, το κείμενο μέσα στις ετικέτες `{label}` θα μεταφραστεί. - -Ακόμη και σε αυτή την περίπτωση, πιο σύνθετα στοιχεία φόρμας, όπως το RadioList ή το CheckboxList, μπορούν να αποδοθούν ανά μεμονωμένο στοιχείο: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Για να αποδώσετε μόνο το `<input>` στο στοιχείο Checkbox, χρησιμοποιήστε `{input myCheckbox:}`. Σε αυτή την περίπτωση, διαχωρίζετε πάντα τα χαρακτηριστικά HTML με κόμμα `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Εκτυπώνει το μήνυμα σφάλματος για ένα στοιχείο φόρμας, αν υπάρχει. Συνήθως τυλίγουμε το μήνυμα σε ένα στοιχείο HTML για στυλ. Μπορείτε να αποτρέψετε την απόδοση ενός κενού στοιχείου εάν δεν υπάρχει μήνυμα, κομψά με τη χρήση του `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -Μπορούμε να ελέγξουμε την παρουσία σφάλματος με τη μέθοδο `hasErrors()` και ανάλογα να ορίσουμε την κλάση στο γονικό στοιχείο: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -Οι ετικέτες `{form signInForm}...{/form}` είναι μια εναλλακτική λύση για το `<form n:name="signInForm">...</form>`. - - -Αυτόματη απόδοση ----------------- - -Χάρη στις ετικέτες `{input}` και `{label}`, μπορούμε εύκολα να δημιουργήσουμε ένα γενικό πρότυπο για οποιαδήποτε φόρμα. Θα επαναλαμβάνει και θα αποδίδει διαδοχικά όλα τα στοιχεία της, εκτός από τα κρυφά στοιχεία, τα οποία αποδίδονται αυτόματα κατά το κλείσιμο της φόρμας με την ετικέτα `</form>`. Το όνομα της αποδιδόμενης φόρμας θα αναμένεται στη μεταβλητή `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -Οι χρησιμοποιούμενες αυτοκλειόμενες ζευγαρωτές ετικέτες `{label .../}` εμφανίζουν τις ετικέτες που προέρχονται από τον ορισμό της φόρμας στον κώδικα PHP. - -Αποθηκεύστε αυτό το γενικό πρότυπο, για παράδειγμα, στο αρχείο `basic-form.latte` και για να αποδώσετε τη φόρμα, αρκεί να το συμπεριλάβετε και να περάσετε το όνομα (ή την παρουσία) της φόρμας στην παράμετρο `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Αν θέλετε να παρέμβετε στη μορφή μιας συγκεκριμένης φόρμας κατά την απόδοσή της και, για παράδειγμα, να αποδώσετε ένα στοιχείο διαφορετικά, ο ευκολότερος τρόπος είναι να προετοιμάσετε μπλοκ στο πρότυπο που θα μπορούν στη συνέχεια να αντικατασταθούν. Τα μπλοκ μπορούν επίσης να έχουν [δυναμικά ονόματα μπλοκ |latte:template-inheritance#Δυναμικά ονόματα μπλοκ], οπότε μπορείτε να εισαγάγετε σε αυτά και το όνομα του αποδιδόμενου στοιχείου. Για παράδειγμα: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Για το στοιχείο, π.χ., `username`, δημιουργείται έτσι το μπλοκ `input-username`, το οποίο μπορεί εύκολα να αντικατασταθεί χρησιμοποιώντας την [ετικέτα {embed} |latte:template-inheritance#Κληρονομικότητα μονάδας embed]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Εναλλακτικά, μπορείτε να [define |latte:template-inheritance#Ορισμοί define] ολόκληρο το περιεχόμενο του template `basic-form.latte` ως μπλοκ, συμπεριλαμβανομένης της παραμέτρου `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Χάρη σε αυτό, η κλήση του θα είναι ελαφρώς απλούστερη: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -Το μπλοκ αρκεί να εισαχθεί σε ένα μόνο σημείο, στην αρχή του προτύπου της διάταξης: - -```latte -{import basic-form.latte} -``` - - -Ειδικές περιπτώσεις -------------------- - -Εάν χρειάζεστε να αποδώσετε μόνο το εσωτερικό μέρος της φόρμας χωρίς τις ετικέτες HTML `<form>`, για παράδειγμα κατά την αποστολή αποσπασμάτων, αποκρύψτε τις χρησιμοποιώντας το χαρακτηριστικό `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Όνομα χρήστη: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -Η ετικέτα `{formContainer}` βοηθά στην απόδοση των στοιχείων μέσα σε ένα κοντέινερ φόρμας. - -```latte -<p>Ποιες ειδήσεις θέλετε να λαμβάνετε:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Απόδοση χωρίς Latte -=================== - -Ο απλούστερος τρόπος για να αποδοθεί μια φόρμα είναι να καλέσετε: - -```php -$form->render(); -``` - -Μπορείτε να επηρεάσετε την εμφάνιση της φόρμας που αποδίδεται με αυτόν τον τρόπο διαμορφώνοντας τον [#Renderer] και τα [μεμονωμένα στοιχεία ελέγχου |#Χαρακτηριστικά HTML]. - - -Χειροκίνητη απόδοση -------------------- - -Κάθε στοιχείο φόρμας διαθέτει μεθόδους που παράγουν τον κώδικα HTML του πεδίου της φόρμας και της ετικέτας. Μπορούν να τον επιστρέψουν είτε ως συμβολοσειρά είτε ως αντικείμενο [Nette\Utils\Html |utils:html-elements]: - -- `getControl(): Html|string` επιστρέφει τον κώδικα HTML του στοιχείου -- `getLabel($caption = null): Html|string|null` επιστρέφει τον κώδικα HTML της ετικέτας, αν υπάρχει - -Έτσι, η φόρμα μπορεί να αποδοθεί ανά μεμονωμένο στοιχείο: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -Ενώ σε ορισμένα στοιχεία η `getControl()` επιστρέφει ένα μοναδικό στοιχείο HTML (π.χ. `<input>`, `<select>` κ.λπ.), σε άλλα επιστρέφει ένα ολόκληρο κομμάτι κώδικα HTML (CheckboxList, RadioList). Σε αυτή την περίπτωση, μπορείτε να χρησιμοποιήσετε μεθόδους που παράγουν μεμονωμένες εισόδους και ετικέτες, για κάθε στοιχείο ξεχωριστά: - -- `getControlPart($key = null): ?Html` επιστρέφει τον κώδικα HTML ενός μεμονωμένου στοιχείου -- `getLabelPart($key = null): ?Html` επιστρέφει τον κώδικα HTML της ετικέτας ενός μεμονωμένου στοιχείου - -.[note] -Αυτές οι μέθοδοι έχουν το πρόθεμα `get` για ιστορικούς λόγους, αλλά το `generate` θα ήταν καλύτερο, επειδή σε κάθε κλήση δημιουργούν και επιστρέφουν ένα νέο στοιχείο `Html`. - - -Renderer -======== - -Πρόκειται για ένα αντικείμενο που εξασφαλίζει την απόδοση της φόρμας. Μπορεί να οριστεί με τη μέθοδο `$form->setRenderer`. Ο έλεγχος του περνά όταν καλείται η μέθοδος `$form->render()`. - -Εάν δεν ορίσουμε δικό μας renderer, θα χρησιμοποιηθεί ο προεπιλεγμένος renderer [api:Nette\Forms\Rendering\DefaultFormRenderer]. Αυτός αποδίδει τα στοιχεία της φόρμας με τη μορφή πίνακα HTML. Η έξοδος μοιάζει κάπως έτσι: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Όνομα:</label></th> - - <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">Ηλικία:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Φύλο:</label></th> - ... -``` - -Το αν θα χρησιμοποιηθεί ή όχι πίνακας για τη δομή της φόρμας είναι αμφιλεγόμενο και πολλοί σχεδιαστές ιστοσελίδων προτιμούν άλλη σήμανση. Για παράδειγμα, μια λίστα ορισμών. Θα αναδιαμορφώσουμε λοιπόν τον `DefaultFormRenderer` έτσι ώστε να αποδώσει τη φόρμα με τη μορφή λίστας. Η διαμόρφωση γίνεται επεξεργαζόμενοι τον πίνακα [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Ο πρώτος δείκτης αντιπροσωπεύει πάντα την περιοχή και ο δεύτερος το χαρακτηριστικό της. Οι μεμονωμένες περιοχές απεικονίζονται στην εικόνα: - -[* defaultformrenderer.webp *] - -Από προεπιλογή, η ομάδα στοιχείων `controls` περιβάλλεται από έναν πίνακα `<table>`, κάθε `pair` αντιπροσωπεύει μια γραμμή του πίνακα `<tr>` και το ζεύγος `label` και `control` είναι κελιά `<th>` και `<td>`. Τώρα θα αλλάξουμε τα περιβάλλοντα στοιχεία. Θα εισαγάγουμε την περιοχή `controls` σε ένα container `<dl>`, την περιοχή `pair` θα την αφήσουμε χωρίς container, το `label` θα το εισαγάγουμε σε `<dt>` και τέλος το `control` θα το περιβάλλουμε με ετικέτες `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Το αποτέλεσμα είναι ο ακόλουθος κώδικας HTML: - -```latte -<dl> - <dt><label class="required" for="frm-name">Όνομα:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Ηλικία:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Φύλο:</label></dt> - ... -</dl> -``` - -Στον πίνακα wrappers μπορείτε να επηρεάσετε πολλά άλλα χαρακτηριστικά: - -- προσθήκη κλάσεων CSS σε μεμονωμένους τύπους στοιχείων φόρμας -- διάκριση μονών και ζυγών γραμμών με κλάση CSS -- οπτική διάκριση υποχρεωτικών και προαιρετικών στοιχείων -- καθορισμός αν τα μηνύματα σφάλματος θα εμφανίζονται απευθείας στα στοιχεία ή πάνω από τη φόρμα - - -Options -------- - -Η συμπεριφορά του Renderer μπορεί επίσης να ελεγχθεί ορίζοντας *options* στα μεμονωμένα στοιχεία της φόρμας. Με αυτόν τον τρόπο μπορείτε να ορίσετε την ετικέτα που θα εκτυπωθεί δίπλα στο πεδίο εισαγωγής: - -```php -$form->addText('phone', 'Αριθμός:') - ->setOption('description', 'Αυτός ο αριθμός θα παραμείνει κρυφός'); -``` - -Εάν θέλουμε να τοποθετήσουμε περιεχόμενο HTML σε αυτό, θα χρησιμοποιήσουμε την [κλάση Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Αριθμός:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Όροι διατήρησης του αριθμού σας</a>') - ); -``` - -.[tip] -Το στοιχείο Html μπορεί επίσης να χρησιμοποιηθεί αντί για ετικέτα: `$form->addCheckbox('conditions', $label)`. - - -Ομαδοποίηση στοιχείων ---------------------- - -Ο Renderer επιτρέπει την ομαδοποίηση στοιχείων σε οπτικές ομάδες (fieldsets): - -```php -$form->addGroup('Προσωπικά δεδομένα'); -``` - -Μετά τη δημιουργία μιας νέας ομάδας, αυτή γίνεται ενεργή και κάθε νεοεισερχόμενο στοιχείο προστίθεται ταυτόχρονα και σε αυτήν. Έτσι, η φόρμα μπορεί να κατασκευαστεί με αυτόν τον τρόπο: - -```php -$form = new Form; -$form->addGroup('Προσωπικά δεδομένα'); -$form->addText('name', 'Το όνομά σας:'); -$form->addInteger('age', 'Η ηλικία σας:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Διεύθυνση αποστολής'); -$form->addCheckbox('send', 'Αποστολή στη διεύθυνση'); -$form->addText('street', 'Οδός:'); -$form->addText('city', 'Πόλη:'); -$form->addSelect('country', 'Χώρα:', $countries); -``` - -Ο Renderer αποδίδει πρώτα τις ομάδες και μετά τα στοιχεία που δεν ανήκουν σε καμία ομάδα. - - -Υποστήριξη για Bootstrap ------------------------- - -[Στα παραδείγματα |https://github.com/nette/forms/tree/master/examples] θα βρείτε παραδείγματα για το πώς να διαμορφώσετε τον Renderer για [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] και [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -Χαρακτηριστικά HTML -=================== - -Για να ορίσετε οποιαδήποτε χαρακτηριστικά HTML για τα στοιχεία της φόρμας, χρησιμοποιούμε τη μέθοδο `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Αριθμός:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Ταξινόμηση κατά:', ['τιμής', 'ονόματος']) - ->setHtmlAttribute('onchange', 'submit()'); // υποβολή κατά την αλλαγή - - -// Για να ορίσετε χαρακτηριστικά για το ίδιο το <form> -$form->setHtmlAttribute('id', 'myForm'); -``` - -Προδιαγραφή του τύπου του στοιχείου: - -```php -$form->addText('tel', 'Το τηλέφωνό σας:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'γράψτε το τηλέφωνο'); -``` - -.[warning] -Η ρύθμιση του τύπου και άλλων χαρακτηριστικών χρησιμεύει μόνο για οπτικούς σκοπούς. Η επαλήθευση της ορθότητας των εισόδων πρέπει να γίνεται στον διακομιστή, πράγμα που εξασφαλίζετε επιλέγοντας το κατάλληλο [στοιχείο φόρμας |controls] και δηλώνοντας [κανόνες επικύρωσης |validation]. - -Μπορούμε να ορίσουμε χαρακτηριστικά HTML για μεμονωμένα στοιχεία σε λίστες radio ή checkbox με διαφορετικές τιμές για καθένα από αυτά. Παρατηρήστε την άνω και κάτω τελεία μετά το `style:`, η οποία εξασφαλίζει την επιλογή της τιμής βάσει κλειδιού: - -```php -$colors = ['r' => 'κόκκινο', 'g' => 'πράσινο', 'b' => 'μπλε']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Χρώματα:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Εκτυπώνει: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">κόκκινο</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">πράσινο</label> -<label><input type="checkbox" name="colors[]" value="b">μπλε</label> -``` - -Για να ορίσετε λογικά χαρακτηριστικά, όπως το `readonly`, μπορούμε να χρησιμοποιήσουμε τη σύνταξη με ερωτηματικό: - -```php -$form->addCheckboxList('colors', 'Χρώματα:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // για πολλαπλά κλειδιά χρησιμοποιήστε έναν πίνακα, π.χ. ['r', 'g'] -``` - -Εκτυπώνει: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">κόκκινο</label> -<label><input type="checkbox" name="colors[]" value="g">πράσινο</label> -<label><input type="checkbox" name="colors[]" value="b">μπλε</label> -``` - -Στην περίπτωση των selectbox, η μέθοδος `setHtmlAttribute()` ορίζει τα χαρακτηριστικά του στοιχείου `<select>`. Εάν θέλουμε να ορίσουμε χαρακτηριστικά για μεμονωμένα `<option>`, χρησιμοποιούμε τη μέθοδο `setOptionAttribute()`. Λειτουργούν επίσης οι συντάξεις με άνω και κάτω τελεία και ερωτηματικό που αναφέρθηκαν παραπάνω: - -```php -$form->addSelect('colors', 'Χρώματα:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Εκτυπώνει: - -```latte -<select name="colors"> - <option value="r" style="background:red">κόκκινο</option> - <option value="g" style="background:green">πράσινο</option> - <option value="b">μπλε</option> -</select> -``` - - -Πρωτότυπα ---------- - -Ένας εναλλακτικός τρόπος ορισμού των χαρακτηριστικών HTML συνίσταται στην τροποποίηση του προτύπου από το οποίο παράγεται το στοιχείο HTML. Το πρότυπο είναι ένα αντικείμενο `Html` και το επιστρέφει η μέθοδος `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Αριθμός:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -Με αυτόν τον τρόπο μπορείτε να τροποποιήσετε και το πρότυπο της ετικέτας, το οποίο επιστρέφει η `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -Στα στοιχεία Checkbox, CheckboxList και RadioList μπορείτε να επηρεάσετε το πρότυπο του στοιχείου που περιβάλλει ολόκληρο το στοιχείο. Το επιστρέφει η `getContainerPrototype()`. Στην προεπιλεγμένη κατάσταση, πρόκειται για ένα «κενό» στοιχείο, οπότε δεν αποδίδεται τίποτα, αλλά ορίζοντας του ένα όνομα, θα αποδίδεται: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -Στην περίπτωση των CheckboxList και RadioList μπορείτε να επηρεάσετε και το πρότυπο του διαχωριστή των μεμονωμένων στοιχείων, το οποίο επιστρέφει η μέθοδος `getSeparatorPrototype()`. Στην προεπιλεγμένη κατάσταση, είναι το στοιχείο `<br>`. Εάν το αλλάξετε σε ζευγαρωτό στοιχείο, θα περιβάλλει τα μεμονωμένα στοιχεία αντί να τα διαχωρίζει. Και επιπλέον, μπορείτε να επηρεάσετε το πρότυπο του στοιχείου HTML της ετικέτας στα μεμονωμένα στοιχεία, το οποίο επιστρέφει η `getItemLabelPrototype()`. - - -Μετάφραση -========= - -Εάν προγραμματίζετε μια πολύγλωσση εφαρμογή, πιθανότατα θα χρειαστείτε να αποδώσετε τη φόρμα σε διάφορες γλωσσικές εκδόσεις. Το Nette Framework ορίζει για αυτόν τον σκοπό μια διεπαφή για μετάφραση [api:Nette\Localization\Translator]. Στη Nette δεν υπάρχει προεπιλεγμένη υλοποίηση, μπορείτε να επιλέξετε ανάλογα με τις ανάγκες σας από διάφορες έτοιμες λύσεις που θα βρείτε στο [Componette |https://componette.org/search/localization]. Στην τεκμηρίωσή τους θα μάθετε πώς να διαμορφώσετε τον μεταφραστή. - -Οι φόρμες υποστηρίζουν την εκτύπωση κειμένων μέσω του μεταφραστή. Τον περνάμε σε αυτές χρησιμοποιώντας τη μέθοδο `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -Από αυτή τη στιγμή, όχι μόνο όλες οι ετικέτες, αλλά και όλα τα μηνύματα σφάλματος ή τα στοιχεία των πλαισίων επιλογής μεταφράζονται σε άλλη γλώσσα. - -Στα μεμονωμένα στοιχεία της φόρμας, είναι δυνατόν να οριστεί διαφορετικός μεταφραστής ή να απενεργοποιηθεί εντελώς η μετάφραση με την τιμή `null`: - -```php -$form->addSelect('carModel', 'Μοντέλο:', $cars) - ->setTranslator(null); -``` - -Στους [κανόνες επικύρωσης|validation], περνούν στον μεταφραστή και συγκεκριμένες παράμετροι, για παράδειγμα στον κανόνα: - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - ->addRule($form::MinLength, 'Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον %d χαρακτήρες', 8); -``` - -καλείται ο μεταφραστής με αυτές τις παραμέτρους: - -```php -$translator->translate('Ο κωδικός πρόσβασης πρέπει να έχει τουλάχιστον %d χαρακτήρες', 8); -``` - -και επομένως μπορεί να επιλέξει τη σωστή μορφή πληθυντικού για τη λέξη `χαρακτήρες` ανάλογα με τον αριθμό. - - -Το συμβάν onRender -================== - -Λίγο πριν αποδοθεί η φόρμα, μπορούμε να αφήσουμε να κληθεί ο κώδικάς μας. Αυτός μπορεί, για παράδειγμα, να συμπληρώσει τα στοιχεία της φόρμας με κλάσεις HTML για σωστή εμφάνιση. Προσθέτουμε τον κώδικα στον πίνακα `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/el/standalone.texy b/forms/el/standalone.texy deleted file mode 100644 index b660fa0178..0000000000 --- a/forms/el/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Αυτόνομες Φόρμες -**************** - -.[perex] -Οι φόρμες Nette διευκολύνουν κατά τάξεις μεγέθους τη δημιουργία και την επεξεργασία φορμών ιστού. Μπορείτε να τις χρησιμοποιήσετε στις εφαρμογές σας εντελώς ανεξάρτητα από το υπόλοιπο framework, όπως θα δείξουμε σε αυτό το κεφάλαιο. - -Ωστόσο, εάν χρησιμοποιείτε το Nette Application και presenters, ο οδηγός για [χρήση σε presenters|in-presenter] είναι για εσάς. - - -Πρώτη Φόρμα -=========== - -Ας προσπαθήσουμε να γράψουμε μια απλή φόρμα εγγραφής. Ο κώδικάς της θα είναι ο ακόλουθος ("πλήρης κώδικας":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Όνομα:'); -$form->addPassword('password', 'Κωδικός πρόσβασης:'); -$form->addSubmit('send', 'Εγγραφή'); -``` - -Μπορούμε να την αποδώσουμε πολύ εύκολα: - -```php -$form->render(); -``` - -και θα εμφανιστεί στον περιηγητή ως εξής: - -[* form-cs.webp *] - -Η φόρμα είναι ένα αντικείμενο της κλάσης `Nette\Forms\Form` (η κλάση `Nette\Application\UI\Form` χρησιμοποιείται σε presenters). Προσθέσαμε σε αυτή τα λεγόμενα στοιχεία: όνομα, κωδικό πρόσβασης και ένα κουμπί υποβολής. - -Τώρα, ας ζωντανέψουμε τη φόρμα. Ρωτώντας `$form->isSuccess()`, θα μάθουμε αν η φόρμα υποβλήθηκε και αν συμπληρώθηκε έγκυρα. Αν ναι, θα εμφανίσουμε τα δεδομένα. Έτσι, μετά τον ορισμό της φόρμας, προσθέτουμε: - -```php -if ($form->isSuccess()) { - echo 'Η φόρμα υποβλήθηκε και επικυρώθηκε με επιτυχία'; - $data = $form->getValues(); - // το $data->name περιέχει το όνομα - // το $data->password περιέχει τον κωδικό πρόσβασης - var_dump($data); -} -``` - -Η μέθοδος `getValues()` επιστρέφει τα υποβληθέντα δεδομένα με τη μορφή ενός αντικειμένου [ArrayHash |utils:arrays#ArrayHash]. Θα δείξουμε πώς να το αλλάξουμε [αργότερα |#Αντιστοίχιση σε Κλάσεις]. Το αντικείμενο `$data` περιέχει τα κλειδιά `name` και `password` με τα δεδομένα που συμπλήρωσε ο χρήστης. - -Συνήθως, στέλνουμε τα δεδομένα απευθείας για περαιτέρω επεξεργασία, η οποία μπορεί να είναι, για παράδειγμα, η εισαγωγή σε μια βάση δεδομένων. Ωστόσο, κατά την επεξεργασία, μπορεί να προκύψει σφάλμα, όπως ένα όνομα χρήστη που είναι ήδη κατειλημμένο. Σε αυτή την περίπτωση, επιστρέφουμε το σφάλμα στη φόρμα χρησιμοποιώντας το `addError()` και την αφήνουμε να αποδοθεί ξανά, μαζί με το μήνυμα σφάλματος. - -```php -$form->addError('Συγγνώμη, αυτό το όνομα χρήστη χρησιμοποιείται ήδη.'); -``` - -Μετά την επεξεργασία της φόρμας, ανακατευθύνουμε σε άλλη σελίδα. Αυτό αποτρέπει την ακούσια επανυποβολή της φόρμας με το κουμπί *ανανέωση*, *πίσω* ή με την κίνηση στο ιστορικό του προγράμματος περιήγησης. - -Η φόρμα υποβάλλεται από προεπιλογή χρησιμοποιώντας τη μέθοδο POST στην ίδια σελίδα. Και τα δύο μπορούν να αλλάξουν: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -Και αυτό είναι όλο :-) Έχουμε μια λειτουργική και τέλεια [ασφαλή |#Προστασία από Ευπάθειες] φόρμα. - -Προσπαθήστε να προσθέσετε και άλλα [στοιχεία φόρμας|controls]. - - -Πρόσβαση στα Στοιχεία -===================== - -Ονομάζουμε τη φόρμα και τα μεμονωμένα στοιχεία της components. Σχηματίζουν ένα δέντρο components, όπου η ρίζα είναι η φόρμα. Μπορούμε να αποκτήσουμε πρόσβαση στα μεμονωμένα στοιχεία της φόρμας ως εξής: - -```php -$input = $form->getComponent('name'); -// εναλλακτική σύνταξη: $input = $form['name']; - -$button = $form->getComponent('send'); -// εναλλακτική σύνταξη: $button = $form['send']; -``` - -Τα στοιχεία αφαιρούνται χρησιμοποιώντας το unset: - -```php -unset($form['name']); -``` - - -Κανόνες Επικύρωσης -================== - -Αναφέραμε τη λέξη *έγκυρη*, αλλά η φόρμα δεν έχει ακόμη κανόνες επικύρωσης. Ας το διορθώσουμε αυτό. - -Το όνομα θα είναι υποχρεωτικό, οπότε θα το επισημάνουμε με τη μέθοδο `setRequired()`. Το όρισμά της είναι το κείμενο του μηνύματος σφάλματος που θα εμφανιστεί εάν ο χρήστης δεν συμπληρώσει το όνομα. Εάν δεν παρέχουμε όρισμα, θα χρησιμοποιηθεί το προεπιλεγμένο μήνυμα σφάλματος. - -```php -$form->addText('name', 'Όνομα:') - ->setRequired('Παρακαλώ εισάγετε το όνομά σας.'); -``` - -Προσπαθήστε να υποβάλετε τη φόρμα χωρίς να συμπληρώσετε το όνομα και θα δείτε ότι εμφανίζεται ένα μήνυμα σφάλματος, και ο περιηγητής ή ο διακομιστής θα αρνηθεί να την αποδεχτεί μέχρι να συμπληρώσετε το πεδίο. - -Ταυτόχρονα, δεν μπορείτε να εξαπατήσετε το σύστημα πληκτρολογώντας μόνο κενά στο πεδίο. Όχι. Το Nette αφαιρεί αυτόματα τα αρχικά και τα τελικά κενά. Δοκιμάστε το. Είναι κάτι που πρέπει πάντα να κάνετε με κάθε input μίας γραμμής, αλλά συχνά ξεχνιέται. Το Nette το κάνει αυτόματα. (Μπορείτε να προσπαθήσετε να εξαπατήσετε τη φόρμα στέλνοντας μια συμβολοσειρά πολλαπλών γραμμών ως όνομα. Ακόμα και εδώ, το Nette δεν θα ξεγελαστεί και θα αλλάξει τις αλλαγές γραμμής σε κενά.) - -Η φόρμα επικυρώνεται πάντα από την πλευρά του διακομιστή, αλλά δημιουργείται επίσης επικύρωση JavaScript, η οποία εκτελείται αμέσως και ο χρήστης ενημερώνεται αμέσως για το σφάλμα, χωρίς να χρειάζεται να υποβάλει τη φόρμα στον διακομιστή. Αυτό γίνεται από το σενάριο `netteForms.js`. Εισάγετέ το στη σελίδα: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Αν κοιτάξετε τον πηγαίο κώδικα της σελίδας με τη φόρμα, μπορείτε να παρατηρήσετε ότι το Nette εισάγει τα υποχρεωτικά στοιχεία σε στοιχεία με την κλάση CSS `required`. Προσπαθήστε να προσθέσετε το ακόλουθο φύλλο στυλ στο πρότυπο και η ετικέτα «Όνομα» θα είναι κόκκινη. Με αυτόν τον τρόπο, επισημαίνουμε κομψά τα υποχρεωτικά στοιχεία για τους χρήστες: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Προσθέτουμε περαιτέρω κανόνες επικύρωσης χρησιμοποιώντας τη μέθοδο `addRule()`. Η πρώτη παράμετρος είναι ο κανόνας, η δεύτερη είναι ξανά το κείμενο του μηνύματος σφάλματος, και μπορεί να ακολουθήσει ένα όρισμα κανόνα επικύρωσης. Τι σημαίνει αυτό; - -Θα επεκτείνουμε τη φόρμα με ένα νέο προαιρετικό πεδίο «ηλικία», το οποίο πρέπει να είναι ακέραιος αριθμός (`addInteger()`) και επιπλέον εντός επιτρεπόμενου εύρους (`$form::Range`). Και εδώ θα χρησιμοποιήσουμε την τρίτη παράμετρο της μεθόδου `addRule()`, με την οποία περνάμε το απαιτούμενο εύρος στον επικυρωτή ως ζεύγος `[από, έως]`: - -```php -$form->addInteger('age', 'Ηλικία:') - ->addRule($form::Range, 'Η ηλικία πρέπει να είναι μεταξύ 18 και 120.', [18, 120]); -``` - -.[tip] -Εάν ο χρήστης δεν συμπληρώσει το πεδίο, οι κανόνες επικύρωσης δεν θα ελεγχθούν, καθώς το στοιχείο είναι προαιρετικό. - -Εδώ υπάρχει περιθώριο για μια μικρή αναδιάρθρωση. Στο μήνυμα σφάλματος και στην τρίτη παράμετρο, οι αριθμοί αναφέρονται διπλά, κάτι που δεν είναι ιδανικό. Εάν δημιουργούσαμε [πολύγλωσσες φόρμες |rendering#Μετάφραση] και το μήνυμα που περιέχει αριθμούς μεταφραζόταν σε πολλές γλώσσες, θα ήταν δύσκολο να αλλάξουμε τις τιμές αργότερα. Για το λόγο αυτό, είναι δυνατό να χρησιμοποιηθούν σύμβολα κράτησης θέσης `%d`, και το Nette θα συμπληρώσει τις τιμές: - -```php - ->addRule($form::Range, 'Η ηλικία πρέπει να είναι μεταξύ %d και %d.', [18, 120]); -``` - -Ας επιστρέψουμε στο στοιχείο `password`, το οποίο θα κάνουμε επίσης υποχρεωτικό και θα ελέγξουμε επίσης το ελάχιστο μήκος του κωδικού πρόσβασης (`$form::MinLength`), χρησιμοποιώντας ξανά ένα σύμβολο κράτησης θέσης: - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - ->setRequired('Επιλέξτε έναν κωδικό πρόσβασης.') - ->addRule($form::MinLength, 'Ο κωδικός πρόσβασης πρέπει να έχει μήκος τουλάχιστον %d χαρακτήρων.', 8); -``` - -Θα προσθέσουμε ένα πεδίο `passwordVerify` στη φόρμα, όπου ο χρήστης εισάγει ξανά τον κωδικό πρόσβασης για επαλήθευση. Χρησιμοποιώντας κανόνες επικύρωσης, θα ελέγξουμε αν οι δύο κωδικοί πρόσβασης είναι ίδιοι (`$form::Equal`). Και ως παράμετρο, θα δώσουμε μια αναφορά στον πρώτο κωδικό πρόσβασης χρησιμοποιώντας [τετράγωνες αγκύλες |#Πρόσβαση στα Στοιχεία]: - -```php -$form->addPassword('passwordVerify', 'Κωδικός πρόσβασης για έλεγχο:') - ->setRequired('Παρακαλώ εισάγετε ξανά τον κωδικό πρόσβασης για έλεγχο.') - ->addRule($form::Equal, 'Οι κωδικοί πρόσβασης δεν ταιριάζουν.', $form['password']) - ->setOmitted(); -``` - -Χρησιμοποιώντας το `setOmitted()`, επισημάναμε ένα στοιχείο του οποίου η τιμή δεν μας ενδιαφέρει πραγματικά και το οποίο υπάρχει μόνο για λόγους επικύρωσης. Η τιμή δεν περνά στο `$data`. - -Με αυτό, έχουμε μια πλήρως λειτουργική φόρμα με επικύρωση τόσο σε PHP όσο και σε JavaScript. Οι δυνατότητες επικύρωσης του Nette είναι πολύ ευρύτερες. Μπορείτε να δημιουργήσετε συνθήκες, να εμφανίσετε και να αποκρύψετε τμήματα της σελίδας με βάση αυτές, κ.λπ. Θα μάθετε τα πάντα στο κεφάλαιο για την [επικύρωση φορμών|validation]. - - -Προεπιλεγμένες Τιμές -==================== - -Συνήθως ορίζουμε προεπιλεγμένες τιμές για τα στοιχεία της φόρμας: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Συχνά είναι χρήσιμο να ορίζουμε προεπιλεγμένες τιμές για όλα τα στοιχεία ταυτόχρονα. Για παράδειγμα, όταν η φόρμα χρησιμοποιείται για την επεξεργασία εγγραφών. Διαβάζουμε την εγγραφή από τη βάση δεδομένων και ορίζουμε τις προεπιλεγμένες τιμές: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Καλέστε το `setDefaults()` μετά τον ορισμό των στοιχείων. - - -Απόδοση Φόρμας -============== - -Από προεπιλογή, η φόρμα αποδίδεται ως πίνακας. Τα μεμονωμένα στοιχεία πληρούν τον βασικό κανόνα προσβασιμότητας - όλες οι ετικέτες γράφονται ως `<label>` και συνδέονται με το αντίστοιχο στοιχείο της φόρμας. Όταν κάνετε κλικ στην ετικέτα, ο κέρσορας εμφανίζεται αυτόματα στο πεδίο της φόρμας. - -Μπορούμε να ορίσουμε οποιαδήποτε χαρακτηριστικά HTML για κάθε στοιχείο. Για παράδειγμα, προσθέστε ένα placeholder: - -```php -$form->addInteger('age', 'Ηλικία:') - ->setHtmlAttribute('placeholder', 'Παρακαλώ συμπληρώστε την ηλικία σας'); -``` - -Υπάρχουν πραγματικά πολλοί τρόποι για την απόδοση μιας φόρμας, οπότε υπάρχει ένα [ξεχωριστό κεφάλαιο για την απόδοση|rendering] αφιερωμένο σε αυτό. - - -Αντιστοίχιση σε Κλάσεις -======================= - -Ας επιστρέψουμε στην επεξεργασία των δεδομένων της φόρμας. Η μέθοδος `getValues()` μας επέστρεψε τα υποβληθέντα δεδομένα ως αντικείμενο `ArrayHash`. Επειδή πρόκειται για μια γενική κλάση, κάτι σαν `stdClass`, θα μας λείψει κάποια άνεση όταν εργαζόμαστε με αυτήν, όπως η αυτόματη συμπλήρωση ιδιοτήτων στους επεξεργαστές ή η στατική ανάλυση κώδικα. Αυτό θα μπορούσε να λυθεί έχοντας μια συγκεκριμένη κλάση για κάθε φόρμα, της οποίας οι ιδιότητες αντιπροσωπεύουν τα μεμονωμένα στοιχεία. Π.χ.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Εναλλακτικά, μπορείτε να χρησιμοποιήσετε έναν κατασκευαστή: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Οι ιδιότητες της κλάσης δεδομένων μπορούν επίσης να είναι enum και θα αντιστοιχιστούν αυτόματα. .{data-version:3.2.4} - -Πώς να πούμε στο Nette να επιστρέψει τα δεδομένα ως αντικείμενα αυτής της κλάσης; Πιο εύκολα από ό,τι νομίζετε. Απλά δώστε το όνομα της κλάσης ή το αντικείμενο για ενυδάτωση ως παράμετρο: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Η παράμετρος μπορεί επίσης να είναι `'array'`, και τότε τα δεδομένα θα επιστραφούν ως πίνακας. - -Εάν οι φόρμες σχηματίζουν μια πολυεπίπεδη δομή που αποτελείται από containers, δημιουργήστε μια ξεχωριστή κλάση για κάθε μία: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Η αντιστοίχιση θα αναγνωρίσει τότε από τον τύπο της ιδιότητας `$person` ότι πρέπει να αντιστοιχίσει το container στην κλάση `PersonFormData`. Εάν η ιδιότητα περιείχε έναν πίνακα από containers, καθορίστε τον τύπο `array` και περάστε την κλάση για αντιστοίχιση απευθείας στο container: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Μπορείτε να δημιουργήσετε το σχέδιο της κλάσης δεδομένων της φόρμας χρησιμοποιώντας τη μέθοδο `Nette\Forms\Blueprint::dataClass($form)`, η οποία θα το εκτυπώσει στη σελίδα του προγράμματος περιήγησης. Στη συνέχεια, απλά επιλέξτε τον κώδικα με κλικ και αντιγράψτε τον στο έργο σας. .{data-version:3.1.15} - - -Πολλαπλά Κουμπιά -================ - -Εάν η φόρμα έχει περισσότερα από ένα κουμπιά, συνήθως πρέπει να διακρίνουμε ποιο από αυτά πατήθηκε. Η μέθοδος `isSubmittedBy()` του κουμπιού θα μας επιστρέψει αυτή την πληροφορία: - -```php -$form->addSubmit('save', 'Αποθήκευση'); -$form->addSubmit('delete', 'Διαγραφή'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // αποθήκευση δεδομένων - } - - if ($form['delete']->isSubmittedBy()) { - // διαγραφή δεδομένων - } -} -``` - -Μην παραλείψετε την ερώτηση `$form->isSuccess()`, καθώς επαληθεύει την εγκυρότητα των δεδομένων. - -Όταν μια φόρμα υποβάλλεται πατώντας το πλήκτρο <kbd>Enter</kbd>, θεωρείται ότι υποβλήθηκε από το πρώτο κουμπί. - - -Προστασία από Ευπάθειες -======================= - -Το Nette Framework δίνει μεγάλη έμφαση στην ασφάλεια και, ως εκ τούτου, φροντίζει σχολαστικά για την καλή ασφάλεια των φορμών. - -Εκτός από την προστασία των φορμών από επιθέσεις [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] και [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], εφαρμόζει πολλές μικρές προστασίες για τις οποίες δεν χρειάζεται πλέον να ανησυχείτε. - -Για παράδειγμα, φιλτράρει όλους τους χαρακτήρες ελέγχου από την είσοδο και επαληθεύει την εγκυρότητα της κωδικοποίησης UTF-8, έτσι ώστε τα δεδομένα από τη φόρμα να είναι πάντα καθαρά. Για τα πλαίσια επιλογής και τις λίστες radio, επαληθεύει ότι τα επιλεγμένα στοιχεία ήταν πράγματι από τα προσφερόμενα και ότι δεν υπήρξε πλαστογράφηση. Έχουμε ήδη αναφέρει ότι αφαιρεί τους χαρακτήρες τέλους γραμμής από τις εισόδους κειμένου μίας γραμμής, τους οποίους θα μπορούσε να στείλει ένας εισβολέας. Για τις εισόδους πολλαπλών γραμμών, κανονικοποιεί τους χαρακτήρες τέλους γραμμής. Και ούτω καθεξής. - -Το Nette αντιμετωπίζει για εσάς κινδύνους ασφαλείας που πολλοί προγραμματιστές δεν γνωρίζουν καν ότι υπάρχουν. - -Η προαναφερθείσα επίθεση CSRF συνίσταται στο ότι ο εισβολέας δελεάζει το θύμα σε μια σελίδα που εκτελεί διακριτικά ένα αίτημα στον διακομιστή στον οποίο είναι συνδεδεμένο το θύμα, στο πρόγραμμα περιήγησης του θύματος, και ο διακομιστής πιστεύει ότι το αίτημα εκτελέστηκε από το θύμα με δική του βούληση. Επομένως, το Nette αποτρέπει την υποβολή μιας φόρμας POST από άλλο domain. Εάν, για κάποιο λόγο, θέλετε να απενεργοποιήσετε την προστασία και να επιτρέψετε την υποβολή της φόρμας από άλλο domain, χρησιμοποιήστε: - -```php -$form->allowCrossOrigin(); // ΠΡΟΣΟΧΗ! Απενεργοποιεί την προστασία! -``` - -Αυτή η προστασία χρησιμοποιεί ένα SameSite cookie με όνομα `_nss`. Επομένως, δημιουργήστε το αντικείμενο της φόρμας πριν στείλετε την πρώτη έξοδο, ώστε το cookie να μπορεί να σταλεί. - -Η προστασία με SameSite cookie μπορεί να μην είναι 100% αξιόπιστη, οπότε συνιστάται να ενεργοποιήσετε επίσης την προστασία με token: - -```php -$form->addProtection(); -``` - -Συνιστούμε την προστασία των φορμών στο διαχειριστικό τμήμα του ιστότοπου που τροποποιούν ευαίσθητα δεδομένα στην εφαρμογή με αυτόν τον τρόπο. Το framework αμύνεται έναντι επιθέσεων CSRF δημιουργώντας και επαληθεύοντας ένα token εξουσιοδότησης που αποθηκεύεται στο session. Επομένως, είναι απαραίτητο να έχετε ανοιχτό το session πριν εμφανίσετε τη φόρμα. Στο διαχειριστικό τμήμα του ιστότοπου, το session συνήθως έχει ήδη ξεκινήσει λόγω της σύνδεσης του χρήστη. Διαφορετικά, ξεκινήστε το session με τη μέθοδο `Nette\Http\Session::start()`. - -Λοιπόν, αυτή ήταν μια γρήγορη εισαγωγή στις φόρμες στο Nette. Προσπαθήστε να ρίξετε μια ματιά στον κατάλογο [examples|https://github.com/nette/forms/tree/master/examples] στη διανομή, όπου θα βρείτε περισσότερη έμπνευση. diff --git a/forms/el/validation.texy b/forms/el/validation.texy deleted file mode 100644 index b17739b2b7..0000000000 --- a/forms/el/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Επικύρωση Φορμών -**************** - - -Υποχρεωτικά Στοιχεία -==================== - -Επισημαίνουμε τα υποχρεωτικά στοιχεία με τη μέθοδο `setRequired()`. Το όρισμά της είναι το κείμενο του [μηνύματος σφάλματος |#Μηνύματα Σφάλματος] που θα εμφανιστεί εάν ο χρήστης δεν συμπληρώσει το στοιχείο. Εάν δεν παρέχουμε όρισμα, θα χρησιμοποιηθεί το προεπιλεγμένο μήνυμα σφάλματος. - -```php -$form->addText('name', 'Όνομα:') - ->setRequired('Παρακαλώ εισάγετε το όνομά σας.'); -``` - - -Κανόνες -======= - -Προσθέτουμε κανόνες επικύρωσης στα στοιχεία χρησιμοποιώντας τη μέθοδο `addRule()`. Η πρώτη παράμετρος είναι ο κανόνας, η δεύτερη είναι το κείμενο του [μηνύματος σφάλματος |#Μηνύματα Σφάλματος] και η τρίτη είναι το όρισμα του κανόνα επικύρωσης. - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - ->addRule($form::MinLength, 'Ο κωδικός πρόσβασης πρέπει να έχει μήκος τουλάχιστον %d χαρακτήρων.', 8); -``` - -**Οι κανόνες επικύρωσης ελέγχονται μόνο εάν ο χρήστης συμπληρώσει το στοιχείο.** - -Το Nette έρχεται με μια σειρά προκαθορισμένων κανόνων, των οποίων τα ονόματα είναι σταθερές της κλάσης `Nette\Forms\Form`. Μπορούμε να χρησιμοποιήσουμε αυτούς τους κανόνες για όλα τα στοιχεία: - -| σταθερά | περιγραφή | τύπος ορίσματος -|------- -| `Required` | υποχρεωτικό στοιχείο, ψευδώνυμο για `setRequired()` | - -| `Filled` | υποχρεωτικό στοιχείο, ψευδώνυμο για `setRequired()` | - -| `Blank` | το στοιχείο δεν πρέπει να συμπληρωθεί | - -| `Equal` | η τιμή είναι ίση με την παράμετρο | `mixed` -| `NotEqual` | η τιμή δεν είναι ίση με την παράμετρο | `mixed` -| `IsIn` | η τιμή είναι ίση με ένα από τα στοιχεία του πίνακα | `array` -| `IsNotIn` | η τιμή δεν είναι ίση με κανένα στοιχείο του πίνακα | `array` -| `Valid` | είναι το στοιχείο συμπληρωμένο σωστά; (για [#συνθήκες]) | - - - -Είσοδοι Κειμένου ----------------- - -Για τα στοιχεία `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()`, μπορούν επίσης να χρησιμοποιηθούν ορισμένοι από τους ακόλουθους κανόνες: - -| `MinLength` | ελάχιστο μήκος κειμένου | `int` -| `MaxLength` | μέγιστο μήκος κειμένου | `int` -| `Length` | μήκος εντός εύρους ή ακριβές μήκος | ζεύγος `[int, int]` ή `int` -| `Email` | έγκυρη διεύθυνση email | - -| `URL` | απόλυτο URL | - -| `Pattern` | ταιριάζει με την κανονική έκφραση | `string` -| `PatternInsensitive` | όπως το `Pattern`, αλλά χωρίς διάκριση πεζών-κεφαλαίων | `string` -| `Integer` | ακέραια τιμή | - -| `Numeric` | ψευδώνυμο για `Integer` | - -| `Float` | αριθμός | - -| `Min` | ελάχιστη τιμή αριθμητικού στοιχείου | `int\|float` -| `Max` | μέγιστη τιμή αριθμητικού στοιχείου | `int\|float` -| `Range` | τιμή εντός εύρους | ζεύγος `[int\|float, int\|float]` - -Οι κανόνες επικύρωσης `Integer`, `Numeric` και `Float` μετατρέπουν αμέσως την τιμή σε ακέραιο ή δεκαδικό, αντίστοιχα. Επιπλέον, ο κανόνας `URL` δέχεται επίσης μια διεύθυνση χωρίς σχήμα (π.χ. `nette.org`) και προσθέτει το σχήμα (`https://nette.org`). Η έκφραση στο `Pattern` και το `PatternIcase` πρέπει να ισχύει για ολόκληρη την τιμή, δηλαδή σαν να ήταν περικλεισμένη από τους χαρακτήρες `^` και `$`. - - -Αριθμός Στοιχείων ------------------ - -Για τα στοιχεία `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()`, μπορούν επίσης να χρησιμοποιηθούν οι ακόλουθοι κανόνες για τον περιορισμό του αριθμού των επιλεγμένων στοιχείων ή των ανεβασμένων αρχείων, αντίστοιχα: - -| `MinLength` | ελάχιστος αριθμός | `int` -| `MaxLength` | μέγιστος αριθμός | `int` -| `Length` | αριθμός εντός εύρους ή ακριβής αριθμός | ζεύγος `[int, int]` ή `int` - - -Ανέβασμα Αρχείων ----------------- - -Για τα στοιχεία `addUpload()`, `addMultiUpload()`, μπορούν επίσης να χρησιμοποιηθούν οι ακόλουθοι κανόνες: - -| `MaxFileSize` | μέγιστο μέγεθος αρχείου σε bytes | `int` -| `MimeType` | Τύπος MIME, επιτρέπονται χαρακτήρες μπαλαντέρ (`'video/*'`) | `string\|string[]` -| `Image` | εικόνα JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | το όνομα αρχείου ταιριάζει με την κανονική έκφραση | `string` -| `PatternInsensitive` | όπως το `Pattern`, αλλά χωρίς διάκριση πεζών-κεφαλαίων | `string` - -Τα `MimeType` και `Image` απαιτούν την επέκταση PHP `fileinfo`. Το αν ένα αρχείο ή μια εικόνα είναι του απαιτούμενου τύπου ανιχνεύεται με βάση την υπογραφή του και **δεν επαληθεύει την ακεραιότητα ολόκληρου του αρχείου.** Το αν μια εικόνα είναι κατεστραμμένη μπορεί να προσδιοριστεί, για παράδειγμα, προσπαθώντας να την [φορτώσετε |http:request#toImage]. - - -Μηνύματα Σφάλματος -================== - -Όλοι οι προκαθορισμένοι κανόνες, εκτός από τα `Pattern` και `PatternInsensitive`, έχουν ένα προεπιλεγμένο μήνυμα σφάλματος, οπότε μπορεί να παραλειφθεί. Ωστόσο, καθορίζοντας και διατυπώνοντας όλα τα μηνύματα κατά παραγγελία, θα κάνετε τη φόρμα πιο φιλική προς τον χρήστη. - -Μπορείτε να αλλάξετε τα προεπιλεγμένα μηνύματα στην [διαμόρφωση|forms:configuration], επεξεργαζόμενοι τα κείμενα στον πίνακα `Nette\Forms\Validator::$messages`, ή χρησιμοποιώντας έναν [μεταφραστή |rendering#Μετάφραση]. - -Οι ακόλουθες συμβολοσειρές κράτησης θέσης μπορούν να χρησιμοποιηθούν στο κείμενο των μηνυμάτων σφάλματος: - -| `%d` | αντικαθίσταται διαδοχικά από τα ορίσματα του κανόνα -| `%n$d` | αντικαθίσταται από το n-οστό όρισμα του κανόνα -| `%label` | αντικαθίσταται από την ετικέτα του στοιχείου (χωρίς την άνω και κάτω τελεία) -| `%name` | αντικαθίσταται από το όνομα του στοιχείου (π.χ. `name`) -| `%value` | αντικαθίσταται από την τιμή που εισήγαγε ο χρήστης - -```php -$form->addText('name', 'Όνομα:') - ->setRequired('Παρακαλώ συμπληρώστε το %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'τουλάχιστον %d και το πολύ %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'το πολύ %2$d και τουλάχιστον %1$d', [5, 10]); -``` - - -Συνθήκες -======== - -Εκτός από τους κανόνες, μπορούν επίσης να προστεθούν συνθήκες. Γράφονται παρόμοια με τους κανόνες, αλλά αντί για `addRule()`, χρησιμοποιούμε τη μέθοδο `addCondition()`, και φυσικά, δεν παρέχουμε κανένα μήνυμα σφάλματος (η συνθήκη απλώς ρωτά): - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης:') - // εάν ο κωδικός πρόσβασης δεν είναι μεγαλύτερος από 8 χαρακτήρες - ->addCondition($form::MaxLength, 8) - // τότε πρέπει να περιέχει ένα ψηφίο - ->addRule($form::Pattern, 'Πρέπει να περιέχει ένα ψηφίο.', '.*[0-9].*'); -``` - -Η συνθήκη μπορεί επίσης να συνδεθεί με ένα στοιχείο διαφορετικό από το τρέχον χρησιμοποιώντας το `addConditionOn()`. Ως πρώτη παράμετρο, παρέχουμε μια αναφορά στο στοιχείο. Σε αυτό το παράδειγμα, το email θα είναι υποχρεωτικό μόνο εάν το πλαίσιο ελέγχου είναι επιλεγμένο (η τιμή του θα είναι true): - -```php -$form->addCheckbox('newsletters', 'στείλτε μου ενημερωτικά δελτία'); - -$form->addEmail('email', 'E-mail:') - // εάν το πλαίσιο ελέγχου είναι επιλεγμένο - ->addConditionOn($form['newsletters'], $form::Equal, true) - // τότε απαιτήστε το email - ->setRequired('Παρακαλώ εισάγετε τη διεύθυνση email σας.'); -``` - -Μπορείτε να δημιουργήσετε σύνθετες δομές από συνθήκες χρησιμοποιώντας τα `elseCondition()` και `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // εάν η πρώτη συνθήκη πληρούται - ->addConditionOn(/* ... */) // και η δεύτερη συνθήκη σε άλλο στοιχείο - ->addRule(/* ... */) // απαιτήστε αυτόν τον κανόνα - ->elseCondition() // εάν η δεύτερη συνθήκη δεν πληρούται - ->addRule(/* ... */) // απαιτήστε αυτούς τους κανόνες - ->addRule(/* ... */) - ->endCondition() // επιστρέφουμε στην πρώτη συνθήκη - ->addRule(/* ... */); -``` - -Στο Nette, είναι πολύ εύκολο να αντιδράσετε στην εκπλήρωση ή μη εκπλήρωση μιας συνθήκης επίσης στην πλευρά του JavaScript χρησιμοποιώντας τη μέθοδο `toggle()`, δείτε [#δυναμικό-javascript]. - - -Αναφορά σε Άλλο Στοιχείο -======================== - -Ένα άλλο στοιχείο φόρμας μπορεί επίσης να περάσει ως όρισμα σε έναν κανόνα ή μια συνθήκη. Ο κανόνας θα χρησιμοποιήσει τότε την τιμή που εισήγαγε αργότερα ο χρήστης στο πρόγραμμα περιήγησης. Με αυτόν τον τρόπο, μπορείτε, για παράδειγμα, να επικυρώσετε δυναμικά ότι το στοιχείο `password` περιέχει την ίδια συμβολοσειρά με το στοιχείο `password_confirm`: - -```php -$form->addPassword('password', 'Κωδικός πρόσβασης'); -$form->addPassword('password_confirm', 'Επιβεβαιώστε τον κωδικό πρόσβασης') - ->addRule($form::Equal, 'Οι κωδικοί πρόσβασης δεν ταιριάζουν.', $form['password']); -``` - - -Προσαρμοσμένοι Κανόνες και Συνθήκες -=================================== - -Μερικές φορές βρισκόμαστε σε μια κατάσταση όπου οι ενσωματωμένοι κανόνες επικύρωσης στο Nette δεν είναι αρκετοί και πρέπει να επικυρώσουμε τα δεδομένα του χρήστη με τον δικό μας τρόπο. Στο Nette, αυτό είναι πολύ εύκολο! - -Μπορείτε να περάσετε οποιαδήποτε επανάκληση ως πρώτη παράμετρο στις μεθόδους `addRule()` ή `addCondition()`. Η επανάκληση δέχεται το ίδιο το στοιχείο ως πρώτη παράμετρο και επιστρέφει μια boolean τιμή που υποδεικνύει εάν η επικύρωση ήταν επιτυχής. Κατά την προσθήκη ενός κανόνα χρησιμοποιώντας το `addRule()`, είναι επίσης δυνατό να καθοριστούν πρόσθετα ορίσματα, τα οποία στη συνέχεια περνούν ως δεύτερη παράμετρος. - -Μπορούμε να δημιουργήσουμε το δικό μας σύνολο επικυρωτών ως κλάση με στατικές μεθόδους: - -```php -class MyValidators -{ - // ελέγχει εάν η τιμή είναι διαιρετή από το όρισμα - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // άλλοι επικυρωτές - } -} -``` - -Η χρήση είναι τότε πολύ απλή: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Η τιμή πρέπει να είναι πολλαπλάσιο του %d.', - 8, - ); -``` - -Προσαρμοσμένοι κανόνες επικύρωσης μπορούν επίσης να προστεθούν στο JavaScript. Η προϋπόθεση είναι ότι ο κανόνας είναι μια στατική μέθοδος. Το όνομά του για τον επικυρωτή JavaScript δημιουργείται συνδυάζοντας το όνομα της κλάσης χωρίς ανάστροφες καθέτους `\`, μια κάτω παύλα `_` και το όνομα της μεθόδου. Για παράδειγμα, το `App\MyValidators::validateDivisibility` γράφεται ως `AppMyValidators_validateDivisibility` και προστίθεται στο αντικείμενο `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Συμβάν onValidate -================= - -Μετά την υποβολή της φόρμας, πραγματοποιείται επικύρωση, όπου ελέγχονται οι μεμονωμένοι κανόνες που προστέθηκαν χρησιμοποιώντας το `addRule()`, και στη συνέχεια ενεργοποιείται το [συμβάν |nette:glossary#Events] `onValidate`. Ο χειριστής του μπορεί να χρησιμοποιηθεί για συμπληρωματική επικύρωση, συνήθως για την επαλήθευση του σωστού συνδυασμού τιμών σε πολλαπλά στοιχεία της φόρμας. - -Εάν εντοπιστεί σφάλμα, το περνάμε στη φόρμα χρησιμοποιώντας τη μέθοδο `addError()`. Αυτή μπορεί να κληθεί είτε σε ένα συγκεκριμένο στοιχείο είτε απευθείας στη φόρμα. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Αυτός ο συνδυασμός δεν είναι δυνατός.'); - } -} -``` - - -Σφάλματα κατά την Επεξεργασία -============================= - -Σε πολλές περιπτώσεις, μαθαίνουμε για ένα σφάλμα μόνο όταν επεξεργαζόμαστε μια έγκυρη φόρμα, για παράδειγμα, όταν γράφουμε ένα νέο στοιχείο στη βάση δεδομένων και συναντάμε διπλότυπα κλειδιά. Σε αυτή την περίπτωση, περνάμε ξανά το σφάλμα στη φόρμα χρησιμοποιώντας τη μέθοδο `addError()`. Αυτή μπορεί να κληθεί είτε σε ένα συγκεκριμένο στοιχείο είτε απευθείας στη φόρμα: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Μη έγκυρος κωδικός πρόσβασης.'); - } -} -``` - -Εάν είναι δυνατόν, συνιστούμε να επισυνάψετε το σφάλμα απευθείας στο στοιχείο της φόρμας, καθώς θα εμφανιστεί δίπλα του όταν χρησιμοποιείτε τον προεπιλεγμένο renderer. - -```php -$form['date']->addError('Συγγνώμη, αλλά αυτή η ημερομηνία είναι ήδη κατειλημμένη.'); -``` - -Μπορείτε να καλέσετε το `addError()` επανειλημμένα για να περάσετε πολλαπλά μηνύματα σφάλματος στη φόρμα ή στο στοιχείο. Μπορείτε να τα λάβετε χρησιμοποιώντας το `getErrors()`. - -Προσοχή, το `$form->getErrors()` επιστρέφει μια σύνοψη όλων των μηνυμάτων σφάλματος, συμπεριλαμβανομένων εκείνων που παραδόθηκαν απευθείας σε μεμονωμένα στοιχεία, όχι μόνο απευθείας στη φόρμα. Μπορείτε να λάβετε τα μηνύματα σφάλματος που παραδόθηκαν μόνο στη φόρμα μέσω του `$form->getOwnErrors()`. - - -Τροποποίηση Εισόδου -=================== - -Χρησιμοποιώντας τη μέθοδο `addFilter()`, μπορούμε να τροποποιήσουμε την τιμή που εισήγαγε ο χρήστης. Σε αυτό το παράδειγμα, θα ανεχτούμε και θα αφαιρέσουμε κενά στον ταχυδρομικό κώδικα: - -```php -$form->addText('zip', 'Τ.Κ.:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // αφαιρούμε τα κενά από τον Τ.Κ. - }) - ->addRule($form::Pattern, 'Ο Τ.Κ. δεν είναι στη μορφή πέντε ψηφίων.', '\d{5}'); -``` - -Το φίλτρο ενσωματώνεται μεταξύ των κανόνων επικύρωσης και των συνθηκών, οπότε η σειρά των μεθόδων έχει σημασία, δηλαδή το φίλτρο και ο κανόνας καλούνται με την ίδια σειρά όπως οι μέθοδοι `addFilter()` και `addRule()`. - - -Επικύρωση JavaScript -==================== - -Η γλώσσα για τη διατύπωση συνθηκών και κανόνων είναι πολύ ισχυρή. Όλες οι κατασκευές λειτουργούν τόσο στην πλευρά του διακομιστή όσο και στην πλευρά του JavaScript. Μεταφέρονται σε χαρακτηριστικά HTML `data-nette-rules` ως JSON. Η ίδια η επικύρωση εκτελείται από ένα σενάριο που παρακολουθεί το συμβάν `submit` της φόρμας, διατρέχει τα μεμονωμένα στοιχεία και εκτελεί την αντίστοιχη επικύρωση. - -Αυτό το σενάριο είναι το `netteForms.js` και είναι διαθέσιμο από πολλές πιθανές πηγές: - -Μπορείτε να εισαγάγετε το σενάριο απευθείας στη σελίδα HTML από ένα CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ή να το αντιγράψετε τοπικά στον δημόσιο φάκελο του έργου (π.χ. από το `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Ή να το εγκαταστήσετε μέσω [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -Και στη συνέχεια να το φορτώσετε και να το εκτελέσετε: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Εναλλακτικά, μπορείτε να το φορτώσετε απευθείας από τον φάκελο `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -Δυναμικό JavaScript -=================== - -Θέλετε να εμφανίσετε τα πεδία για την εισαγωγή της διεύθυνσης μόνο εάν ο χρήστης επιλέξει την αποστολή των αγαθών ταχυδρομικώς; Κανένα πρόβλημα. Το κλειδί είναι το ζεύγος μεθόδων `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Αυτός ο κώδικας λέει ότι όταν η συνθήκη πληρούται, δηλαδή όταν το πλαίσιο ελέγχου είναι επιλεγμένο, το στοιχείο HTML `#address-container` θα είναι ορατό. Και αντίστροφα. Έτσι, τοποθετούμε τα στοιχεία της φόρμας με τη διεύθυνση του παραλήπτη σε ένα container με αυτό το ID, και όταν κάνουμε κλικ στο πλαίσιο ελέγχου, θα αποκρυφθούν ή θα εμφανιστούν. Αυτό διασφαλίζεται από το σενάριο `netteForms.js`. - -Οποιοσδήποτε επιλογέας μπορεί να περάσει ως όρισμα στη μέθοδο `toggle()`. Για ιστορικούς λόγους, μια αλφαριθμητική συμβολοσειρά χωρίς άλλους ειδικούς χαρακτήρες νοείται ως το ID του στοιχείου, δηλαδή σαν να προηγείται ο χαρακτήρας `#`. Η δεύτερη προαιρετική παράμετρος επιτρέπει την αντιστροφή της συμπεριφοράς, δηλαδή αν χρησιμοποιούσαμε `toggle('#address-container', false)`, το στοιχείο θα εμφανιζόταν μόνο εάν το πλαίσιο ελέγχου δεν ήταν επιλεγμένο. - -Η προεπιλεγμένη υλοποίηση στο JavaScript αλλάζει την ιδιότητα `hidden` των στοιχείων. Ωστόσο, μπορούμε εύκολα να αλλάξουμε τη συμπεριφορά, για παράδειγμα, προσθέτοντας μια κίνηση. Απλά αντικαταστήστε τη μέθοδο `Nette.toggle` στο JavaScript με τη δική σας λύση: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // απόκρυψη ή εμφάνιση του 'el' βάσει της τιμής 'visible' - }); -}; -``` - - -Απενεργοποίηση Επικύρωσης -========================= - -Μερικές φορές μπορεί να είναι χρήσιμο να απενεργοποιήσετε την επικύρωση. Εάν το πάτημα ενός κουμπιού υποβολής δεν πρέπει να εκτελεί επικύρωση (κατάλληλο για κουμπιά *Ακύρωση* ή *Προεπισκόπηση*), μπορούμε να την απενεργοποιήσουμε χρησιμοποιώντας τη μέθοδο `$submit->setValidationScope([])`. Εάν πρέπει να εκτελεί μόνο μερική επικύρωση, μπορούμε να καθορίσουμε ποια πεδία ή containers φόρμας πρέπει να επικυρωθούν. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Επικυρώνει ολόκληρη τη φόρμα -$form->addSubmit('send2') - ->setValidationScope([]); // Δεν επικυρώνει καθόλου -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Επικυρώνει μόνο το στοιχείο name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Επικυρώνει μόνο το στοιχείο age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Επικυρώνει το container details -``` - -Το `setValidationScope` δεν επηρεάζει το [#συμβάν onValidate] στη φόρμα, το οποίο θα καλείται πάντα. Το συμβάν `onValidate` σε ένα container θα ενεργοποιείται μόνο εάν αυτό το container έχει επισημανθεί για μερική επικύρωση. diff --git a/forms/hu/@home.texy b/forms/hu/@home.texy deleted file mode 100644 index 2a5dfbfe40..0000000000 --- a/forms/hu/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -A Nette Forms forradalmasította a webes űrlapok létrehozását. Hirtelen elég volt néhány érthető sor kódot írni, és kész volt az űrlap, beleértve a renderelést, a JavaScript és szerveroldali validációt, ráadásul csúcsminőségű biztonsággal. Megmutatjuk, hogyan - -- hozzunk létre felhasználóbarát űrlapokat -- validáljuk az elküldött adatokat -- rendereljük az elemeket pontosan az igények szerint - -</div> - - -A Nette Forms használatával elkerülheti a rutin feladatok egész sorát, mint például a validáció írását (ráadásul kétszer, szerver- és kliensoldalon), minimalizálhatja a hibák és biztonsági rések kialakulásának valószínűségét. - -Az űrlapokat használhatja a Nette Alkalmazás részeként (azaz presenterekben), vagy teljesen önállóan. Mivel mindkét esetben a használat kissé eltérő, két útmutatót készítettünk Önnek: - -<div class="wiki-buttons"> -<div> "Űrlapok presenterekben .[wiki-button]":in-presenter </div> -<div> "Űrlapok önállóan .[wiki-button]":standalone </div> -</div> - - -Telepítés ---------- - -A könyvtárat a [Composer|best-practices:composer] eszközzel töltheti le és telepítheti: - -```shell -composer require nette/forms -``` diff --git a/forms/hu/@left-menu.texy b/forms/hu/@left-menu.texy deleted file mode 100644 index 6fda5dc323..0000000000 --- a/forms/hu/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Bevezetés |@home] -- [Űrlapok presenterekben|in-presenter] -- [Űrlapok önállóan|standalone] -- [Űrlap elemek |controls] -- [Validáció |validation] -- [Renderelés |rendering] -- [Konfiguráció |configuration] - - -További olvasmányok -******************* -- [Útmutatók és eljárások |best-practices:] diff --git a/forms/hu/@meta.texy b/forms/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/forms/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/forms/hu/configuration.texy b/forms/hu/configuration.texy deleted file mode 100644 index d4b7a767c0..0000000000 --- a/forms/hu/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Űrlapok konfigurálása -********************* - -.[perex] -A konfigurációban megváltoztathatók az alapértelmezett [űrlap hibaüzenetek|validation]. - -```neon -forms: - messages: - 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.' - 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.' -``` - -Itt a magyar fordítás: - -```neon -forms: - messages: - Equal: 'Kérjük, adja meg a %s értéket.' - NotEqual: 'Ez az érték nem lehet %s.' - Filled: 'Ez a mező kötelező.' - Blank: 'Ennek a mezőnek üresnek kell lennie.' - MinLength: 'Kérjük, adjon meg legalább %d karaktert.' - MaxLength: 'Kérjük, legfeljebb %d karaktert adjon meg.' - Length: 'Kérjük, adjon meg egy %d és %d karakter közötti értéket.' - Email: 'Kérjük, adjon meg egy érvényes e-mail címet.' - URL: 'Kérjük, adjon meg egy érvényes URL-t.' - Integer: 'Kérjük, adjon meg egy érvényes egész számot.' - Float: 'Kérjük, adjon meg egy érvényes számot.' - Min: 'Kérjük, adjon meg egy %d vagy annál nagyobb értéket.' - Max: 'Kérjük, adjon meg egy %d vagy annál kisebb értéket.' - Range: 'Kérjük, adjon meg egy %d és %d közötti értéket.' - MaxFileSize: 'A feltöltött fájl mérete legfeljebb %d bájt lehet.' - MaxPostSize: 'A feltöltött adatok meghaladják a %d bájtos korlátot.' - MimeType: 'A feltöltött fájl nem a várt formátumban van.' - Image: 'A feltöltött fájlnak JPEG, GIF, PNG, WebP vagy AVIF formátumú képnek kell lennie.' - Nette\Forms\Controls\SelectBox::Valid: 'Kérjük, válasszon érvényes opciót.' - Nette\Forms\Controls\UploadControl::Valid: 'Hiba történt a fájl feltöltése során.' - Nette\Forms\Controls\CsrfProtection::Protection: 'A munkamenete lejárt. Kérjük, térjen vissza a kezdőlapra, és próbálja újra.' -``` - -Ha nem használja a teljes keretrendszert, és így a konfigurációs fájlokat sem, megváltoztathatja az alapértelmezett hibaüzeneteket közvetlenül a `Nette\Forms\Validator::$messages` tömbben. diff --git a/forms/hu/controls.texy b/forms/hu/controls.texy deleted file mode 100644 index 46c606fbef..0000000000 --- a/forms/hu/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Űrlap elemek -************ - -.[perex] -A standard űrlap elemek áttekintése. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Hozzáad egy egysoros szöveges mezőt (osztály: [TextInput |api:Nette\Forms\Controls\TextInput]). Ha a felhasználó nem tölti ki a mezőt, üres stringet `''` ad vissza, vagy a `setNullable()` segítségével beállítható, hogy `null`-t adjon vissza. - -```php -$form->addText('name', 'Név:') - ->setRequired() - ->setNullable(); -``` - -Automatikusan validálja az UTF-8 kódolást, levágja a bal- és jobboldali szóközöket, és eltávolítja azokat az újsor karaktereket, amelyeket egy támadó küldhetett. - -A maximális hosszúságot a `setMaxLength()` segítségével lehet korlátozni. A felhasználó által bevitt érték módosítását az [addFilter() |validation#Bemenet módosítása] teszi lehetővé. - -A `setHtmlType()` segítségével megváltoztatható a szöveges mező vizuális jellege olyan típusokra, mint a `search`, `tel` vagy `url`, lásd a [specifikációt|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Ne feledje, hogy a típusváltoztatás csak vizuális, és nem helyettesíti a validálási funkciót. Az `url` típushoz célszerű hozzáadni egy specifikus [URL validálási szabályt |validation#Szöveges bevitelek]. - -.[note] -Más beviteli típusokhoz, mint például a `number`, `range`, `email`, `date`, `datetime-local`, `time` és `color`, használjon specializált metódusokat, mint a [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] és [#addColor], amelyek biztosítják a szerveroldali validációt. A `month` és `week` típusok még nem támogatottak teljes mértékben minden böngészőben. - -Az elemhez beállítható ún. empty-value, ami valami olyasmi, mint az alapértelmezett érték, de ha a felhasználó nem változtatja meg, az elem üres stringet vagy `null`-t ad vissza. - -```php -$form->addText('phone', 'Telefon:') - ->setHtmlType('tel') - ->setEmptyValue('+36'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Hozzáad egy mezőt többsoros szöveg bevitelére (osztály: [TextArea |api:Nette\Forms\Controls\TextArea]). Ha a felhasználó nem tölti ki a mezőt, üres stringet `''` ad vissza, vagy a `setNullable()` segítségével beállítható, hogy `null`-t adjon vissza. - -```php -$form->addTextArea('note', 'Megjegyzés:') - ->addRule($form::MaxLength, 'A megjegyzés túl hosszú', 10000); -``` - -Automatikusan validálja az UTF-8 kódolást és normalizálja a sorelválasztókat `\n`-re. Ellentétben az egysoros beviteli mezővel, itt nem történik szóközök levágása. - -A maximális hosszúságot a `setMaxLength()` segítségével lehet korlátozni. A felhasználó által bevitt érték módosítását az [addFilter() |validation#Bemenet módosítása] teszi lehetővé. Beállítható ún. empty-value a `setEmptyValue()` segítségével. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Hozzáad egy mezőt egész számok bevitelére (osztály: [TextInput |api:Nette\Forms\Controls\TextInput]). Vagy integert ad vissza, vagy `null`-t, ha a felhasználó nem ad meg semmit. - -```php -$form->addInteger('year', 'Év:') - ->addRule($form::Range, 'Az évnek %d és %d között kell lennie.', [1900, 2023]); -``` - -Az elem `<input type="number">`-ként jelenik meg. A `setHtmlType()` metódus használatával a típust `range`-re lehet változtatni a csúszka formájában történő megjelenítéshez, vagy `text`-re, ha a standard szöveges mezőt részesíti előnyben a `number` típus speciális viselkedése nélkül. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Hozzáad egy mezőt tizedes számok bevitelére (osztály: [TextInput |api:Nette\Forms\Controls\TextInput]). Vagy floatot ad vissza, vagy `null`-t, ha a felhasználó nem ad meg semmit. - -```php -$form->addFloat('level', 'Szint:') - ->setDefaultValue(0) - ->addRule($form::Range, 'A szintnek %d és %d között kell lennie.', [0, 100]); -``` - -Az elem `<input type="number">`-ként jelenik meg. A `setHtmlType()` metódus használatával a típust `range`-re lehet változtatni a csúszka formájában történő megjelenítéshez, vagy `text`-re, ha a standard szöveges mezőt részesíti előnyben a `number` típus speciális viselkedése nélkül. - -A Nette és a böngésző tizedes elválasztóként elfogadja mind a vesszőt, mind a pontot. Annak érdekében, hogy ez a funkcionalitás a Firefoxban is elérhető legyen, ajánlott beállítani a `lang` attribútumot vagy az adott elemre, vagy az egész oldalra, például `<html lang="hu">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Hozzáad egy mezőt e-mail cím bevitelére (osztály: [TextInput |api:Nette\Forms\Controls\TextInput]). Ha a felhasználó nem tölti ki a mezőt, üres stringet `''` ad vissza, vagy a `setNullable()` segítségével beállítható, hogy `null`-t adjon vissza. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Ellenőrzi, hogy az érték érvényes e-mail cím-e. Nem ellenőrzi, hogy a domain valóban létezik-e, csak a szintaxist ellenőrzi. Automatikusan validálja az UTF-8 kódolást, levágja a bal- és jobboldali szóközöket. - -A maximális hosszúságot a `setMaxLength()` segítségével lehet korlátozni. A felhasználó által bevitt érték módosítását az [addFilter() |validation#Bemenet módosítása] teszi lehetővé. Beállítható ún. empty-value a `setEmptyValue()` segítségével. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Hozzáad egy mezőt jelszó bevitelére (osztály: [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Jelszó:') - ->setRequired() - ->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8) - ->addRule($form::Pattern, 'Tartalmaznia kell számjegyet', '.*[0-9].*'); -``` - -Az űrlap újramegjelenítésekor a mező üres lesz. Automatikusan validálja az UTF-8 kódolást, levágja a bal- és jobboldali szóközöket, és eltávolítja azokat az újsor karaktereket, amelyeket egy támadó küldhetett. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Hozzáad egy jelölőnégyzetet (osztály: [Checkbox |api:Nette\Forms\Controls\Checkbox]). Vagy `true` vagy `false` értéket ad vissza, attól függően, hogy be van-e jelölve. - -```php -$form->addCheckbox('agree', 'Elfogadom a feltételeket') - ->setRequired('El kell fogadni a feltételeket'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Hozzáad jelölőnégyzeteket több elem kiválasztásához (osztály: [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). A kiválasztott elemek kulcsainak tömbjét adja vissza. A `getSelectedItems()` metódus az értékeket adja vissza a kulcsok helyett. - -```php -$form->addCheckboxList('colors', 'Színek:', [ - 'r' => 'piros', - 'g' => 'zöld', - 'b' => 'kék', -]); -``` - -A kínált elemek tömbjét harmadik paraméterként vagy a `setItems()` metódussal adjuk át. - -A `setDisabled(['r', 'g'])` segítségével letilthatók az egyes elemek. - -Az elem automatikusan ellenőrzi, hogy nem történt-e hamisítás, és hogy a kiválasztott elemek valóban a kínáltak közül valók-e, és nem voltak-e letiltva. A `getRawValue()` metódussal lekérhetők az elküldött elemek e fontos ellenőrzés nélkül. - -Az alapértelmezett kiválasztott elemek beállításakor is ellenőrzi, hogy azok a kínáltak közül valók-e, különben kivételt dob. Ezt az ellenőrzést a `checkDefaultValue(false)` segítségével lehet kikapcsolni. - -Ha az űrlapot `GET` metódussal küldi el, választhat egy kompaktabb adatátviteli módot, amely csökkenti a query string méretét. Ez az űrlap HTML attribútumának beállításával aktiválható: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Hozzáad rádiógombokat (osztály: [RadioList |api:Nette\Forms\Controls\RadioList]). A kiválasztott elem kulcsát adja vissza, vagy `null`-t, ha a felhasználó nem választott semmit. A `getSelectedItem()` metódus az értéket adja vissza a kulcs helyett. - -```php -$sex = [ - 'm' => 'férfi', - 'f' => 'nő', -]; -$form->addRadioList('gender', 'Nem:', $sex); -``` - -A kínált elemek tömbjét harmadik paraméterként vagy a `setItems()` metódussal adjuk át. - -A `setDisabled(['m', 'f'])` segítségével letilthatók az egyes elemek. - -Az elem automatikusan ellenőrzi, hogy nem történt-e hamisítás, és hogy a kiválasztott elem valóban a kínáltak közül való-e, és nem volt-e letiltva. A `getRawValue()` metódussal lekérhető az elküldött elem e fontos ellenőrzés nélkül. - -Az alapértelmezett kiválasztott elem beállításakor is ellenőrzi, hogy az a kínáltak közül való-e, különben kivételt dob. Ezt az ellenőrzést a `checkDefaultValue(false)` segítségével lehet kikapcsolni. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Hozzáad egy select boxot (osztály: [SelectBox |api:Nette\Forms\Controls\SelectBox]). A kiválasztott elem kulcsát adja vissza, vagy `null`-t, ha a felhasználó nem választott semmit. A `getSelectedItem()` metódus az értéket adja vissza a kulcs helyett. - -```php -$countries = [ - 'CZ' => 'Cseh Köztársaság', - 'SK' => 'Szlovákia', - 'GB' => 'Nagy-Britannia', -]; - -$form->addSelect('country', 'Ország:', $countries) - ->setDefaultValue('SK'); -``` - -A kínált elemek tömbjét harmadik paraméterként vagy a `setItems()` metódussal adjuk át. Az elemek lehetnek kétdimenziós tömbök is: - -```php -$countries = [ - 'Európa' => [ - 'CZ' => 'Cseh Köztársaság', - 'SK' => 'Szlovákia', - 'GB' => 'Nagy-Britannia', - ], - 'CA' => 'Kanada', - 'US' => 'USA', - '?' => 'más', -]; -``` - -A select boxoknál gyakran az első elemnek speciális jelentése van, felhívásként szolgál. Ilyen elem hozzáadására a `setPrompt()` metódus szolgál. - -```php -$form->addSelect('country', 'Ország:', $countries) - ->setPrompt('Válasszon országot'); -``` - -A `setDisabled(['CZ', 'SK'])` segítségével letilthatók az egyes elemek. - -Az elem automatikusan ellenőrzi, hogy nem történt-e hamisítás, és hogy a kiválasztott elem valóban a kínáltak közül való-e, és nem volt-e letiltva. A `getRawValue()` metódussal lekérhető az elküldött elem e fontos ellenőrzés nélkül. - -Az alapértelmezett kiválasztott elem beállításakor is ellenőrzi, hogy az a kínáltak közül való-e, különben kivételt dob. Ezt az ellenőrzést a `checkDefaultValue(false)` segítségével lehet kikapcsolni. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Hozzáad egy select boxot több elem kiválasztásához (osztály: [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). A kiválasztott elemek kulcsainak tömbjét adja vissza. A `getSelectedItems()` metódus az értékeket adja vissza a kulcsok helyett. - -```php -$form->addMultiSelect('countries', 'Országok:', $countries); -``` - -A kínált elemek tömbjét harmadik paraméterként vagy a `setItems()` metódussal adjuk át. Az elemek lehetnek kétdimenziós tömbök is. - -A `setDisabled(['CZ', 'SK'])` segítségével letilthatók az egyes elemek. - -Az elem automatikusan ellenőrzi, hogy nem történt-e hamisítás, és hogy a kiválasztott elemek valóban a kínáltak közül valók-e, és nem voltak-e letiltva. A `getRawValue()` metódussal lekérhetők az elküldött elemek e fontos ellenőrzés nélkül. - -Az alapértelmezett kiválasztott elemek beállításakor is ellenőrzi, hogy azok a kínáltak közül valók-e, különben kivételt dob. Ezt az ellenőrzést a `checkDefaultValue(false)` segítségével lehet kikapcsolni. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Hozzáad egy mezőt fájl feltöltéséhez (osztály: [UploadControl |api:Nette\Forms\Controls\UploadControl]). Egy [FileUpload |http:request#FileUpload] objektumot ad vissza, még akkor is, ha a felhasználó nem küldött fájlt, amit a `FileUpload::hasFile()` metódussal lehet ellenőrizni. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Az avatarnak JPEG, PNG, GIF, WebP vagy AVIF formátumúnak kell lennie.') - ->addRule($form::MaxFileSize, 'A maximális méret 1 MB.', 1024 * 1024); -``` - -Ha a fájl feltöltése nem sikerül megfelelően, az űrlap nem kerül sikeresen elküldésre, és hiba jelenik meg. Vagyis sikeres elküldés esetén nem szükséges ellenőrizni a `FileUpload::isOk()` metódust. - -Soha ne bízzon a `FileUpload::getName()` metódus által visszaadott eredeti fájlnévben, a kliens rosszindulatú fájlnevet küldhetett azzal a szándékkal, hogy kárt okozzon vagy feltörje az alkalmazását. - -A `MimeType` és `Image` szabályok a fájl aláírása alapján észlelik a kívánt típust, és nem ellenőrzik annak integritását. Azt, hogy a kép nem sérült-e, például a [betöltésének |http:request#toImage] megkísérlésével lehet megállapítani. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Hozzáad egy mezőt több fájl egyidejű feltöltéséhez (osztály: [UploadControl |api:Nette\Forms\Controls\UploadControl]). [FileUpload |http:request#FileUpload] objektumok tömbjét adja vissza. A `FileUpload::hasFile()` metódus mindegyiknél `true`-t fog visszaadni. - -```php -$form->addMultiUpload('files', 'Fájlok:') - ->addRule($form::MaxLength, 'Legfeljebb %d fájlt lehet feltölteni', 10); -``` - -Ha valamelyik fájl feltöltése nem sikerül megfelelően, az űrlap nem kerül sikeresen elküldésre, és hiba jelenik meg. Vagyis sikeres elküldés esetén nem szükséges ellenőrizni a `FileUpload::isOk()` metódust. - -Soha ne bízzon a `FileUpload::getName()` metódus által visszaadott eredeti fájlnevekben, a kliens rosszindulatú fájlnevet küldhetett azzal a szándékkal, hogy kárt okozzon vagy feltörje az alkalmazását. - -A `MimeType` és `Image` szabályok a fájl aláírása alapján észlelik a kívánt típust, és nem ellenőrzik annak integritását. Azt, hogy a kép nem sérült-e, például a [betöltésének |http:request#toImage] megkísérlésével lehet megállapítani. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Hozzáad egy mezőt, amely lehetővé teszi a felhasználó számára, hogy könnyen megadjon egy dátumot, amely évből, hónapból és napból áll (osztály: [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Alapértelmezett értékként elfogadja vagy a `DateTimeInterface` interfészt implementáló objektumokat, egy időt tartalmazó stringet, vagy egy UNIX timestamp-et képviselő számot. Ugyanez vonatkozik a `Min`, `Max` vagy `Range` szabályok argumentumaira is, amelyek meghatározzák a minimális és maximális megengedett dátumot. - -```php -$form->addDate('date', 'Dátum:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'A dátumnak legalább egy hónaposnak kell lennie.', new DateTime('-1 month')); -``` - -Alapértelmezés szerint `DateTimeImmutable` objektumot ad vissza, a `setFormat()` metódussal megadhatja a [szöveges formátumot|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] vagy a timestamp-et: - -```php -$form->addDate('date', 'Dátum:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Hozzáad egy mezőt, amely lehetővé teszi a felhasználó számára, hogy könnyen megadjon egy időt, amely órákból, percekből és opcionálisan másodpercekből áll (osztály: [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Alapértelmezett értékként elfogadja vagy a `DateTimeInterface` interfészt implementáló objektumokat, egy időt tartalmazó stringet, vagy egy UNIX timestamp-et képviselő számot. Ezekből a bemenetekből csak az időinformáció kerül felhasználásra, a dátum figyelmen kívül marad. Ugyanez vonatkozik a `Min`, `Max` vagy `Range` szabályok argumentumaira is, amelyek meghatározzák a minimális és maximális megengedett időt. Ha a beállított minimális érték magasabb, mint a maximális, akkor egy éjfélen átnyúló időtartomány jön létre. - -```php -$form->addTime('time', 'Idő:', withSeconds: true) - ->addRule($form::Range, 'Az időnek %d és %d között kell lennie.', ['12:30', '13:30']); -``` - -Alapértelmezés szerint `DateTimeImmutable` objektumot ad vissza (az 1. év január 1-jei dátummal), a `setFormat()` metódussal megadhatja a [szöveges formátumot|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Idő:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Hozzáad egy mezőt, amely lehetővé teszi a felhasználó számára, hogy könnyen megadjon egy dátumot és időt, amely évből, hónapból, napból, órákból, percekből és opcionálisan másodpercekből áll (osztály: [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Alapértelmezett értékként elfogadja vagy a `DateTimeInterface` interfészt implementáló objektumokat, egy időt tartalmazó stringet, vagy egy UNIX timestamp-et képviselő számot. Ugyanez vonatkozik a `Min`, `Max` vagy `Range` szabályok argumentumaira is, amelyek meghatározzák a minimális és maximális megengedett dátumot. - -```php -$form->addDateTime('datetime', 'Dátum és idő:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'A dátumnak legalább egy hónaposnak kell lennie.', new DateTime('-1 month')); -``` - -Alapértelmezés szerint `DateTimeImmutable` objektumot ad vissza, a `setFormat()` metódussal megadhatja a [szöveges formátumot|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] vagy a timestamp-et: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Hozzáad egy mezőt színválasztáshoz (osztály: [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). A szín egy `#rrggbb` formátumú string. Ha a felhasználó nem választ, a fekete szín `#000000` kerül visszaadásra. - -```php -$form->addColor('color', 'Szín:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Hozzáad egy rejtett mezőt (osztály: [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -A `setNullable()` segítségével beállítható, hogy `null`-t adjon vissza üres string helyett. Az elküldött érték módosítását az [addFilter() |validation#Bemenet módosítása] teszi lehetővé. - -Bár az elem rejtett, **fontos tudatosítani**, hogy az értékét egy támadó továbbra is módosíthatja vagy hamisíthatja. Mindig alaposan ellenőrizze és validálja az összes fogadott értéket a szerveroldalon, hogy megelőzze az adatmanipulációval kapcsolatos biztonsági kockázatokat. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Hozzáad egy küldés gombot (osztály: [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Küldés'); -``` - -Az űrlapon több küldés gomb is lehet: - -```php -$form->addSubmit('register', 'Regisztráció'); -$form->addSubmit('cancel', 'Mégse'); -``` - -Annak megállapításához, hogy melyikre kattintottak, használja a következőt: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Ha nem szeretné az egész űrlapot validálni a gomb megnyomásakor (például a *Mégse* vagy *Előnézet* gomboknál), használja a [setValidationScope() |validation#Validáció kikapcsolása] metódust. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Hozzáad egy gombot (osztály: [Button |api:Nette\Forms\Controls\Button]), amelynek nincs küldési funkciója. Használható tehát valamilyen más funkcióra, pl. JavaScript függvény meghívására kattintáskor. - -```php -$form->addButton('raise', 'Fizetésemelés') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Hozzáad egy kép formájú küldés gombot (osztály: [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/útvonal/a/képhez.png'); -``` - -Több küldés gomb használatakor a `$form['submit']->isSubmittedBy()` segítségével megállapítható, hogy melyikre kattintottak. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Hozzáad egy alűrlapot (osztály: [Container|api:Nette\Forms\Container]), vagyis egy konténert, amelybe ugyanúgy lehet további elemeket hozzáadni, mint ahogy az űrlaphoz adjuk őket. Működnek a `setDefaults()` vagy `getValues()` metódusok is. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Neved:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Neved:'); -$sub2->addEmail('email', 'Email:'); -``` - -Az elküldött adatok ezután többdimenziós struktúraként kerülnek visszaadásra: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Beállítások áttekintése -======================= - -Minden elemnél meghívhatjuk a következő metódusokat (teljes áttekintés az [API dokumentációban|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | beállítja az alapértelmezett értéket -| `getValue()` | lekéri az aktuális értéket -| `setOmitted()` | [#Érték kihagyása] -| `setDisabled()` | [#Elemek letiltása] - -Megjelenítés: -.[table-form-methods language-php] -| `setCaption($caption)` | megváltoztatja az elem címkéjét -| `setTranslator($translator)` | beállítja a [fordítót |rendering#Fordítás] -| `setHtmlAttribute($name, $value)` | beállítja az elem [HTML attribútumát |rendering#HTML attribútumok] -| `setHtmlId($id)` | beállítja a HTML `id` attribútumot -| `setHtmlType($type)` | beállítja a HTML `type` attribútumot -| `setHtmlName($name)` | beállítja a HTML `name` attribútumot -| `setOption($key, $value)` | [megjelenítési beállítások |rendering#Options] - -Validáció: -.[table-form-methods language-php] -| `setRequired()` | [kötelező elem |validation] -| `addRule()` | beállítja az [érvényesítési szabályt |validation#Szabályok] -| `addCondition()`, `addConditionOn()` | beállítja az [érvényesítési feltételt |validation#Feltételek] -| `addError($message)` | [hibaüzenet átadása |validation#Hibák a feldolgozás során] - -Az `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` elemeknél a következő metódusokat lehet meghívni: - -.[table-form-methods language-php] -| `setNullable()` | beállítja, hogy a getValue() `null`-t adjon-e vissza üres string helyett -| `setEmptyValue($value)` | beállít egy speciális értéket, amely üres stringnek minősül -| `setMaxLength($length)` | beállítja a megengedett karakterek maximális számát -| `addFilter($filter)` | [bemenet módosítása |validation#Bemenet módosítása] - - -Érték kihagyása -=============== - -Ha nem érdekel minket a felhasználó által kitöltött érték, a `setOmitted()` segítségével kihagyhatjuk a `$form->getValues()` metódus eredményéből vagy a handlereknek átadott adatokból. Ez hasznos lehet különböző ellenőrző jelszavaknál, antispam elemeknél stb. - -```php -$form->addPassword('passwordVerify', 'Jelszó újra:') - ->setRequired('Kérjük, adja meg a jelszót újra az ellenőrzéshez') - ->addRule($form::Equal, 'A jelszavak nem egyeznek', $form['password']) - ->setOmitted(); -``` - - -Elemek letiltása -================ - -Az elemeket a `setDisabled()` segítségével lehet letiltani. Egy ilyen elemet a felhasználó nem tud szerkeszteni. - -```php -$form->addText('username', 'Felhasználónév:') - ->setDisabled(); -``` - -A letiltott elemeket a böngésző egyáltalán nem küldi el a szerverre, tehát nem is találja meg őket a `$form->getValues()` függvény által visszaadott adatokban. Ha azonban beállítja a `setOmitted(false)` értéket, a Nette ezekbe az adatokba belefoglalja az alapértelmezett értéküket. - -A `setDisabled()` hívásakor biztonsági okokból **törlődik az elem értéke**. Ha alapértelmezett értéket állít be, azt a letiltás után kell megtenni: - -```php -$form->addText('username', 'Felhasználónév:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -A letiltott elemek alternatívája a `readonly` HTML attribútummal rendelkező elemek, amelyeket a böngésző elküld a szerverre. Bár az elem csak olvasható, **fontos tudatosítani**, hogy az értékét egy támadó továbbra is módosíthatja vagy hamisíthatja. - - -Egyéni elemek -============= - -A beépített űrlap elemek széles skálája mellett egyéni elemeket is hozzáadhat az űrlaphoz a következő módon: - -```php -$form->addComponent(new DateInput('Dátum:'), 'date'); -// alternatív szintaxis: $form['date'] = new DateInput('Dátum:'); -``` - -.[note] -Az űrlap a [Container |component-model:#Container] osztály leszármazottja, az egyes elemek pedig a [Component |component-model:#Component] leszármazottai. - -Létezik egy módszer új űrlap metódusok definiálására, amelyek egyéni elemek hozzáadására szolgálnak (pl. `$form->addZip()`). Ezek az ún. extension methods. Hátrányuk, hogy a szerkesztőkben nem fog működni rájuk a kódkiegészítés. - -```php -use Nette\Forms\Container; - -// hozzáadjuk az addZip(string $name, ?string $label = null) metódust -Container::extensionMethod('addZip', function (Container $form, string $name, ?string $label = null) { - return $form->addText($name, $label) - ->addRule($form::Pattern, 'Legalább 5 számjegy', '[0-9]{5}'); -}); - -// használat -$form->addZip('zip', 'Irányítószám:'); -``` - - -Alacsony szintű elemek -====================== - -Használhatunk olyan elemeket is, amelyeket csak a sablonban írunk le, és nem adjuk hozzá az űrlaphoz valamelyik `$form->addXyz()` metódussal. Ha például adatbázisból listázunk rekordokat, és előre nem tudjuk, hány lesz belőlük és milyen ID-jük lesz, és minden sornál szeretnénk egy checkboxot vagy radio buttont megjeleníteni, elég azt a sablonban kódolni: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -És elküldés után lekérjük az értéket: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -ahol az első paraméter az elem típusa (`DataFile` a `type=file`-hoz, `DataLine` az egysoros bemenetekhez, mint `text`, `password`, `email` stb., és `DataText` az összes többihez), a második paraméter `sel[]` pedig a HTML `name` attribútumnak felel meg. Az elem típusát kombinálhatjuk a `DataKeys` értékkel, amely megőrzi az elemek kulcsait. Ez különösen hasznos a `select`, `radioList` és `checkboxList` esetén. - -Lényeges, hogy a `getHttpData()` szanitizált értéket ad vissza, ebben az esetben ez mindig érvényes UTF-8 stringek tömbje lesz, függetlenül attól, hogy a támadó mit próbált a szervernek becsempészni. Ez hasonló a közvetlen `$_POST` vagy `$_GET` kezeléséhez, azzal a lényeges különbséggel, hogy mindig tiszta adatokat ad vissza, ahogy azt a standard Nette űrlap elemeknél megszokhattuk. diff --git a/forms/hu/in-presenter.texy b/forms/hu/in-presenter.texy deleted file mode 100644 index 5fe623b39d..0000000000 --- a/forms/hu/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Űrlapok presenterekben -********************** - -.[perex] -A Nette Forms rendkívül megkönnyíti a webes űrlapok létrehozását és feldolgozását. Ebben a fejezetben megismerkedhet az űrlapok használatával a presentereken belül. - -Ha érdekli, hogyan használhatja őket teljesen önállóan a keretrendszer többi része nélkül, akkor az [önálló használat|standalone] útmutatója Önnek szól. - - -Első űrlap -========== - -Próbáljunk meg írni egy egyszerű regisztrációs űrlapot. A kódja a következő lesz: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Név:'); -$form->addPassword('password', 'Jelszó:'); -$form->addSubmit('send', 'Regisztráció'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -és a böngészőben így jelenik meg: - -[* form-cs.webp *] - -Az űrlap a presenterben egy `Nette\Application\UI\Form` osztály objektuma, elődje, a `Nette\Forms\Form` önálló használatra készült. Hozzáadtunk ún. név, jelszó elemeket és egy küldés gombot. Végül a `$form->onSuccess` sor azt mondja, hogy elküldés és sikeres validálás után meg kell hívni a `$this->formSucceeded()` metódust. - -A presenter szempontjából az űrlap egy szokásos komponens. Ezért komponensként kezeljük, és a presenterbe egy [factory metódus |application:components#Factory metódusok] segítségével illesztjük be. Ez így fog kinézni: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Név:'); - $form->addPassword('password', 'Jelszó:'); - $form->addSubmit('send', 'Regisztráció'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // itt dolgozzuk fel az űrlappal küldött adatokat - // $data->name tartalmazza a nevet - // $data->password tartalmazza a jelszót - $this->flashMessage('Sikeresen regisztrált.'); - $this->redirect('Home:'); - } -} -``` - -És a sablonban az űrlapot a `{control}` taggel jelenítjük meg: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Regisztráció</h1> - -{control registrationForm} -``` - -És ez tulajdonképpen minden :-) Van egy működő és tökéletesen [biztonságos |#Védelem a sebezhetőségekkel szemben] űrlapunk. - -És most valószínűleg azt gondolja, hogy ez túl gyors volt, azon tűnődik, hogyan lehetséges, hogy meghívódik a `formSucceeded()` metódus, és mik azok a paraméterek, amelyeket kap. Persze, igaza van, ez magyarázatot érdemel. - -A Nette ugyanis egy friss mechanizmussal érkezik, amelyet [Hollywood style |application:components#Hollywood style]-nak nevezünk. Ahelyett, hogy fejlesztőként állandóan kérdezgetnie kellene, hogy történt-e valami („el lett küldve az űrlap?”, „érvényesen lett elküldve?” és „nem hamisították-e meg?”), azt mondja a keretrendszernek: „amikor az űrlap érvényesen ki van töltve, hívd meg ezt a metódust”, és a további munkát ráhagyja. Ha JavaScriptben programozik, ezt a programozási stílust jól ismeri. Függvényeket ír, amelyek akkor hívódnak meg, amikor egy bizonyos [esemény |nette:glossary#Eventek események] bekövetkezik. És a nyelv átadja nekik a megfelelő argumentumokat. - -Pontosan így épül fel a fenti presenter kód is. A `$form->onSuccess` tömb PHP callbackek listáját képviseli, amelyeket a Nette akkor hív meg, amikor az űrlap elküldésre kerül és helyesen van kitöltve (azaz érvényes). A [presenter életciklusa |application:presenters#Presenter életciklusa] keretében ez egy ún. signal, tehát az `action*` metódus után és a `render*` metódus előtt hívódnak meg. És minden callbacknek átadja első paraméterként magát az űrlapot, második paraméterként pedig az elküldött adatokat egy [ArrayHash |utils:arrays#ArrayHash] objektum formájában. Az első paramétert kihagyhatja, ha nincs szüksége az űrlap objektumra. A második paraméter pedig lehet okosabb, de erről majd [később |#Leképezés osztályokra]. - -A `$data` objektum tartalmazza a `name` és `password` kulcsokat azokkal az adatokkal, amelyeket a felhasználó kitöltött. Általában az adatokat azonnal továbbítjuk további feldolgozásra, ami lehet például adatbázisba való beszúrás. A feldolgozás során azonban hiba léphet fel, például a felhasználónév már foglalt. Ebben az esetben a hibát visszaküldjük az űrlapnak az `addError()` segítségével, és hagyjuk újra megjeleníteni, a hibaüzenettel együtt. - -```php -$form->addError('Sajnáljuk, a felhasználónév már foglalt.'); -``` - -Az `onSuccess` mellett létezik még az `onSubmit`: a callbackek mindig az űrlap elküldése után hívódnak meg, akkor is, ha nincs helyesen kitöltve. Továbbá az `onError`: a callbackek csak akkor hívódnak meg, ha az elküldés nem érvényes. Akkor is meghívódnak, ha az `onSuccess` vagy `onSubmit` során érvénytelenítjük az űrlapot az `addError()` segítségével. - -Az űrlap feldolgozása után átirányítunk a következő oldalra. Ez megakadályozza az űrlap nem kívánt újraküldését a *frissítés*, *vissza* gombbal vagy a böngésző előzményeiben való mozgással. - -Próbáljon meg hozzáadni további [űrlap elemeket|controls] is. - - -Elemekhez való hozzáférés -========================= - -Az űrlap a presenter komponense, esetünkben `registrationForm` néven (a `createComponentRegistrationForm` factory metódus neve alapján), így bárhol a presenterben hozzáférhet az űrlaphoz a következőképpen: - -```php -$form = $this->getComponent('registrationForm'); -// alternatív szintaxis: $form = $this['registrationForm']; -``` - -Az egyes űrlap elemek is komponensek, ezért ugyanúgy hozzáférhet hozzájuk: - -```php -$input = $form->getComponent('name'); // vagy $input = $form['name']; -$button = $form->getComponent('send'); // vagy $button = $form['send']; -``` - -Az elemeket az `unset` segítségével távolíthatja el: - -```php -unset($form['name']); -``` - - -Validálási szabályok -==================== - -Elhangzott a *valid* szó, de az űrlapnak még nincsenek validálási szabályai. Javítsuk ezt ki. - -A név kötelező lesz, ezért megjelöljük a `setRequired()` metódussal, amelynek argumentuma a hibaüzenet szövege, amely akkor jelenik meg, ha a felhasználó nem tölti ki a nevet. Ha nem adunk meg argumentumot, az alapértelmezett hibaüzenet kerül felhasználásra. - -```php -$form->addText('name', 'Név:') - ->setRequired('Kérjük, adja meg a nevét'); -``` - -Próbálja meg elküldeni az űrlapot kitöltetlen névvel, és látni fogja, hogy megjelenik a hibaüzenet, és a böngésző vagy a szerver elutasítja, amíg ki nem tölti a mezőt. - -Ugyanakkor a rendszert nem csaphatja be azzal, hogy a mezőbe például csak szóközöket ír. Dehogy. A Nette automatikusan eltávolítja a bal- és jobboldali szóközöket. Próbálja ki. Ez egy olyan dolog, amit minden egysoros inputtal mindig meg kellene tennie, de gyakran elfelejtik. A Nette ezt automatikusan megteszi. (Megpróbálhatja becsapni az űrlapot, és névként többsoros stringet küldeni. A Nette itt sem hagyja magát megtéveszteni, és az újsorokat szóközökre cseréli.) - -Az űrlap mindig a szerveroldalon validálódik, de JavaScript validáció is generálódik, amely villámgyorsan lefut, és a felhasználó azonnal értesül a hibáról, anélkül, hogy az űrlapot el kellene küldenie a szerverre. Ezt a `netteForms.js` szkript végzi. Illessze be a layout sablonba: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ha megnézi az űrlapot tartalmazó oldal forráskódját, észreveheti, hogy a Nette a kötelező elemeket `required` CSS osztállyal rendelkező elemekbe helyezi. Próbálja meg hozzáadni a következő stíluslapot a sablonhoz, és a „Név” címke piros lesz. Elegánsan jelöljük így a felhasználóknak a kötelező elemeket: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -További validálási szabályokat az `addRule()` metódussal adunk hozzá. Az első paraméter a szabály, a második ismét a hibaüzenet szövege, és még következhet a validálási szabály argumentuma. Mit jelent ez? - -Az űrlapot kibővítjük egy új, nem kötelező „életkor” mezővel, amelynek egész számnak kell lennie (`addInteger()`), és ráadásul a megengedett tartományban (`$form::Range`). És itt pontosan kihasználjuk az `addRule()` metódus harmadik paraméterét, amellyel átadjuk a validátornak a kívánt tartományt egy `[tól, ig]` párként: - -```php -$form->addInteger('age', 'Életkor:') - ->addRule($form::Range, 'Az életkornak 18 és 120 között kell lennie', [18, 120]); -``` - -.[tip] -Ha a felhasználó nem tölti ki a mezőt, a validálási szabályok nem kerülnek ellenőrzésre, mivel az elem nem kötelező. - -Itt van lehetőség egy kis refaktorálásra. A hibaüzenetben és a harmadik paraméterben a számok duplikáltan szerepelnek, ami nem ideális. Ha [többnyelvű űrlapokat |rendering#Fordítás] hoznánk létre, és a számokat tartalmazó üzenet több nyelvre lenne lefordítva, megnehezítené az értékek esetleges megváltoztatását. Ebből az okból kifolyólag használhatók a `%d` helyettesítő karakterek, és a Nette kiegészíti az értékeket: - -```php - ->addRule($form::Range, 'Az életkornak %d és %d év között kell lennie', [18, 120]); -``` - -Térjünk vissza a `password` elemhez, amelyet szintén kötelezővé teszünk, és még ellenőrizzük a jelszó minimális hosszát (`$form::MinLength`), ismét a helyettesítő karakter használatával: - -```php -$form->addPassword('password', 'Jelszó:') - ->setRequired('Válasszon jelszót') - ->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8); -``` - -Hozzáadunk az űrlaphoz még egy `passwordVerify` mezőt, ahol a felhasználó még egyszer megadja a jelszót, ellenőrzés céljából. Validálási szabályokkal ellenőrizzük, hogy mindkét jelszó azonos-e (`$form::Equal`). És paraméterként hivatkozást adunk az első jelszóra a [szögletes zárójelek |#Elemekhez való hozzáférés] segítségével: - -```php -$form->addPassword('passwordVerify', 'Jelszó újra:') - ->setRequired('Kérjük, adja meg a jelszót újra az ellenőrzéshez') - ->addRule($form::Equal, 'A jelszavak nem egyeznek', $form['password']) - ->setOmitted(); -``` - -A `setOmitted()` segítségével megjelöltük azt az elemet, amelynek az értékére valójában nem vagyunk kíváncsiak, és amely csak a validáció miatt létezik. Az érték nem kerül átadásra a `$data`-ba. - -Ezzel kész is van egy teljesen működőképes űrlapunk validációval PHP-ban és JavaScriptben is. A Nette validálási képességei sokkal szélesebbek, lehet feltételeket létrehozni, azok alapján megjeleníteni és elrejteni az oldal részeit stb. Mindent megtudhat az [űrlap validációról|validation] szóló fejezetben. - - -Alapértelmezett értékek -======================= - -Az űrlap elemeinek általában beállítunk alapértelmezett értékeket: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Gyakran hasznos az összes elem alapértelmezett értékét egyszerre beállítani. Például, ha az űrlap rekordok szerkesztésére szolgál. Kiolvassuk a rekordot az adatbázisból, és beállítjuk az alapértelmezett értékeket: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Hívja meg a `setDefaults()`-t az elemek definiálása után. - - -Űrlap megjelenítése -=================== - -Alapértelmezés szerint az űrlap táblázatként jelenik meg. Az egyes elemek megfelelnek az alapvető hozzáférhetőségi szabálynak - minden címke `<label>`-ként van megírva és összekapcsolva a megfelelő űrlap elemmel. A címkére kattintva a kurzor automatikusan az űrlap mezőbe kerül. - -Minden elemhez beállíthatunk tetszőleges HTML attribútumokat. Például hozzáadhatunk egy placeholdert: - -```php -$form->addInteger('age', 'Életkor:') - ->setHtmlAttribute('placeholder', 'Kérjük, töltse ki az életkort'); -``` - -Az űrlap megjelenítésének módjai valóban nagyon sokfélék, ezért ennek egy [külön fejezetet szentelünk a megjelenítésről|rendering]. - - -Leképezés osztályokra -===================== - -Térjünk vissza a `formSucceeded()` metódushoz, amely a második paraméterben, a `$data`-ban kapja meg az elküldött adatokat `ArrayHash` objektumként. Mivel ez egy generikus osztály, valami olyasmi, mint a `stdClass`, hiányozni fog belőle bizonyos kényelem a munkavégzés során, mint például a property-k kódkiegészítése a szerkesztőkben vagy a statikus kódelemzés. Ezt meg lehetne oldani azzal, hogy minden űrlaphoz lenne egy konkrét osztályunk, amelynek property-jei az egyes elemeket reprezentálják. Pl.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternatívaként használhatja a konstruktort: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Az adatosztály property-jei lehetnek enumok is, és automatikusan leképeződnek. .{data-version:3.2.4} - -Hogyan mondjuk meg a Nette-nek, hogy az adatokat ennek az osztálynak az objektumaiként adja vissza? Könnyebben, mint gondolná. Elég csak az osztályt megadni a `$data` paraméter típusaként a kezelő metódusban: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $data a RegistrationFormData példánya - $name = $data->name; - // ... -} -``` - -Típusként megadható az `array` is, és akkor az adatokat tömbként adja át. - -Hasonló módon használható a `getValues()` függvény is, amelynek az osztály nevét vagy a hidratálandó objektumot paraméterként adjuk át: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Ha az űrlapok többszintű struktúrát alkotnak konténerekből, hozzon létre mindegyikhez külön osztályt: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -A leképezés ezután a `$person` property típusából felismeri, hogy a konténert a `PersonFormData` osztályra kell leképezni. Ha a property konténerek tömbjét tartalmazná, adja meg az `array` típust, és a leképezendő osztályt adja át közvetlenül a konténernek: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Az űrlap adatosztályának tervét legeneráltathatja a `Nette\Forms\Blueprint::dataClass($form)` metódussal, amely kiírja azt a böngésző oldalára. A kódot ezután elég kattintással kijelölni és bemásolni a projektbe. .{data-version:3.1.15} - - -Több gomb -========= - -Ha az űrlapnak több mint egy gombja van, általában meg kell különböztetnünk, hogy melyiket nyomták meg. Minden gombhoz létrehozhatunk saját kezelő függvényt. Ezt beállítjuk a [esemény |nette:glossary#Eventek események] `onClick` kezelőjeként: - -```php -$form->addSubmit('save', 'Mentés') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Törlés') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Ezek a handlerek csak érvényesen kitöltött űrlap esetén hívódnak meg, ugyanúgy, mint az `onSuccess` esemény esetén. A különbség az, hogy első paraméterként az űrlap helyett átadható a küldő gomb, attól függően, hogy milyen típust ad meg: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Amikor az űrlapot az <kbd>Enter</kbd> gombbal küldik el, az úgy tekintendő, mintha az első gombbal küldték volna el. - - -onAnchor esemény -================ - -Amikor a factory metódusban (mint pl. a `createComponentRegistrationForm`) összeállítjuk az űrlapot, az még nem tudja, hogy el lett-e küldve, sem azt, hogy milyen adatokkal. Vannak azonban esetek, amikor szükségünk van az elküldött értékek ismeretére, például ezek alapján alakul az űrlap további formája, vagy szükségünk van rájuk a függő selectboxokhoz stb. - -Az űrlapot összeállító kódrészletet ezért hagyhatjuk meghívni csak abban a pillanatban, amikor az ún. lehorgonyzott, tehát már kapcsolódik a presenterhez és ismeri az elküldött adatait. Ilyen kódot adunk át az `$onAnchor` tömbbe: - -```php -$country = $form->addSelect('country', 'Ország:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Város:'); - -$form->onAnchor[] = function () use ($country, $city) { - // ez a függvény csak akkor hívódik meg, amikor az űrlap már tudja, hogy el lett-e küldve és milyen adatokkal - // tehát használható a getValue() metódus - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Védelem a sebezhetőségekkel szemben -=================================== - -A Nette Framework nagy hangsúlyt fektet a biztonságra, ezért gondosan ügyel az űrlapok jó védelmére. Ezt teljesen átláthatóan teszi, és nem igényel manuális beállítást. - -Amellett, hogy az űrlapokat megvédi a [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] és a [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF] támadásoktól, számos apró biztonsági intézkedést tesz, amelyekre Önnek már nem kell gondolnia. - -Például kiszűri a bemenetekből az összes vezérlőkaraktert és ellenőrzi az UTF-8 kódolás érvényességét, így az űrlap adatai mindig tiszták lesznek. A select boxoknál és radio listáknál ellenőrzi, hogy a kiválasztott elemek valóban a kínáltak közül valók voltak-e, és nem történt-e hamisítás. Már említettük, hogy az egysoros szöveges bemeneteknél eltávolítja a sorvégi karaktereket, amelyeket egy támadó küldhetett. A többsoros bemeneteknél pedig normalizálja a sorvégi karaktereket. És így tovább. - -A Nette megoldja Ön helyett azokat a biztonsági kockázatokat, amelyekről sok programozó nem is tudja, hogy léteznek. - -Az említett CSRF támadás lényege, hogy a támadó ráveszi az áldozatot egy olyan oldalra, amely észrevétlenül végrehajt egy kérést az áldozat böngészőjében arra a szerverre, amelyen az áldozat be van jelentkezve, és a szerver azt hiszi, hogy a kérést az áldozat saját akaratából hajtotta végre. Ezért a Nette megakadályozza a POST űrlap elküldését más domainről. Ha valamilyen okból ki szeretné kapcsolni a védelmet, és engedélyezni szeretné az űrlap elküldését más domainről, használja a következőt: - -```php -$form->allowCrossOrigin(); // FIGYELEM! Kikapcsolja a védelmet! -``` - -Ez a védelem a `_nss` nevű SameSite cookie-t használja. A SameSite cookie segítségével történő védelem nem feltétlenül 100%-ban megbízható, ezért célszerű bekapcsolni a token alapú védelmet is: - -```php -$form->addProtection(); -``` - -Javasoljuk, hogy így védje az adminisztrációs felületen lévő űrlapokat, amelyek érzékeny adatokat módosítanak az alkalmazásban. A keretrendszer a CSRF támadás ellen egy engedélyezési token generálásával és ellenőrzésével védekezik, amely a sessionben tárolódik. Ezért szükséges, hogy az űrlap megjelenítése előtt nyitva legyen a session. Az adminisztrációs felületen általában már el van indítva a session a felhasználó bejelentkezése miatt. Ellenkező esetben indítsa el a sessiont a `Nette\Http\Session::start()` metódussal. - - -Ugyanaz az űrlap több presenterben -================================== - -Ha ugyanazt az űrlapot több presenterben is használni szeretné, javasoljuk, hogy hozzon létre hozzá egy factory-t, amelyet aztán átad a presenternek. Egy ilyen osztály megfelelő helye például az `app/Forms` könyvtár. - -A factory osztály például így nézhet ki: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Név:'); - $form->addSubmit('send', 'Bejelentkezés'); - return $form; - } -} -``` - -Az osztályt megkérjük az űrlap legyártására a presenter komponens factory metódusában: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // módosíthatjuk az űrlapot, itt például megváltoztatjuk a gomb címkéjét - $form['send']->setCaption('Folytatás'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // és hozzáadunk egy handlert - return $form; -} -``` - -Az űrlap feldolgozására szolgáló handler is származhat már a factory-ból: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Név:'); - $form->addSubmit('send', 'Bejelentkezés'); - $form->onSuccess[] = function (Form $form, $data): void { - // itt végezzük el az űrlap feldolgozását - }; - return $form; - } -} -``` - -Nos, túl vagyunk a Nette űrlapok gyors bevezetésén. Próbáljon meg még belenézni a disztribúció [examples|https://github.com/nette/forms/tree/master/examples] könyvtárába, ahol további inspirációt találhat. diff --git a/forms/hu/rendering.texy b/forms/hu/rendering.texy deleted file mode 100644 index 2362b0a634..0000000000 --- a/forms/hu/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Űrlapok megjelenítése -********************* - -Az űrlapok megjelenése nagyon változatos lehet. A gyakorlatban két szélsőséggel találkozhatunk. Az egyik oldalon az az igény áll, hogy az alkalmazásban számos olyan űrlapot jelenítsünk meg, amelyek vizuálisan hasonlítanak egymásra, mint két tojás, és értékelnénk az egyszerű megjelenítést sablon nélkül a `$form->render()` segítségével. Ez általában az adminisztrációs felületek esete. - -A másik oldalon pedig ott vannak a változatos űrlapok, amelyekre igaz: minden darab egyedi. Formájukat leginkább HTML nyelven írhatjuk le az űrlap sablonjában. És természetesen a két említett szélsőségen kívül számos olyan űrlappal találkozunk, amelyek valahol a kettő között helyezkednek el. - - -Megjelenítés Latte segítségével -=============================== - -A [Latte sablonrendszer|latte:] alapvetően megkönnyíti az űrlapok és elemeik megjelenítését. Először megmutatjuk, hogyan lehet az űrlapokat manuálisan, elemenként megjeleníteni, és ezzel teljes kontrollt szerezni a kód felett. Később megmutatjuk, hogyan lehet ezt a megjelenítést [automatizálni |#Automatikus megjelenítés]. - -Az űrlap Latte sablonjának tervét legeneráltathatja a `Nette\Forms\Blueprint::latte($form)` metódussal, amely kiírja azt a böngésző oldalára. A kódot ezután elég egy kattintással kijelölni és bemásolni a projektbe. .{data-version:3.1.15} - - -`{control}` ------------ - -Az űrlap megjelenítésének legegyszerűbb módja, ha a sablonba beírjuk: - -```latte -{control signInForm} -``` - -Az így megjelenített űrlap kinézetét a [#Renderer] és az [egyes elemek |#HTML attribútumok] konfigurálásával lehet befolyásolni. - - -`n:name` --------- - -Az űrlap definícióját a PHP kódban rendkívül egyszerűen össze lehet kapcsolni a HTML kóddal. Csak hozzá kell adni a `n:name` attribútumokat. Ennyire egyszerű! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Az eredményül kapott HTML kód formáját teljes mértékben Ön irányítja. Ha az `n:name` attribútumot a `<select>`, `<button>` vagy `<textarea>` elemeknél használja, azok belső tartalma automatikusan kiegészül. A `<form n:name>` tag ezenkívül létrehoz egy lokális `$form` változót a rajzolt űrlap objektumával, és a záró `</form>` megjeleníti az összes meg nem jelenített rejtett elemet (ugyanez érvényes a `{form} ... {/form}`-ra is). - -Nem szabad azonban elfelejtenünk a lehetséges hibaüzenetek megjelenítését. Mindazokat, amelyeket az `addError()` metódussal adtak hozzá az egyes elemekhez (a `{inputError}` segítségével), mindazokat, amelyeket közvetlenül az űrlaphoz adtak hozzá (ezeket a `$form->getOwnErrors()` adja vissza): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -A bonyolultabb űrlap elemeket, mint a RadioList vagy a CheckboxList, így lehet elemenként megjeleníteni: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Nem akar minden elemnél azon gondolkodni, hogy milyen HTML elemet használjon hozzá a sablonban, legyen az `<input>`, `<textarea>` stb? A megoldás az univerzális `{input}` tag: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Password: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Ha az űrlap fordítót használ, a `{label}` tageken belüli szöveg lefordításra kerül. - -Ebben az esetben is a bonyolultabb űrlap elemeket, mint a RadioList vagy a CheckboxList, elemenként lehet megjeleníteni: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Magának az `<input>` elemnek a megjelenítéséhez a Checkbox elemben használja a `{input myCheckbox:}`-t. A HTML attribútumokat ebben az esetben mindig vesszővel válassza el `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Kiírja a hibaüzenetet az űrlap elemhez, ha van ilyen. Az üzenetet általában HTML elembe csomagoljuk a stílusozás miatt. Az üres elem megjelenítésének elkerülését, ha nincs üzenet, elegánsan meg lehet oldani az `n:ifcontent` segítségével: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -A hiba jelenlétét a `hasErrors()` metódussal ellenőrizhetjük, és ennek megfelelően beállíthatjuk a szülő elem osztályát: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -A `{form signInForm}...{/form}` tagek alternatívái a `<form n:name="signInForm">...</form>`-nak. - - -Automatikus megjelenítés ------------------------- - -Az `{input}` és `{label}` tageknek köszönhetően könnyen létrehozhatunk egy általános sablont bármilyen űrlaphoz. Fokozatosan iterál és megjeleníti az összes elemét, kivéve a rejtett elemeket, amelyek automatikusan megjelennek az űrlap lezárásakor a `</form>` taggel. A megjelenítendő űrlap nevét a `$form` változóban fogja várni. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -A használt önlezáró páros `{label .../}` tagek a PHP kódban lévő űrlap definícióból származó címkéket jelenítik meg. - -Ezt az általános sablont mentse el például a `basic-form.latte` fájlba, és az űrlap megjelenítéséhez elég csak beilleszteni és átadni az űrlap nevét (vagy példányát) a `$form` paraméterbe: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Ha egy adott űrlap megjelenítésekor bele szeretne szólni a formájába, és például egy elemet másképp szeretne megjeleníteni, akkor a legegyszerűbb út, ha a sablonban előre elkészít blokkokat, amelyeket később felül lehet írni. A blokkoknak lehetnek [dinamikus nevek |latte:template-inheritance#Dinamikus blokknevek] is, így beléjük lehet illeszteni a megjelenítendő elem nevét is. Például: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Egy `username` nevű elemhez így létrejön az `input-username` blokk, amelyet könnyen felül lehet írni a [{embed} |latte:template-inheritance#Egység öröklődés embed] tag használatával: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Alternatívaként a `basic-form.latte` sablon teljes tartalmát [definiálni |latte:template-inheritance#Definíciók define] lehet blokként, beleértve a `$form` paramétert is: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Ennek köszönhetően kissé egyszerűbb lesz a hívása: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -A blokkot pedig elég egyetlen helyen importálni, a layout sablon elején: - -```latte -{import basic-form.latte} -``` - - -Speciális esetek ----------------- - -Ha csak az űrlap belső részét kell megjeleníteni a `<form>` HTML tagek nélkül, például snippek küldésekor, rejtse el őket az `n:tag-if` attribútummal: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Username: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -Az elemek megjelenítésében az űrlap konténeren belül segít a `{formContainer}` tag. - -```latte -<p>Melyik híreket szeretné megkapni:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Megjelenítés Latte nélkül -========================= - -Az űrlap megjelenítésének legegyszerűbb módja a következő hívás: - -```php -$form->render(); -``` - -Az így megjelenített űrlap kinézetét a [#Renderer] és az [egyes elemek |#HTML attribútumok] konfigurálásával lehet befolyásolni. - - -Manuális megjelenítés ---------------------- - -Minden űrlap elem rendelkezik metódusokkal, amelyek generálják az űrlap mező és a címke HTML kódját. Ezt visszaadhatják vagy stringként, vagy [Nette\Utils\Html|utils:html-elements] objektumként: - -- `getControl(): Html|string` visszaadja az elem HTML kódját -- `getLabel($caption = null): Html|string|null` visszaadja a címke HTML kódját, ha létezik - -Az űrlapot így elemenként lehet megjeleníteni: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -Míg egyes elemeknél a `getControl()` egyetlen HTML elemet ad vissza (pl. `<input>`, `<select>` stb.), másoknál egy egész HTML kódrészletet (CheckboxList, RadioList). Ebben az esetben használhatja azokat a metódusokat, amelyek az egyes inputokat és címkéket generálják, minden elemhez külön: - -- `getControlPart($key = null): ?Html` visszaadja egy elem HTML kódját -- `getLabelPart($key = null): ?Html` visszaadja egy elem címkéjének HTML kódját - -.[note] -Ezeknek a metódusoknak történelmi okokból `get` prefixük van, de jobb lenne a `generate`, mert minden híváskor új `Html` elemet hoznak létre és adnak vissza. - - -Renderer -======== - -Ez egy objektum, amely biztosítja az űrlap megjelenítését. Ezt a `$form->setRenderer` metódussal lehet beállítani. A vezérlés átadódik neki a `$form->render()` metódus hívásakor. - -Ha nem állítunk be saját renderert, az alapértelmezett megjelenítő [api:Nette\Forms\Rendering\DefaultFormRenderer] kerül felhasználásra. Ez az űrlap elemeit HTML táblázat formájában jeleníti meg. A kimenet így néz ki: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Név:</label></th> - - <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">Életkor:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Nem:</label></th> - ... -``` - -Az, hogy használjunk-e táblázatot az űrlap vázához, vitatható, és sok webdesigner más jelölést részesít előnyben. Például a definíciós listát. Ezért újrakonfiguráljuk a `DefaultFormRenderer`-t úgy, hogy az űrlapot lista formájában jelenítse meg. A konfiguráció a [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers] tömb szerkesztésével történik. Az első index mindig a területet, a második pedig annak attribútumát jelenti. Az egyes területeket az ábra mutatja: - -[* defaultformrenderer.webp *] - -Alapértelmezés szerint az `controls` elemcsoport egy `<table>`-ba van csomagolva, minden `pair` egy táblázat sort (`<tr>`) képvisel, a `label` és `control` páros pedig cellák (`<th>` és `<td>`). Most megváltoztatjuk a csomagoló elemeket. A `controls` területet egy `<dl>` konténerbe helyezzük, a `pair` területet konténer nélkül hagyjuk, a `label`-t `<dt>`-be, végül a `control`-t `<dd>` tagekkel csomagoljuk: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Az eredmény ez a HTML kód: - -```latte -<dl> - <dt><label class="required" for="frm-name">Név:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Életkor:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Nem:</label></dt> - ... -</dl> -``` - -A wrappers tömbben számos további attribútumot lehet befolyásolni: - -- CSS osztályok hozzáadása az egyes űrlap elem típusokhoz -- CSS osztállyal megkülönböztetni a páros és páratlan sorokat -- vizuálisan megkülönböztetni a kötelező és választható elemeket -- meghatározni, hogy a hibaüzenetek közvetlenül az elemeknél vagy az űrlap felett jelenjenek-e meg - - -Options -------- - -A Renderer viselkedését az egyes űrlap elemeken beállított *options* segítségével is lehet irányítani. Így lehet beállítani a leírást, amely a beviteli mező mellett jelenik meg: - -```php -$form->addText('phone', 'Szám:') - ->setOption('description', 'Ez a szám rejtve marad'); -``` - -Ha HTML tartalmat szeretnénk elhelyezni benne, használjuk a [Html |utils:html-elements] osztályt: - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Szám:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">A szám megőrzésének feltételei</a>') - ); -``` - -.[tip] -A Html elemet a címke helyett is lehet használni: `$form->addCheckbox('conditions', $label)`. - - -Elemek csoportosítása ---------------------- - -A Renderer lehetővé teszi az elemek vizuális csoportokba (fieldsetekbe) való csoportosítását: - -```php -$form->addGroup('Személyes adatok'); -``` - -Új csoport létrehozása után ez válik aktívvá, és minden újonnan hozzáadott elem egyúttal hozzáadódik ehhez a csoporthoz is. Tehát az űrlapot így lehet építeni: - -```php -$form = new Form; -$form->addGroup('Személyes adatok'); -$form->addText('name', 'Neved:'); -$form->addInteger('age', 'Korod:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Szállítási cím'); -$form->addCheckbox('send', 'Szállítás címre'); -$form->addText('street', 'Utca:'); -$form->addText('city', 'Város:'); -$form->addSelect('country', 'Ország:', $countries); -``` - -A Renderer először a csoportokat jeleníti meg, és csak utána azokat az elemeket, amelyek egyik csoportba sem tartoznak. - - -Támogatás a Bootstraphez ------------------------- - -[A példákban |https://github.com/nette/forms/tree/master/examples] találhatók minták arra, hogyan konfigurálja a Renderert a [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] és [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] számára. - - -HTML attribútumok -================= - -Tetszőleges HTML attribútumok beállításához az űrlap elemekhez használja a `setHtmlAttribute(string $name, $value = true)` metódust: - -```php -$form->addInteger('number', 'Szám:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Rendezés:', ['ár', 'név']) - ->setHtmlAttribute('onchange', 'submit()'); // változáskor küldés - - -// A <form> elem attribútumainak beállításához -$form->setHtmlAttribute('id', 'myForm'); -``` - -Az elem típusának specifikációja: - -```php -$form->addText('tel', 'Telefonod:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'írja be a telefonszámot'); -``` - -.[warning] -A típus és más attribútumok beállítása csak vizuális célokat szolgál. A bemenetek helyességének ellenőrzése a szerveroldalon kell, hogy történjen, amit a megfelelő [űrlap elem|controls] kiválasztásával és [érvényesítési szabályok|validation] megadásával biztosíthat. - -A rádió- vagy checkbox listák egyes elemeihez beállíthatunk HTML attribútumot különböző értékekkel mindegyikhez. Figyelje meg a kettőspontot a `style:` után, amely biztosítja az érték kiválasztását kulcs szerint: - -```php -$colors = ['r' => 'piros', 'g' => 'zöld', 'b' => 'kék']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Színek:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Kiírja: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">piros</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">zöld</label> -<label><input type="checkbox" name="colors[]" value="b">kék</label> -``` - -Logikai attribútumok, mint a `readonly`, beállításához használhatunk kérdőjeles írásmódot: - -```php -$form->addCheckboxList('colors', 'Színek:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // több kulcshoz használjon tömböt, pl. ['r', 'g'] -``` - -Kiírja: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">piros</label> -<label><input type="checkbox" name="colors[]" value="g">zöld</label> -<label><input type="checkbox" name="colors[]" value="b">kék</label> -``` - -Select boxok esetén a `setHtmlAttribute()` metódus a `<select>` elem attribútumait állítja be. Ha az egyes `<option>` elemek attribútumait szeretnénk beállítani, használjuk a `setOptionAttribute()` metódust. Működnek a fentebb említett kettőspontos és kérdőjeles írásmódok is: - -```php -$form->addSelect('colors', 'Színek:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Kiírja: - -```latte -<select name="colors"> - <option value="r" style="background:red">piros</option> - <option value="g" style="background:green">zöld</option> - <option value="b">kék</option> -</select> -``` - - -Prototípusok ------------- - -A HTML attribútumok beállításának alternatív módja a minta módosítása, amelyből a HTML elem generálódik. A minta egy `Html` objektum, és a `getControlPrototype()` metódus adja vissza: - -```php -$input = $form->addInteger('number', 'Szám:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -Ezzel a módszerrel módosítható a címke mintája is, amelyet a `getLabelPrototype()` ad vissza: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -A Checkbox, CheckboxList és RadioList elemeknél befolyásolhatja annak az elemnek a mintáját, amely az egész elemet csomagolja. Ezt a `getContainerPrototype()` adja vissza. Alapértelmezett állapotban ez egy „üres” elem, tehát semmi sem jelenik meg, de azzal, hogy nevet adunk neki, megjelenítésre kerül: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -CheckboxList és RadioList esetén befolyásolható az egyes elemek elválasztójának mintája is, amelyet a `getSeparatorPrototype()` metódus ad vissza. Alapértelmezett állapotban ez a `<br>` elem. Ha páros elemre változtatja, akkor az egyes elemeket csomagolni fogja elválasztás helyett. Továbbá befolyásolható az egyes elemek címkéjének HTML elem mintája is, amelyet a `getItemLabelPrototype()` ad vissza. - - -Fordítás -======== - -Ha többnyelvű alkalmazást programoz, valószínűleg szüksége lesz az űrlap különböző nyelvi változatokban történő megjelenítésére. A Nette Framework ehhez definiál egy fordítási interfészt [api:Nette\Localization\Translator]. A Nette-ben nincs alapértelmezett implementáció, választhat igényei szerint több kész megoldás közül, amelyeket a [Componette |https://componette.org/search/localization] oldalon talál. Dokumentációjukban megtudhatja, hogyan konfigurálja a fordítót. - -Az űrlapok támogatják a szövegek fordítón keresztüli kiírását. Ezt a `setTranslator()` metódussal adjuk át nekik: - -```php -$form->setTranslator($translator); -``` - -Ettől a pillanattól kezdve nemcsak az összes címke, hanem az összes hibaüzenet vagy select box elem is lefordításra kerül egy másik nyelvre. - -Az egyes űrlap elemeknél lehetőség van más fordító beállítására vagy a fordítás teljes kikapcsolására `null` értékkel: - -```php -$form->addSelect('carModel', 'Modell:', $cars) - ->setTranslator(null); -``` - -Az [érvényesítési szabályoknál|validation] a fordítónak specifikus paraméterek is átadásra kerülnek, például a szabálynál: - -```php -$form->addPassword('password', 'Jelszó:') - ->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8); -``` - -a fordító ezekkel a paraméterekkel hívódik meg: - -```php -$translator->translate('A jelszónak legalább %d karakter hosszúnak kell lennie', 8); -``` - -és így kiválaszthatja a `karakter` szó helyes többes számú alakját a szám alapján. - - -onRender esemény -================ - -Közvetlenül azelőtt, hogy az űrlap megjelenne, meghívathatjuk a kódunkat. Ez például kiegészítheti az űrlap elemeket HTML osztályokkal a helyes megjelenítés érdekében. A kódot az `onRender` tömbhöz adjuk hozzá: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/hu/standalone.texy b/forms/hu/standalone.texy deleted file mode 100644 index 8d53892ddf..0000000000 --- a/forms/hu/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Önállóan használt űrlapok -************************* - -.[perex] -A Nette Forms nagyságrendekkel megkönnyíti a webes űrlapok létrehozását és feldolgozását. Alkalmazásaiban teljesen önállóan is használhatja őket a keretrendszer többi része nélkül, amit ebben a fejezetben bemutatunk. - -Ha azonban a Nette Applicationt és a presentereket használja, akkor a [használat presenterekben|in-presenter] útmutató Önnek szól. - - -Első űrlap -========== - -Próbáljunk meg írni egy egyszerű regisztrációs űrlapot. A kódja a következő lesz ("teljes kód":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Név:'); -$form->addPassword('password', 'Jelszó:'); -$form->addSubmit('send', 'Regisztráció'); -``` - -Nagyon könnyen megjeleníthetjük: - -```php -$form->render(); -``` - -és a böngészőben így jelenik meg: - -[* form-cs.webp *] - -Az űrlap a `Nette\Forms\Form` osztály objektuma (a `Nette\Application\UI\Form` osztályt a presenterekben használják). Hozzáadtuk az úgynevezett név, jelszó elemeket és egy küldés gombot. - -Most pedig keltsük életre az űrlapot. A `$form->isSuccess()` lekérdezésével megtudjuk, hogy az űrlapot elküldték-e és érvényesen töltötték-e ki. Ha igen, kiírjuk az adatokat. Az űrlapdefiníció után tehát hozzáadjuk: - -```php -if ($form->isSuccess()) { - echo 'Az űrlap helyesen lett kitöltve és elküldve'; - $data = $form->getValues(); - // $data->name tartalmazza a nevet - // $data->password tartalmazza a jelszót - var_dump($data); -} -``` - -A `getValues()` metódus az elküldött adatokat [ArrayHash |utils:arrays#ArrayHash] objektum formájában adja vissza. Hogy ezt hogyan lehet megváltoztatni, azt [később |#Osztályokra való leképezés] mutatjuk be. A `$data` objektum tartalmazza a `name` és `password` kulcsokat a felhasználó által megadott adatokkal. - -Általában az adatokat azonnal további feldolgozásra küldjük, ami lehet például adatbázisba való beszúrás. A feldolgozás során azonban hiba léphet fel, például a felhasználónév már foglalt. Ebben az esetben a hibát az `addError()` segítségével visszaküldjük az űrlapnak, és újra megjelenítjük, a hibaüzenettel együtt. - -```php -$form->addError('Elnézést, ezt a felhasználónevet már használja valaki.'); -``` - -Az űrlap feldolgozása után átirányítunk a következő oldalra. Ez megakadályozza az űrlap nem kívánt újraküldését a *frissítés*, *vissza* gombbal vagy a böngésző előzményeiben való mozgással. - -Az űrlap alapértelmezés szerint POST metódussal és ugyanarra az oldalra küldődik. Mindkettő megváltoztatható: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -És ez tulajdonképpen minden :-) Van egy működő és tökéletesen [biztonságos |#Védelem a sebezhetőségek ellen] űrlapunk. - -Próbáljon meg hozzáadni más [űrlap elemeket|controls] is. - - -Elemekhez való hozzáférés -========================= - -Az űrlapot és annak egyes elemeit komponenseknek nevezzük. Komponensfát alkotnak, ahol a gyökér maga az űrlap. Az űrlap egyes elemeihez a következő módon férhetünk hozzá: - -```php -$input = $form->getComponent('name'); -// alternatív szintaxis: $input = $form['name']; - -$button = $form->getComponent('send'); -// alternatív szintaxis: $button = $form['send']; -``` - -Az elemeket az unset segítségével távolítjuk el: - -```php -unset($form['name']); -``` - - -Validációs szabályok -==================== - -Elhangzott az *érvényes* szó, de az űrlapnak még nincsenek validációs szabályai. Javítsuk ki ezt. - -A név kötelező lesz, ezért a `setRequired()` metódussal jelöljük meg, amelynek argumentuma a hibaüzenet szövege, amely akkor jelenik meg, ha a felhasználó nem tölti ki a nevet. Ha nem adunk meg argumentumot, az alapértelmezett hibaüzenet kerül felhasználásra. - -```php -$form->addText('name', 'Név:') - ->setRequired('Kérjük, adja meg a nevét'); -``` - -Próbálja meg elküldeni az űrlapot a név kitöltése nélkül, és látni fogja, hogy hibaüzenet jelenik meg, és a böngésző vagy a szerver addig elutasítja, amíg ki nem tölti a mezőt. - -Ugyanakkor a rendszert nem lehet becsapni azzal, hogy például csak szóközöket ír a mezőbe. Nem. A Nette automatikusan eltávolítja a bal és jobb oldali szóközöket. Próbálja ki. Ezt minden egysoros beviteli mezővel meg kellene tenni, de gyakran elfelejtik. A Nette ezt automatikusan megteszi. (Megpróbálhatja becsapni az űrlapot, és névként többsoros stringet küldeni. A Nette itt sem hagyja magát becsapni, és a sortöréseket szóközökre cseréli.) - -Az űrlap mindig a szerveroldalon validálódik, de JavaScript validáció is generálódik, amely villámgyorsan lefut, és a felhasználó azonnal értesül a hibáról, anélkül, hogy az űrlapot el kellene küldenie a szerverre. Ezt a `netteForms.js` szkript végzi. Illessze be az oldalba: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ha megnézi az űrlapot tartalmazó oldal forráskódját, észreveheti, hogy a Nette a kötelező elemeket `required` CSS osztállyal rendelkező elemekbe helyezi. Próbálja meg hozzáadni a következő stíluslapot a sablonhoz, és a „Név” címke piros lesz. Így elegánsan jelölhetjük meg a felhasználók számára a kötelező elemeket: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -További validációs szabályokat az `addRule()` metódussal adunk hozzá. Az első paraméter a szabály, a második ismét a hibaüzenet szövege, és még következhet a validációs szabály argumentuma. Mit jelent ez? - -Az űrlapot kibővítjük egy új, nem kötelező „kor” mezővel, amelynek egész számnak kell lennie (`addInteger()`), és ezen felül egy megengedett tartományban kell lennie (`$form::Range`). És itt pontosan az `addRule()` metódus harmadik paraméterét használjuk, amellyel átadjuk a validátornak a kívánt tartományt `[tól, ig]` párként: - -```php -$form->addInteger('age', 'Életkor:') - ->addRule($form::Range, 'Az életkornak 18 és 120 között kell lennie', [18, 120]); -``` - -.[tip] -Ha a felhasználó nem tölti ki a mezőt, a validációs szabályok nem kerülnek ellenőrzésre, mivel az elem nem kötelező. - -Itt van lehetőség egy kis refaktorálásra. A hibaüzenetben és a harmadik paraméterben a számok duplikáltan szerepelnek, ami nem ideális. Ha [többnyelvű űrlapokat |rendering#Fordítás] hoznánk létre, és a számokat tartalmazó üzenet több nyelvre lenne lefordítva, megnehezítené az értékek esetleges megváltoztatását. Emiatt lehetséges a `%d` helyettesítő karakterek használata, és a Nette kiegészíti az értékeket: - -```php - ->addRule($form::Range, 'Az életkornak %d és %d év között kell lennie', [18, 120]); -``` - -Térjünk vissza a `password` elemhez, amelyet szintén kötelezővé teszünk, és még ellenőrizzük a jelszó minimális hosszát (`$form::MinLength`), ismét a helyettesítő karakter használatával: - -```php -$form->addPassword('password', 'Jelszó:') - ->setRequired('Válasszon jelszót') - ->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8); -``` - -Adunk hozzá az űrlaphoz még egy `passwordVerify` mezőt, ahol a felhasználó még egyszer megadja a jelszót, ellenőrzés céljából. A validációs szabályok segítségével ellenőrizzük, hogy a két jelszó megegyezik-e (`$form::Equal`). És paraméterként hivatkozást adunk az első jelszóra a [szögletes zárójelek |#Elemekhez való hozzáférés] segítségével: - -```php -$form->addPassword('passwordVerify', 'Jelszó ellenőrzéshez:') - ->setRequired('Kérjük, adja meg a jelszót még egyszer ellenőrzés céljából') - ->addRule($form::Equal, 'A jelszavak nem egyeznek', $form['password']) - ->setOmitted(); -``` - -A `setOmitted()` segítségével megjelöltük azt az elemet, amelynek az értéke valójában nem számít, és amely csak a validáció miatt létezik. Az érték nem kerül átadásra a `$data`-ba. - -Ezzel van egy teljesen működőképes űrlapunk validációval PHP-ban és JavaScriptben is. A Nette validációs képességei sokkal szélesebbek, lehet feltételeket létrehozni, azok alapján megjeleníteni és elrejteni az oldal részeit stb. Mindent megtudhat az [űrlapok validációjáról|validation] szóló fejezetben. - - -Alapértelmezett értékek -======================= - -Az űrlap elemeinek általában alapértelmezett értékeket állítunk be: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Gyakran hasznos az összes elem alapértelmezett értékét egyszerre beállítani. Például, ha az űrlap rekordok szerkesztésére szolgál. Beolvassuk a rekordot az adatbázisból, és beállítjuk az alapértelmezett értékeket: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Hívja a `setDefaults()` metódust az elemek definiálása után. - - -Űrlap megjelenítése -=================== - -Alapértelmezés szerint az űrlap táblázatként jelenik meg. Az egyes elemek megfelelnek az alapvető hozzáférhetőségi szabálynak - minden címke `<label>`-ként van megírva, és a megfelelő űrlap elemhez van kapcsolva. A címkére kattintva a kurzor automatikusan az űrlap mezőjébe kerül. - -Minden elemhez beállíthatunk tetszőleges HTML attribútumokat. Például hozzáadhatunk egy placeholdert: - -```php -$form->addInteger('age', 'Életkor:') - ->setHtmlAttribute('placeholder', 'Kérjük, töltse ki az életkort'); -``` - -Az űrlap megjelenítésének módjai valóban nagyon sokfélék, ezért ennek [külön fejezetet szentelünk a megjelenítésről|rendering]. - - -Osztályokra való leképezés -========================== - -Térjünk vissza az űrlapadatok feldolgozásához. A `getValues()` metódus az elküldött adatokat `ArrayHash` objektumként adta vissza. Mivel ez egy generikus osztály, valami olyasmi, mint a `stdClass`, hiányozni fog belőle bizonyos kényelem a vele való munka során, mint például a propertyk súgása a szerkesztőkben vagy a statikus kódelemzés. Ezt úgy lehetne megoldani, hogy minden űrlaphoz lenne egy konkrét osztályunk, amelynek propertyjei az egyes elemeket reprezentálják. Pl.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternatívaként használhatja a konstruktort: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Az adatosztály propertyjei lehetnek enumok is, és automatikusan leképezésre kerülnek. .{data-version:3.2.4} - -Hogyan mondjuk meg a Nette-nek, hogy az adatokat ennek az osztálynak az objektumaiként adja vissza? Könnyebben, mint gondolná. Csak az osztály nevét vagy a hidratálandó objektumot kell paraméterként megadni: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Paraméterként megadható az `'array'` is, és akkor az adatokat tömbként adja vissza. - -Ha az űrlapok többszintű struktúrát alkotnak konténerekből, hozzon létre mindegyikhez külön osztályt: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -A leképezés ezután a `$person` property típusából tudja, hogy a konténert a `PersonFormData` osztályra kell leképeznie. Ha a property konténerek tömbjét tartalmazná, adja meg az `array` típust, és adja át a leképezendő osztályt közvetlenül a konténernek: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Az űrlap adatosztályának tervét legeneráltathatja a `Nette\Forms\Blueprint::dataClass($form)` metódussal, amely kiírja azt a böngésző oldalára. A kódot ezután elég egy kattintással kijelölni és a projektbe másolni. .{data-version:3.1.15} - - -Több gomb -========= - -Ha az űrlapnak több mint egy gombja van, általában meg kell különböztetnünk, melyiket nyomták meg. Ezt az információt a gomb `isSubmittedBy()` metódusa adja vissza: - -```php -$form->addSubmit('save', 'Mentés'); -$form->addSubmit('delete', 'Törlés'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Ne hagyja ki a `$form->isSuccess()` lekérdezést, ezzel ellenőrzi az adatok érvényességét. - -Amikor az űrlapot az <kbd>Enter</kbd> gombbal küldik el, úgy veszi, mintha az első gombbal küldték volna el. - - -Védelem a sebezhetőségek ellen -============================== - -A Nette Framework nagy hangsúlyt fektet a biztonságra, ezért gondosan ügyel az űrlapok megfelelő védelmére. - -Amellett, hogy az űrlapokat megvédi a [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] és a [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF] támadásoktól, számos apró biztonsági intézkedést tesz, amelyekre Önnek már nem kell gondolnia. - -Például kiszűri az összes vezérlőkaraktert a bemenetekből, és ellenőrzi az UTF-8 kódolás érvényességét, így az űrlap adatai mindig tiszták lesznek. A select boxoknál és radio listáknál ellenőrzi, hogy a kiválasztott elemek valóban a felajánlottak közül valók-e, és nem történt-e hamisítás. Már említettük, hogy az egysoros szöveges beviteleknél eltávolítja a sorvégi karaktereket, amelyeket a támadó küldhetett volna. A többsoros beviteleknél pedig normalizálja a sorvégi karaktereket. És így tovább. - -A Nette megoldja Ön helyett azokat a biztonsági kockázatokat, amelyekről sok programozó nem is tudja, hogy léteznek. - -Az említett CSRF támadás lényege, hogy a támadó egy olyan oldalra csalja az áldozatot, amely észrevétlenül végrehajt egy kérést az áldozat böngészőjében ahhoz a szerverhez, amelyen az áldozat be van jelentkezve, és a szerver azt hiszi, hogy a kérést az áldozat saját akaratából hajtotta végre. Ezért a Nette megakadályozza a POST űrlapok küldését más domainről. Ha valamilyen okból ki akarja kapcsolni a védelmet, és engedélyezni szeretné az űrlap küldését más domainről, használja a következőt: - -```php -$form->allowCrossOrigin(); // FIGYELEM! Kikapcsolja a védelmet! -``` - -Ez a védelem a `_nss` nevű SameSite cookie-t használja. Ezért hozza létre az űrlap objektumot még az első kimenet elküldése előtt, hogy a cookie elküldhető legyen. - -A SameSite cookie-val történő védelem nem feltétlenül 100%-ban megbízható, ezért célszerű bekapcsolni a token alapú védelmet is: - -```php -$form->addProtection(); -``` - -Javasoljuk, hogy így védje azokat az űrlapokat a webhely adminisztrációs részében, amelyek érzékeny adatokat módosítanak az alkalmazásban. A keretrendszer a CSRF támadás ellen egy engedélyezési token generálásával és ellenőrzésével védekezik, amelyet a sessionben tárol. Ezért az űrlap megjelenítése előtt nyitott sessionre van szükség. A webhely adminisztrációs részében általában már elindult a session a felhasználó bejelentkezése miatt. Ellenkező esetben indítsa el a sessiont a `Nette\Http\Session::start()` metódussal. - -Nos, ezzel végeztünk a Nette űrlapjainak gyors bemutatásával. Próbáljon meg még belenézni a disztribúció [examples|https://github.com/nette/forms/tree/master/examples] könyvtárába, ahol további inspirációt találhat. diff --git a/forms/hu/validation.texy b/forms/hu/validation.texy deleted file mode 100644 index 78249dc48a..0000000000 --- a/forms/hu/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Űrlap validáció -*************** - - -Kötelező elemek -=============== - -A kötelező elemeket a `setRequired()` metódussal jelöljük meg, amelynek argumentuma a [#hibaüzenetek] hibaüzenet szövege, amely akkor jelenik meg, ha a felhasználó nem tölti ki az elemet. Ha nem adunk meg argumentumot, az alapértelmezett hibaüzenet kerül felhasználásra. - -```php -$form->addText('name', 'Név:') - ->setRequired('Kérjük, adja meg a nevét'); -``` - - -Szabályok -========= - -Validációs szabályokat az elemekhez az `addRule()` metódussal adunk hozzá. Az első paraméter a szabály, a második a [#hibaüzenetek] hibaüzenet szövege, a harmadik pedig a validációs szabály argumentuma. - -```php -$form->addPassword('password', 'Jelszó:') - ->addRule($form::MinLength, 'A jelszónak legalább %d karakter hosszúnak kell lennie', 8); -``` - -**A validációs szabályok csak akkor kerülnek ellenőrzésre, ha a felhasználó kitöltötte az elemet.** - -A Nette számos előre definiált szabállyal rendelkezik, amelyek nevei a `Nette\Forms\Form` osztály konstansai. Minden elemhez használhatjuk ezeket a szabályokat: - -| konstans | leírás | argumentum típusa -|------- -| `Required` | kötelező elem, alias a `setRequired()` számára | - -| `Filled` | kötelező elem, alias a `setRequired()` számára | - -| `Blank` | az elem nem lehet kitöltve | - -| `Equal` | az érték megegyezik a paraméterrel | `mixed` -| `NotEqual` | az érték nem egyezik meg a paraméterrel | `mixed` -| `IsIn` | az érték megegyezik a tömb valamelyik elemével | `array` -| `IsNotIn` | az érték nem egyezik meg a tömb egyik elemével sem | `array` -| `Valid` | az elem helyesen van kitöltve? ([#feltételek] számára) | - - - -Szöveges bevitelek ------------------- - -Az `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` elemekhez használhatók a következő szabályok is: - -| `MinLength` | minimális szöveghossz | `int` -| `MaxLength` | maximális szöveghossz | `int` -| `Length` | hossz tartományban vagy pontos hossz | pár `[int, int]` vagy `int` -| `Email` | érvényes e-mail cím | - -| `URL` | abszolút URL | - -| `Pattern` | megfelel a reguláris kifejezésnek | `string` -| `PatternInsensitive` | mint a `Pattern`, de kis- és nagybetű érzéketlen | `string` -| `Integer` | egész szám érték | - -| `Numeric` | alias az `Integer` számára | - -| `Float` | szám | - -| `Min` | numerikus elem minimális értéke | `int\|float` -| `Max` | numerikus elem maximális értéke | `int\|float` -| `Range` | érték tartományban | pár `[int\|float, int\|float]` - -Az `Integer`, `Numeric` és `Float` validációs szabályok azonnal átalakítják az értéket integerre, illetve floatra. Továbbá az `URL` szabály elfogadja a séma nélküli címet is (pl. `nette.org`), és kiegészíti a sémát (`https://nette.org`). A `Pattern` és `PatternIcase` kifejezésnek az egész értékre kell érvényesnek lennie, azaz mintha `^` és `$` karakterekkel lenne körbevéve. - - -Elemek száma ------------- - -Az `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` elemekhez használhatók a következő szabályok is a kiválasztott elemek, illetve feltöltött fájlok számának korlátozására: - -| `MinLength` | minimális szám | `int` -| `MaxLength` | maximális szám | `int` -| `Length` | szám tartományban vagy pontos szám | pár `[int, int]` vagy `int` - - -Fájlfeltöltések ---------------- - -Az `addUpload()`, `addMultiUpload()` elemekhez használhatók a következő szabályok is: - -| `MaxFileSize` | maximális fájlméret bájtban | `int` -| `MimeType` | MIME típus, helyettesítő karakterek engedélyezettek (`'video/*'`) | `string\|string[]` -| `Image` | JPEG, PNG, GIF, WebP, AVIF kép | - -| `Pattern` | a fájlnév megfelel a reguláris kifejezésnek | `string` -| `PatternInsensitive` | mint a `Pattern`, de kis- és nagybetű érzéketlen | `string` - -A `MimeType` és `Image` szabályokhoz szükség van a `fileinfo` PHP kiterjesztésre. Azt, hogy a fájl vagy kép a kívánt típusú-e, az aláírása alapján észlelik, és **nem ellenőrzik az egész fájl integritását.** Azt, hogy a kép nem sérült-e, például a [betöltésével |http:request#toImage] lehet megállapítani. - - -Hibaüzenetek -============ - -Minden előre definiált szabálynak, kivéve a `Pattern` és `PatternInsensitive` szabályokat, van alapértelmezett hibaüzenete, így azt el lehet hagyni. Azonban az összes üzenet testreszabott megadásával és megfogalmazásával felhasználóbarátabbá teheti az űrlapot. - -Az alapértelmezett üzeneteket megváltoztathatja a [konfigurációban|forms:configuration], a `Nette\Forms\Validator::$messages` tömb szövegeinek módosításával, vagy a [fordító |rendering#Fordítás] használatával. - -A hibaüzenetek szövegében a következő helyettesítő stringek használhatók: - -| `%d` | sorban helyettesíti a szabály argumentumaival -| `%n$d` | helyettesíti a szabály n-edik argumentumával -| `%label` | helyettesíti az elem címkéjével (kettőspont nélkül) -| `%name` | helyettesíti az elem nevével (pl. `name`) -| `%value` | helyettesíti a felhasználó által beírt értékkel - -```php -$form->addText('name', 'Név:') - ->setRequired('Kérjük, töltse ki a %label mezőt'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'legalább %d és legfeljebb %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'legfeljebb %2$d és legalább %1$d', [5, 10]); -``` - - -Feltételek -========== - -A szabályokon kívül feltételeket is hozzáadhatunk. Ezeket hasonlóan írjuk, mint a szabályokat, csak az `addRule()` helyett az `addCondition()` metódust használjuk, és természetesen nem adunk meg hibaüzenetet (a feltétel csak kérdez): - -```php -$form->addPassword('password', 'Jelszó:') - // ha a jelszó nem hosszabb 8 karakternél - ->addCondition($form::MaxLength, 8) - // akkor számjegyet kell tartalmaznia - ->addRule($form::Pattern, 'Számjegyet kell tartalmaznia', '.*[0-9].*'); -``` - -A feltételt az aktuális elemen kívül más elemhez is köthetjük az `addConditionOn()` segítségével. Első paraméterként az elemre való hivatkozást adjuk meg. Ebben a példában az e-mail csak akkor lesz kötelező, ha a checkbox be van jelölve (az értéke true lesz): - -```php -$form->addCheckbox('newsletters', 'küldjenek nekem hírleveleket'); - -$form->addEmail('email', 'E-mail:') - // ha a checkbox be van jelölve - ->addConditionOn($form['newsletters'], $form::Equal, true) - // akkor követelje meg az e-mailt - ->setRequired('Adja meg az e-mail címét'); -``` - -A feltételekből komplex struktúrákat hozhatunk létre az `elseCondition()` és `endCondition()` segítségével: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // ha az első feltétel teljesül - ->addConditionOn(/* ... */) // és a második feltétel egy másik elemen - ->addRule(/* ... */) // követelje meg ezt a szabályt - ->elseCondition() // ha a második feltétel nem teljesül - ->addRule(/* ... */) // követelje meg ezeket a szabályokat - ->addRule(/* ... */) - ->endCondition() // visszatérünk az első feltételhez - ->addRule(/* ... */); -``` - -A Nette-ben nagyon könnyen reagálhatunk a feltétel teljesülésére vagy nem teljesülésére JavaScript oldalon is a `toggle()` metódus segítségével, lásd [#dinamikus JavaScript]. - - -Hivatkozás más elemre -===================== - -Szabály vagy feltétel argumentumaként más űrlap elemet is átadhatunk. A szabály ezután a felhasználó által később a böngészőben beírt értéket használja. Így például dinamikusan validálhatjuk, hogy a `password` elem ugyanazt a stringet tartalmazza-e, mint a `password_confirm` elem: - -```php -$form->addPassword('password', 'Jelszó'); -$form->addPassword('password_confirm', 'Jelszó megerősítése') - ->addRule($form::Equal, 'A megadott jelszavak nem egyeznek', $form['password']); -``` - - -Egyéni szabályok és feltételek -============================== - -Néha olyan helyzetbe kerülünk, amikor a Nette beépített validációs szabályai nem elegendőek, és a felhasználótól származó adatokat a saját módunkon kell validálnunk. A Nette-ben ez nagyon egyszerű! - -Az `addRule()` vagy `addCondition()` metódusoknak első paraméterként tetszőleges callbacket adhatunk át. Ez első paraméterként magát az elemet kapja, és boolean értéket ad vissza, amely meghatározza, hogy a validáció rendben lezajlott-e. Az `addRule()` segítségével történő szabály hozzáadásakor további argumentumokat is megadhatunk, ezeket aztán második paraméterként adjuk át. - -Így létrehozhatunk egy saját validátor készletet osztályként statikus metódusokkal: - -```php -class MyValidators -{ - // teszteli, hogy az érték osztható-e az argumentummal - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // további validátorok - } -} -``` - -A használat ezután nagyon egyszerű: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Az értéknek a %d szám többszörösének kell lennie', - 8, - ); -``` - -Egyéni validációs szabályokat JavaScripthez is hozzáadhatunk. A feltétel az, hogy a szabály statikus metódus legyen. A neve a JavaScript validátor számára az osztály nevének a visszaperjelek `\` nélküli, aláhúzásjel `_` és a metódus nevének összekapcsolásával jön létre. Pl. az `App\MyValidators::validateDivisibility`-t `AppMyValidators_validateDivisibility`-ként írjuk, és hozzáadjuk a `Nette.validators` objektumhoz: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -onValidate esemény -================== - -Az űrlap elküldése után validáció történik, amely során ellenőrzésre kerülnek az `addRule()` segítségével hozzáadott egyes szabályok, majd kiváltódik az [esemény |nette:glossary#Eventek események] `onValidate`. Ennek a kezelőjét (handler) kiegészítő validációra használhatjuk, tipikusan az értékek helyes kombinációjának ellenőrzésére több űrlap elemben. - -Ha hibát észlelünk, azt az `addError()` metódussal adjuk át az űrlapnak. Ezt vagy egy konkrét elemen, vagy közvetlenül az űrlapon hívhatjuk meg. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Ez a kombináció nem lehetséges.'); - } -} -``` - - -Hibák a feldolgozás során -========================= - -Sok esetben csak akkor értesülünk a hibáról, amikor az érvényes űrlapot dolgozzuk fel, például új elemet írunk az adatbázisba, és kulcsduplikációba ütközünk. Ebben az esetben a hibát ismét az `addError()` metódussal adjuk át az űrlapnak. Ezt vagy egy konkrét elemen, vagy közvetlenül az űrlapon hívhatjuk meg: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Érvénytelen jelszó.'); - } -} -``` - -Ha lehetséges, javasoljuk, hogy a hibát közvetlenül az űrlap eleméhez csatolja, mert az alapértelmezett renderer használatakor mellette jelenik meg. - -```php -$form['date']->addError('Elnézést, de ez a dátum már foglalt.'); -``` - -Az `addError()` metódust ismételten meghívhatja, és így több hibaüzenetet adhat át az űrlapnak vagy elemnek. Ezeket a `getErrors()` segítségével szerezheti meg. - -Figyelem, a `$form->getErrors()` az összes hibaüzenet összegzését adja vissza, beleértve azokat is, amelyeket közvetlenül az egyes elemekhez adtak át, nem csak közvetlenül az űrlaphoz. A csak az űrlaphoz átadott hibaüzeneteket a `$form->getOwnErrors()` segítségével szerezheti meg. - - -Bemenet módosítása -================== - -Az `addFilter()` metódus segítségével módosíthatjuk a felhasználó által beírt értéket. Ebben a példában toleráljuk és eltávolítjuk a szóközöket az irányítószámban: - -```php -$form->addText('zip', 'Irányítószám:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // eltávolítjuk a szóközöket az irányítószámból - }) - ->addRule($form::Pattern, 'Az irányítószám nem öt számjegyű', '\d{5}'); -``` - -A szűrő beépül a validációs szabályok és feltételek közé, tehát a metódusok sorrendje számít, azaz a szűrő és a szabály abban a sorrendben hívódik meg, ahogy az `addFilter()` és `addRule()` metódusok sorrendje van. - - -JavaScript validáció -==================== - -A feltételek és szabályok megfogalmazásának nyelve nagyon erős. Minden konstrukció működik mind a szerveroldalon, mind a JavaScript oldalon. HTML attribútumokban `data-nette-rules` JSON formátumban kerülnek átadásra. Magát a validációt pedig egy szkript végzi, amely elfogja az űrlap `submit` eseményét, végigmegy az egyes elemeken, és végrehajtja a megfelelő validációt. - -Ez a szkript a `netteForms.js`, és több lehetséges forrásból érhető el: - -A szkriptet közvetlenül beillesztheti a HTML oldalba egy CDN-ről: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Vagy másolja helyileg a projekt nyilvános mappájába (pl. a `vendor/nette/forms/src/assets/netteForms.min.js` fájlból): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Vagy telepítse [npm|https://www.npmjs.com/package/nette-forms] segítségével: - -```shell -npm install nette-forms -``` - -Majd töltse be és futtassa: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Alternatívaként betöltheti közvetlenül a `vendor` mappából: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -Dinamikus JavaScript -==================== - -Szeretné megjeleníteni a cím megadására szolgáló mezőket csak akkor, ha a felhasználó postai kézbesítést választ? Semmi probléma. A kulcs az `addCondition()` & `toggle()` metóduspár: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Ez a kód azt mondja, hogy amikor a feltétel teljesül, tehát amikor a checkbox be van jelölve, a `#address-container` HTML elem látható lesz. És fordítva. A címzett címét tartalmazó űrlap elemeket tehát egy ilyen ID-jű konténerbe helyezzük, és a checkboxra kattintva elrejtődnek vagy megjelennek. Ezt a `netteForms.js` szkript biztosítja. - -A `toggle()` metódus argumentumaként tetszőleges selectort adhatunk át. Történelmi okokból az alfanumerikus string további speciális karakterek nélkül elem ID-ként értelmeződik, tehát ugyanúgy, mintha `#` karakter előzné meg. A második, nem kötelező paraméter lehetővé teszi a viselkedés megfordítását, azaz ha a `toggle('#address-container', false)`-t használnánk, az elem éppen ellenkezőleg, csak akkor jelenne meg, ha a checkbox nem lenne bejelölve. - -Az alapértelmezett implementáció JavaScriptben az elemek `hidden` propertyjét változtatja meg. A viselkedést azonban könnyen megváltoztathatjuk, például animációt adhatunk hozzá. Elég JavaScriptben felülírni a `Nette.toggle` metódust saját megoldással: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // elrejtjük vagy megjelenítjük az 'el'-t a 'visible' értékétől függően - }); -}; -``` - - -Validáció kikapcsolása -====================== - -Néha hasznos lehet a validáció kikapcsolása. Ha a küldés gomb megnyomása nem kell, hogy validációt végezzen (alkalmas *Cancel* vagy *Preview* gombokhoz), kikapcsoljuk a `$submit->setValidationScope([])` metódussal. Ha csak részleges validációt kell végeznie, megadhatjuk, mely mezők vagy űrlap konténerek validálódjanak. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Az egész űrlapot validálja -$form->addSubmit('send2') - ->setValidationScope([]); // Egyáltalán nem validál -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Csak a name elemet validálja -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Csak az age elemet validálja -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // A details konténert validálja -``` - -A `setValidationScope` nem befolyásolja a [#onValidate esemény] eseményt az űrlapon, amely mindig meghívásra kerül. A konténer `onValidate` eseménye csak akkor kerül kiváltásra, ha ez a konténer részleges validációra van megjelölve. diff --git a/forms/pt/@home.texy b/forms/pt/@home.texy deleted file mode 100644 index 6a9f3c2857..0000000000 --- a/forms/pt/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Nette Forms revolucionou a criação de formulários web. De repente, bastava escrever algumas linhas de código compreensíveis e você tinha um formulário completo, incluindo renderização, validação JavaScript e do lado do servidor, e além disso, extremamente seguro. Mostraremos como - -- criar formulários amigáveis -- validar os dados enviados -- renderizar elementos exatamente conforme necessário - -</div> - - -Ao usar Nette Forms, você evitará uma série de tarefas rotineiras, como escrever validação (além disso, dupla, no lado do servidor e do cliente), minimizará a probabilidade de erros e falhas de segurança. - -Você pode usar formulários como parte da Aplicação Nette (ou seja, em presenters) ou de forma completamente independente. Como o uso difere um pouco em ambos os casos, preparamos dois guias para você: - -<div class="wiki-buttons"> -<div> "Formulários em presenters .[wiki-button]":in-presenter </div> -<div> "Formulários independentes .[wiki-button]":standalone </div> -</div> - - -Instalação ----------- - -Faça o download e instale a biblioteca usando a ferramenta [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/pt/@left-menu.texy b/forms/pt/@left-menu.texy deleted file mode 100644 index 9cf4dd5b9b..0000000000 --- a/forms/pt/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Introdução |@home] -- [Formulários em presenters|in-presenter] -- [Formulários independentes|standalone] -- [Elementos de formulário |controls] -- [Validação |validation] -- [Renderização |rendering] -- [Configuração |configuration] - - -Leitura adicional -***************** -- [Guias e melhores práticas |best-practices:] diff --git a/forms/pt/@meta.texy b/forms/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/forms/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/forms/pt/configuration.texy b/forms/pt/configuration.texy deleted file mode 100644 index e3538aa4f6..0000000000 --- a/forms/pt/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Configuração de Formulários -*************************** - -.[perex] -Na configuração, é possível alterar as [mensagens de erro padrão dos formulários|validation]. - -```neon -forms: - messages: - 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.' - 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.' -``` - -Aqui está a tradução para o português: - -```neon -forms: - messages: - Equal: 'Por favor, insira %s.' - NotEqual: 'Este valor não deve ser %s.' - Filled: 'Este campo é obrigatório.' - Blank: 'Este campo deve estar em branco.' - MinLength: 'Por favor, insira pelo menos %d caracteres.' - MaxLength: 'Por favor, insira no máximo %d caracteres.' - Length: 'Por favor, insira um valor entre %d e %d caracteres.' - Email: 'Por favor, insira um endereço de e-mail válido.' - URL: 'Por favor, insira uma URL válida.' - Integer: 'Por favor, insira um número inteiro válido.' - Float: 'Por favor, insira um número válido.' - Min: 'Por favor, insira um valor maior ou igual a %d.' - Max: 'Por favor, insira um valor menor ou igual a %d.' - Range: 'Por favor, insira um valor entre %d e %d.' - MaxFileSize: 'O tamanho do arquivo enviado pode ser de no máximo %d bytes.' - MaxPostSize: 'Os dados enviados excedem o limite de %d bytes.' - MimeType: 'O arquivo enviado não está no formato esperado.' - Image: 'O arquivo enviado deve ser uma imagem no formato JPEG, GIF, PNG, WebP ou AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Por favor, selecione uma opção válida.' - Nette\Forms\Controls\UploadControl::Valid: 'Ocorreu um erro durante o upload do arquivo.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Sua sessão expirou. Por favor, retorne à página inicial e tente novamente.' -``` - -Se você não usa o framework completo e, portanto, nem os arquivos de configuração, pode alterar as mensagens de erro padrão diretamente no array `Nette\Forms\Validator::$messages`. diff --git a/forms/pt/controls.texy b/forms/pt/controls.texy deleted file mode 100644 index 4b7b77ebbf..0000000000 --- a/forms/pt/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Elementos de Formulário -*********************** - -.[perex] -Visão geral dos elementos de formulário padrão. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Adiciona um campo de texto de linha única (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Se o usuário não preencher o campo, retorna uma string vazia `''`, ou usando `setNullable()` pode-se especificar que retorne `null`. - -```php -$form->addText('name', 'Nome:') - ->setRequired() - ->setNullable(); -``` - -Valida automaticamente UTF-8, remove espaços à esquerda e à direita e remove quebras de linha que um invasor poderia enviar. - -O comprimento máximo pode ser limitado usando `setMaxLength()`. Modificar o valor inserido pelo usuário é possível com [addFilter() |validation#Modificação da entrada]. - -Usando `setHtmlType()`, é possível alterar o caractere visual do campo de texto para tipos como `search`, `tel` ou `url`, veja a [especificação|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Lembre-se que a alteração do tipo é apenas visual e não substitui a função de validação. Para o tipo `url`, é apropriado adicionar uma [regra URL |validation#Entradas de texto] de validação específica. - -.[note] -Para outros tipos de entrada, como `number`, `range`, `email`, `date`, `datetime-local`, `time` e `color`, use métodos especializados como [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] e [#addColor], que garantem a validação do lado do servidor. Os tipos `month` e `week` ainda não são totalmente suportados em todos os navegadores. - -Ao elemento pode ser definido o chamado empty-value, que é algo como um valor padrão, mas se o usuário não o alterar, o elemento retorna uma string vazia ou `null`. - -```php -$form->addText('phone', 'Telefone:') - ->setHtmlType('tel') - ->setEmptyValue('+55'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Adiciona um campo para inserir texto multilinha (classe [TextArea |api:Nette\Forms\Controls\TextArea]). Se o usuário não preencher o campo, retorna uma string vazia `''`, ou usando `setNullable()` pode-se especificar que retorne `null`. - -```php -$form->addTextArea('note', 'Nota:') - ->addRule($form::MaxLength, 'A nota é muito longa', 10000); -``` - -Valida automaticamente UTF-8 e normaliza os separadores de linha para `\n`. Ao contrário do campo de entrada de linha única, não ocorre remoção de espaços. - -O comprimento máximo pode ser limitado usando `setMaxLength()`. Modificar o valor inserido pelo usuário é possível com [addFilter() |validation#Modificação da entrada]. É possível definir o chamado empty-value usando `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Adiciona um campo para inserir um número inteiro (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Retorna um inteiro ou `null` se o usuário não inserir nada. - -```php -$form->addInteger('year', 'Ano:') - ->addRule($form::Range, 'O ano deve estar no intervalo de %d a %d.', [1900, 2023]); -``` - -O elemento é renderizado como `<input type="number">`. Usando o método `setHtmlType()`, é possível alterar o tipo para `range` para exibição na forma de um controle deslizante, ou para `text`, se preferir um campo de texto padrão sem o comportamento especial do tipo `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Adiciona um campo para inserir um número decimal (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Retorna um float ou `null` se o usuário não inserir nada. - -```php -$form->addFloat('level', 'Nível:') - ->setDefaultValue(0) - ->addRule($form::Range, 'O nível deve estar no intervalo de %d a %d.', [0, 100]); -``` - -O elemento é renderizado como `<input type="number">`. Usando o método `setHtmlType()`, é possível alterar o tipo para `range` para exibição na forma de um controle deslizante, ou para `text`, se preferir um campo de texto padrão sem o comportamento especial do tipo `number`. - -Nette e o navegador Chrome aceitam tanto vírgula quanto ponto como separador decimal. Para que essa funcionalidade esteja disponível também no Firefox, é recomendado definir o atributo `lang` para o elemento específico ou para a página inteira, por exemplo, `<html lang="pt-BR">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Adiciona um campo para inserir um endereço de e-mail (classe [TextInput |api:Nette\Forms\Controls\TextInput]). Se o usuário não preencher o campo, retorna uma string vazia `''`, ou usando `setNullable()` pode-se especificar que retorne `null`. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Verifica se o valor é um endereço de e-mail válido. Não verifica se o domínio realmente existe, apenas a sintaxe é verificada. Valida automaticamente UTF-8, remove espaços à esquerda e à direita. - -O comprimento máximo pode ser limitado usando `setMaxLength()`. Modificar o valor inserido pelo usuário é possível com [addFilter() |validation#Modificação da entrada]. É possível definir o chamado empty-value usando `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Adiciona um campo para inserir uma senha (classe [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Senha:') - ->setRequired() - ->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8) - ->addRule($form::Pattern, 'Deve conter um dígito', '.*[0-9].*'); -``` - -Ao reexibir o formulário, o campo estará vazio. Valida automaticamente UTF-8, remove espaços à esquerda e à direita e remove quebras de linha que um invasor poderia enviar. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Adiciona uma caixa de seleção (classe [Checkbox |api:Nette\Forms\Controls\Checkbox]). Retorna o valor `true` ou `false`, dependendo se está marcada. - -```php -$form->addCheckbox('agree', 'Concordo com os termos') - ->setRequired('É necessário concordar com os termos'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Adiciona caixas de seleção para escolher vários itens (classe [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Retorna um array das chaves dos itens selecionados. O método `getSelectedItems()` retorna os valores em vez das chaves. - -```php -$form->addCheckboxList('colors', 'Cores:', [ - 'r' => 'vermelho', - 'g' => 'verde', - 'b' => 'azul', -]); -``` - -O array de itens oferecidos é passado como terceiro parâmetro ou pelo método `setItems()`. - -Usando `setDisabled(['r', 'g'])`, é possível desativar itens individuais. - -O elemento verifica automaticamente se não houve falsificação e se os itens selecionados estão realmente entre os oferecidos e não foram desativados. O método `getRawValue()` permite obter os itens enviados sem essa importante verificação. - -Ao definir os itens selecionados padrão, também verifica se eles estão entre os oferecidos, caso contrário, lança uma exceção. Essa verificação pode ser desativada usando `checkDefaultValue(false)`. - -Se você enviar o formulário pelo método `GET`, pode escolher um método de transmissão de dados mais compacto, que economiza o tamanho da query string. Ele é ativado definindo o atributo HTML do formulário: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Adiciona botões de opção (classe [RadioList |api:Nette\Forms\Controls\RadioList]). Retorna a chave do item selecionado, ou `null` se o usuário não selecionou nada. O método `getSelectedItem()` retorna o valor em vez da chave. - -```php -$sex = [ - 'm' => 'masculino', - 'f' => 'feminino', -]; -$form->addRadioList('gender', 'Sexo:', $sex); -``` - -O array de itens oferecidos é passado como terceiro parâmetro ou pelo método `setItems()`. - -Usando `setDisabled(['m', 'f'])`, é possível desativar itens individuais. - -O elemento verifica automaticamente se não houve falsificação e se o item selecionado está realmente entre os oferecidos e não foi desativado. O método `getRawValue()` permite obter o item enviado sem essa importante verificação. - -Ao definir o item selecionado padrão, também verifica se ele está entre os oferecidos, caso contrário, lança uma exceção. Essa verificação pode ser desativada usando `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Adiciona uma caixa de seleção (classe [SelectBox |api:Nette\Forms\Controls\SelectBox]). Retorna a chave do item selecionado, ou `null` se o usuário não selecionou nada. O método `getSelectedItem()` retorna o valor em vez da chave. - -```php -$countries = [ - 'BR' => 'Brasil', - 'PT' => 'Portugal', - 'GB' => 'Reino Unido', -]; - -$form->addSelect('country', 'País:', $countries) - ->setDefaultValue('BR'); -``` - -O array de itens oferecidos é passado como terceiro parâmetro ou pelo método `setItems()`. Os itens também podem ser um array bidimensional: - -```php -$countries = [ - 'Europa' => [ - 'CZ' => 'República Tcheca', - 'SK' => 'Eslováquia', - 'GB' => 'Reino Unido', - ], - 'CA' => 'Canadá', - 'US' => 'EUA', - '?' => 'outro', -]; -``` - -Nas caixas de seleção, o primeiro item geralmente tem um significado especial, servindo como um prompt para ação. Para adicionar tal item, use o método `setPrompt()`. - -```php -$form->addSelect('country', 'País:', $countries) - ->setPrompt('Escolha um país'); -``` - -Usando `setDisabled(['CZ', 'SK'])`, é possível desativar itens individuais. - -O elemento verifica automaticamente se não houve falsificação e se o item selecionado está realmente entre os oferecidos e não foi desativado. O método `getRawValue()` permite obter o item enviado sem essa importante verificação. - -Ao definir o item selecionado padrão, também verifica se ele está entre os oferecidos, caso contrário, lança uma exceção. Essa verificação pode ser desativada usando `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Adiciona uma caixa de seleção para escolher vários itens (classe [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Retorna um array das chaves dos itens selecionados. O método `getSelectedItems()` retorna os valores em vez das chaves. - -```php -$form->addMultiSelect('countries', 'Países:', $countries); -``` - -O array de itens oferecidos é passado como terceiro parâmetro ou pelo método `setItems()`. Os itens também podem ser um array bidimensional. - -Usando `setDisabled(['CZ', 'SK'])`, é possível desativar itens individuais. - -O elemento verifica automaticamente se não houve falsificação e se os itens selecionados estão realmente entre os oferecidos e não foram desativados. O método `getRawValue()` permite obter os itens enviados sem essa importante verificação. - -Ao definir os itens selecionados padrão, também verifica se eles estão entre os oferecidos, caso contrário, lança uma exceção. Essa verificação pode ser desativada usando `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Adiciona um campo para upload de arquivo (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Retorna um objeto [FileUpload |http:request#FileUpload], mesmo que o usuário não tenha enviado nenhum arquivo, o que pode ser verificado pelo método `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'O avatar deve ser JPEG, PNG, GIF, WebP ou AVIF.') - ->addRule($form::MaxFileSize, 'O tamanho máximo é 1 MB.', 1024 * 1024 /* 1 MB em bytes */); -``` - -Se o arquivo não for carregado corretamente, o formulário não é enviado com sucesso e um erro é exibido. Ou seja, em caso de envio bem-sucedido, não é necessário verificar o método `FileUpload::isOk()`. - -Nunca confie no nome original do arquivo retornado pelo método `FileUpload::getName()`, o cliente pode ter enviado um nome de arquivo malicioso com a intenção de danificar ou hackear sua aplicação. - -As regras `MimeType` e `Image` detectam o tipo necessário com base na assinatura do arquivo e não verificam sua integridade. Se a imagem está danificada pode ser verificado, por exemplo, tentando [carregá-la |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Adiciona um campo para upload de vários arquivos de uma vez (classe [UploadControl |api:Nette\Forms\Controls\UploadControl]). Retorna um array de objetos [FileUpload |http:request#FileUpload]. O método `FileUpload::hasFile()` em cada um deles retornará `true`. - -```php -$form->addMultiUpload('files', 'Arquivos:') - ->addRule($form::MaxLength, 'No máximo %d arquivos podem ser enviados', 10); -``` - -Se algum arquivo não for carregado corretamente, o formulário não é enviado com sucesso e um erro é exibido. Ou seja, em caso de envio bem-sucedido, não é necessário verificar o método `FileUpload::isOk()`. - -Nunca confie nos nomes originais dos arquivos retornados pelo método `FileUpload::getName()`, o cliente pode ter enviado um nome de arquivo malicioso com a intenção de danificar ou hackear sua aplicação. - -As regras `MimeType` e `Image` detectam o tipo necessário com base na assinatura do arquivo e não verificam sua integridade. Se a imagem está danificada pode ser verificado, por exemplo, tentando [carregá-la |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Adiciona um campo que permite ao usuário inserir facilmente uma data composta por ano, mês e dia (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Como valor padrão, aceita objetos que implementam a interface `DateTimeInterface`, uma string com a hora, ou um número representando um timestamp UNIX. O mesmo se aplica aos argumentos das regras `Min`, `Max` ou `Range`, que definem a data mínima e máxima permitida. - -```php -$form->addDate('date', 'Data:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'A data deve ter pelo menos um mês.', new DateTime('-1 month')); -``` - -Por padrão, retorna um objeto `DateTimeImmutable`, com o método `setFormat()` você pode especificar o [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ou timestamp: - -```php -$form->addDate('date', 'Data:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Adiciona um campo que permite ao usuário inserir facilmente uma hora composta por horas, minutos e opcionalmente segundos (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Como valor padrão, aceita objetos que implementam a interface `DateTimeInterface`, uma string com a hora, ou um número representando um timestamp UNIX. Desses inputs, apenas a informação de tempo é utilizada, a data é ignorada. O mesmo se aplica aos argumentos das regras `Min`, `Max` ou `Range`, que definem a hora mínima e máxima permitida. Se o valor mínimo definido for maior que o máximo, cria-se um intervalo de tempo que ultrapassa a meia-noite. - -```php -$form->addTime('time', 'Hora:', withSeconds: true) - ->addRule($form::Range, 'A hora deve estar no intervalo de %d a %d.', ['12:30', '13:30']); -``` - -Por padrão, retorna um objeto `DateTimeImmutable` (com a data de 1º de janeiro do ano 1), com o método `setFormat()` você pode especificar o [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Hora:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Adiciona um campo que permite ao usuário inserir facilmente data e hora compostas por ano, mês, dia, horas, minutos e opcionalmente segundos (classe [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Como valor padrão, aceita objetos que implementam a interface `DateTimeInterface`, uma string com a hora, ou um número representando um timestamp UNIX. O mesmo se aplica aos argumentos das regras `Min`, `Max` ou `Range`, que definem a data mínima e máxima permitida. - -```php -$form->addDateTime('datetime', 'Data e hora:') - ->setDefaultValue(new \DateTime) - ->addRule($form::Min, 'A data deve ter pelo menos um mês.', new \DateTime('-1 month')); -``` - -Por padrão, retorna um objeto `DateTimeImmutable`, com o método `setFormat()` você pode especificar o [formato de texto|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ou timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Adiciona um campo para seleção de cor (classe [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). A cor é uma string no formato `#rrggbb`. Se o usuário não fizer a escolha, retorna a cor preta `#000000`. - -```php -$form->addColor('color', 'Cor:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Adiciona um campo oculto (classe [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -Usando `setNullable()`, pode-se definir que retorne `null` em vez de uma string vazia. Modificar o valor enviado é possível com [addFilter() |validation#Modificação da entrada]. - -Embora o elemento esteja oculto, é **importante notar** que o valor ainda pode ser modificado ou falsificado por um invasor. Sempre verifique e valide cuidadosamente todos os valores recebidos no lado do servidor para evitar riscos de segurança associados à manipulação de dados. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Adiciona um botão de envio (classe [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Enviar'); -``` - -No formulário, é possível ter vários botões de envio: - -```php -$form->addSubmit('register', 'Registrar'); -$form->addSubmit('cancel', 'Cancelar'); -``` - -Para descobrir qual deles foi clicado, use: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Se você não quiser validar o formulário inteiro ao pressionar o botão (por exemplo, para botões *Cancelar* ou *Visualizar*), use [setValidationScope() |validation#Desativação da validação]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Adiciona um botão (classe [Button |api:Nette\Forms\Controls\Button]) que não tem função de envio. Pode ser usado para alguma outra função, por exemplo, chamar uma função JavaScript ao clicar. - -```php -$form->addButton('raise', 'Aumentar salário') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Adiciona um botão de envio na forma de uma imagem (classe [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image'); -``` - -Ao usar vários botões de envio, é possível descobrir qual foi clicado usando `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Adiciona um subformulário (classe [Container|api:Nette\Forms\Container]), ou seja, um contêiner, ao qual é possível adicionar outros elementos da mesma forma que os adicionamos ao formulário. Os métodos `setDefaults()` ou `getValues()` também funcionam. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Seu nome:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Seu nome:'); -$sub2->addEmail('email', 'Email:'); -``` - -Os dados enviados retornam como uma estrutura multidimensional: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Visão geral das configurações -============================= - -Para todos os elementos, podemos chamar os seguintes métodos (visão geral completa na [documentação da API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | define o valor padrão -| `getValue()` | obtém o valor atual -| `setOmitted()` | [#Omissão de valor] -| `setDisabled()` | [#Desativação de elementos] - -Renderização: -.[table-form-methods language-php] -| `setCaption($caption)` | altera o rótulo do elemento -| `setTranslator($translator)` | define o [tradutor |rendering#Tradução] -| `setHtmlAttribute($name, $value)` | define o [atributo HTML |rendering#Atributos HTML] do elemento -| `setHtmlId($id)` | define o atributo HTML `id` -| `setHtmlType($type)` | define o atributo HTML `type` -| `setHtmlName($name)` | define o atributo HTML `name` -| `setOption($key, $value)` | [configurações para renderização |rendering#Options] - -Validação: -.[table-form-methods language-php] -| `setRequired()` | [elemento obrigatório |validation] -| `addRule()` | define a [regra de validação |validation#Regras] -| `addCondition()`, `addConditionOn()` | define a [condição de validação |validation#Condições] -| `addError($message)` | [passagem de mensagem de erro |validation#Erros durante o processamento] - -Para os elementos `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, podem ser chamados os seguintes métodos: - -.[table-form-methods language-php] -| `setNullable()` | define se getValue() retorna `null` em vez de string vazia -| `setEmptyValue($value)` | define um valor especial que é considerado uma string vazia -| `setMaxLength($length)` | define o número máximo de caracteres permitidos -| `addFilter($filter)` | [modificação da entrada |validation#Modificação da entrada] - - -Omissão de valor -================ - -Se o valor preenchido pelo usuário não nos interessa, podemos omiti-lo do resultado do método `$form->getValues()` ou dos dados passados para os handlers usando `setOmitted()`. Isso é útil para várias senhas de verificação, elementos antispam, etc. - -```php -$form->addPassword('passwordVerify', 'Senha para verificação:') - ->setRequired('Por favor, digite a senha novamente para verificação') - ->addRule($form::Equal, 'As senhas não coincidem', $form['password']) - ->setOmitted(); -``` - - -Desativação de elementos -======================== - -Elementos podem ser desativados usando `setDisabled()`. Tal elemento não pode ser editado pelo usuário. - -```php -$form->addText('username', 'Nome de usuário:') - ->setDisabled(); -``` - -Elementos desativados não são enviados pelo navegador para o servidor, portanto, você não os encontrará nos dados retornados pela função `$form->getValues()`. No entanto, se você definir `setOmitted(false)`, o Nette incluirá seu valor padrão nesses dados. - -Ao chamar `setDisabled()`, por razões de segurança, **o valor do elemento é apagado**. Se você estiver definindo um valor padrão, é necessário fazê-lo após desativá-lo: - -```php -$form->addText('username', 'Nome de usuário:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Uma alternativa aos elementos desativados são elementos com o atributo HTML `readonly`, que o navegador envia para o servidor. Embora o elemento seja apenas para leitura, é **importante notar** que seu valor ainda pode ser modificado ou falsificado por um invasor. - - -Elementos personalizados -======================== - -Além da ampla gama de elementos de formulário embutidos, você pode adicionar elementos personalizados ao formulário desta forma: - -```php -$form->addComponent(new DateInput('Data:'), 'date'); -// sintaxe alternativa: $form['date'] = new DateInput('Data:'); -``` - -.[note] -O formulário é um descendente da classe [Container |component-model:#Container] e os elementos individuais são descendentes de [Component |component-model:#Component]. - -Existe uma maneira de definir novos métodos de formulário para adicionar elementos personalizados (por exemplo, `$form->addZip()`). São os chamados métodos de extensão. A desvantagem é que a sugestão nos editores não funcionará para eles. - -```php -use Nette\Forms\Container; - -// adicionamos o método 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, 'Pelo menos 5 números', '[0-9]{5}'); -}); - -// uso -$form->addZip('zip', 'CEP:'); -``` - - -Elementos de baixo nível -======================== - -Também é possível usar elementos que escrevemos apenas no template e não os adicionamos ao formulário com algum dos métodos `$form->addXyz()`. Por exemplo, ao listar registros do banco de dados sem saber antecipadamente quantos serão e quais serão seus IDs, e queremos exibir uma caixa de seleção ou botão de opção para cada linha, basta codificá-lo no template: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -E após o envio, obtemos o valor: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -onde o primeiro parâmetro é o tipo do elemento (`DataFile` para `type=file`, `DataLine` para entradas de linha única como `text`, `password`, `email`, etc. e `DataText` para todos os outros) e o segundo parâmetro `sel[]` corresponde ao atributo HTML name. O tipo do elemento pode ser combinado com o valor `DataKeys`, que preserva as chaves dos elementos. Isso é especialmente útil para `select`, `radioList` e `checkboxList`. - -O essencial é que `getHttpData()` retorna um valor sanitizado, neste caso, será sempre um array de strings UTF-8 válidas, independentemente do que um invasor tente enviar ao servidor. É análogo ao trabalho direto com `$_POST` ou `$_GET`, mas com a diferença essencial de que sempre retorna dados limpos, como você está acostumado com os elementos padrão dos formulários Nette. diff --git a/forms/pt/in-presenter.texy b/forms/pt/in-presenter.texy deleted file mode 100644 index 955d4dc3f3..0000000000 --- a/forms/pt/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Formulários em Presenters -************************* - -.[perex] -Nette Forms facilitam enormemente a criação e processamento de formulários web. Neste capítulo, você aprenderá a usar formulários dentro de presenters. - -Se você está interessado em como usá-los de forma totalmente independente do resto do framework, o guia para [uso independente|standalone] é para você. - - -Primeiro formulário -=================== - -Vamos tentar escrever um formulário de registro simples. Seu código será o seguinte: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Nome:'); -$form->addPassword('password', 'Senha:'); -$form->addSubmit('send', 'Registrar'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -e no navegador será exibido assim: - -[* form-cs.webp *] - -O formulário no presenter é um objeto da classe `Nette\Application\UI\Form`, seu predecessor `Nette\Forms\Form` é destinado ao uso independente. Adicionamos a ele os chamados elementos nome, senha e botão de envio. E, finalmente, a linha com `$form->onSuccess` diz que após o envio e validação bem-sucedida, o método `$this->formSucceeded()` deve ser chamado. - -Do ponto de vista do presenter, o formulário é um componente comum. Portanto, ele é tratado como um componente e incorporado ao presenter usando [métodos de fábrica |application:components#Métodos de fábrica]. Ficará assim: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Nome:'); - $form->addPassword('password', 'Senha:'); - $form->addSubmit('send', 'Registrar'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // aqui processamos os dados enviados pelo formulário - // $data->name contém o nome - // $data->password contém a senha - $this->flashMessage('Você foi registrado com sucesso.'); - $this->redirect('Home:'); - } -} -``` - -E no template, renderizamos o formulário com a tag `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Registro</h1> - -{control registrationForm} -``` - -E isso é basicamente tudo :-) Temos um formulário funcional e perfeitamente [seguro |#Proteção contra vulnerabilidades]. - -E agora você provavelmente está pensando que foi muito rápido, imaginando como é possível que o método `formSucceeded()` seja chamado e quais são os parâmetros que ele recebe. Certamente, você está certo, isso merece uma explicação. - -Nette introduz um mecanismo inovador que chamamos de [estilo Hollywood |application:components#Estilo Hollywood]. Em vez de você, como desenvolvedor, ter que perguntar constantemente se algo aconteceu ("o formulário foi enviado?", "foi enviado validamente?" e "não foi falsificado?"), você diz ao framework "quando o formulário estiver validamente preenchido, chame este método" e deixa o trabalho restante para ele. Se você programa em JavaScript, conhece bem este estilo de programação. Você escreve funções que são chamadas quando um determinado [evento |nette:glossary#Eventos] ocorre. E a linguagem passa os argumentos apropriados para elas. - -É exatamente assim que o código do presenter acima é construído. O array `$form->onSuccess` representa uma lista de callbacks PHP que o Nette chama no momento em que o formulário é enviado e preenchido corretamente (ou seja, é válido). Dentro do [ciclo de vida do presenter |application:presenters#Ciclo de vida do presenter], isso é chamado de sinal, eles são chamados após o método `action*` e antes do método `render*`. E para cada callback, ele passa como primeiro parâmetro o próprio formulário e como segundo os dados enviados na forma de um objeto [ArrayHash |utils:arrays#ArrayHash] por padrão (ou uma classe/array mapeado). Você pode omitir o primeiro parâmetro se não precisar do objeto do formulário. E o segundo parâmetro pode ser mais inteligente, mas falaremos sobre isso [mais tarde |#Mapeamento para classes]. - -O objeto `$data` contém as chaves `name` e `password` com os dados que o usuário preencheu. Geralmente, enviamos os dados diretamente para processamento adicional, que pode ser, por exemplo, inserção no banco de dados. Durante o processamento, no entanto, pode ocorrer um erro, por exemplo, o nome de usuário já está em uso. Nesse caso, passamos o erro de volta para o formulário usando `addError()` e o deixamos renderizar novamente, com a mensagem de erro. - -```php -$form->addError('Desculpe, o nome de usuário já está em uso.'); -``` - -Além de `onSuccess`, existe também `onSubmit`: os callbacks são chamados sempre após o envio do formulário, mesmo que não esteja preenchido corretamente. E também `onError`: os callbacks são chamados apenas se o envio não for válido. Eles são chamados mesmo se invalidarmos o formulário em `onSuccess` ou `onSubmit` usando `addError()`. - -Após processar o formulário, redirecionamos para a próxima página. Isso evita o reenvio indesejado do formulário pelo botão *atualizar*, *voltar* ou movimento no histórico do navegador. - -Tente adicionar outros [elementos de formulário|controls]. - - -Acesso aos elementos -==================== - -O formulário é um componente do presenter, em nosso caso chamado `registrationForm` (pelo nome do método de fábrica `createComponentRegistrationForm`), então em qualquer lugar no presenter você pode acessar o formulário usando: - -```php -$form = $this->getComponent('registrationForm'); -// sintaxe alternativa: $form = $this['registrationForm']; -``` - -Os elementos individuais do formulário também são componentes, então você pode acessá-los da mesma maneira: - -```php -$input = $form->getComponent('name'); // ou $input = $form['name']; -$button = $form->getComponent('send'); // ou $button = $form['send']; -``` - -Os elementos são removidos usando unset: - -```php -unset($form['name']); -``` - - -Regras de validação -=================== - -A palavra *válido* foi mencionada, mas o formulário ainda não tem regras de validação. Vamos corrigir isso. - -O nome será obrigatório, então o marcamos com o método `setRequired()`, cujo argumento é o texto da mensagem de erro que será exibida se o usuário não preencher o nome. Se o argumento não for fornecido, a mensagem de erro padrão será usada. - -```php -$form->addText('name', 'Nome:') - ->setRequired('Por favor, insira o nome'); -``` - -Tente enviar o formulário sem preencher o nome e você verá que uma mensagem de erro será exibida e o navegador ou servidor o rejeitará até que você preencha o campo. - -Ao mesmo tempo, você não pode enganar o sistema escrevendo apenas espaços no campo. De jeito nenhum. O Nette remove automaticamente os espaços à esquerda e à direita. Experimente. É algo que você deve fazer com cada entrada de linha única, mas muitas vezes é esquecido. O Nette faz isso automaticamente. (Você pode tentar enganar o formulário e enviar uma string multilinha como nome. Mesmo aqui, o Nette não se deixa enganar e transforma as quebras de linha em espaços.) - -O formulário é sempre validado no lado do servidor, mas também é gerada uma validação JavaScript, que ocorre instantaneamente e o usuário é informado sobre o erro imediatamente, sem a necessidade de enviar o formulário ao servidor. Isso é feito pelo script `netteForms.js`. Insira-o no template de layout: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Se você olhar o código-fonte da página com o formulário, poderá notar que o Nette insere os elementos obrigatórios em elementos com a classe CSS `required`. Tente adicionar a seguinte folha de estilo ao template e o rótulo "Nome" ficará vermelho. Elegantemente, marcamos os elementos obrigatórios para os usuários: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Outras regras de validação são adicionadas com o método `addRule()`. O primeiro parâmetro é a regra, o segundo é novamente o texto da mensagem de erro e pode ainda seguir um argumento da regra de validação. O que isso significa? - -Vamos estender o formulário com um novo campo opcional "idade", que deve ser um número inteiro (`addInteger()`) e, além disso, dentro de um intervalo permitido (`Form::Range`). E aqui usaremos o terceiro parâmetro do método `addRule()`, pelo qual passamos o intervalo necessário ao validador como um par `[de, até]`: - -```php -$form->addInteger('age', 'Idade:') - ->addRule($form::Range, 'A idade deve ser entre 18 e 120', [18, 120]); -``` - -.[tip] -Se o usuário não preencher o campo, as regras de validação não serão verificadas, pois o elemento é opcional. - -Aqui surge espaço para uma pequena refatoração. Na mensagem de erro e no terceiro parâmetro, os números são mencionados duplicadamente, o que não é ideal. Se estivéssemos criando [formulários multilíngues |rendering#Tradução] e a mensagem contendo números fosse traduzida para vários idiomas, dificultaria uma possível alteração dos valores. Por esse motivo, é possível usar os marcadores `%d` e o Nette completará os valores: - -```php - ->addRule($form::Range, 'A idade deve ser entre %d e %d anos', [18, 120]); -``` - -Voltemos ao elemento `password`, que também tornaremos obrigatório e verificaremos o comprimento mínimo da senha (`$form::MinLength`), novamente usando o marcador: - -```php -$form->addPassword('password', 'Senha:') - ->setRequired('Escolha uma senha') - ->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8); -``` - -Adicionaremos ao formulário ainda o campo `passwordVerify`, onde o usuário digita a senha novamente, para verificação. Usando regras de validação, verificamos se ambas as senhas são iguais (`$form::Equal`). E como parâmetro, damos uma referência à primeira senha usando [colchetes |#Acesso aos elementos]: - -```php -$form->addPassword('passwordVerify', 'Senha para verificação:') - ->setRequired('Por favor, digite a senha novamente para verificação') - ->addRule($form::Equal, 'As senhas não coincidem', $form['password']) - ->setOmitted(); -``` - -Usando `setOmitted()`, marcamos o elemento cujo valor realmente não nos importa e que existe apenas para fins de validação. O valor não é passado para `$data`. - -Com isso, temos um formulário totalmente funcional com validação em PHP e JavaScript. As capacidades de validação do Nette são muito mais amplas, é possível criar condições, deixar partes da página serem exibidas e ocultadas com base nelas, etc. Tudo será explicado no capítulo sobre [validação de formulários|validation]. - - -Valores padrão -============== - -Normalmente, definimos valores padrão para os elementos do formulário: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Muitas vezes, é útil definir valores padrão para todos os elementos de uma vez. Por exemplo, quando o formulário serve para editar registros. Lemos o registro do banco de dados e definimos os valores padrão: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Chame `setDefaults()` após definir os elementos. - - -Renderização do formulário -========================== - -Por padrão, o formulário é renderizado como uma tabela. Os elementos individuais cumprem a regra básica de acessibilidade - todos os rótulos são escritos como `<label>` e vinculados ao elemento de formulário correspondente. Ao clicar no rótulo, o cursor aparece automaticamente no campo do formulário. - -Podemos definir quaisquer atributos HTML para cada elemento. Por exemplo, adicionar um placeholder: - -```php -$form->addInteger('age', 'Idade:') - ->setHtmlAttribute('placeholder', 'Por favor, preencha a idade'); -``` - -Existem realmente muitas maneiras de renderizar um formulário, então há um [capítulo separado sobre renderização|rendering] dedicado a isso. - - -Mapeamento para classes .{mapeamento-para-classes} -================================================== - -Voltemos ao método `formSucceeded()`, que no segundo parâmetro `$data` recebe os dados enviados como um objeto `ArrayHash`. Como é uma classe genérica, algo como `stdClass`, sentiremos falta de certo conforto ao trabalhar com ela, como sugestão de propriedades nos editores ou análise estática de código. Isso poderia ser resolvido tendo uma classe específica para cada formulário, cujas propriedades representam os elementos individuais. Por exemplo: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativamente, você pode usar o construtor: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public ?int $age, - public string $password, - ) { - } -} -``` - -As propriedades da classe de dados também podem ser enumerações e serão mapeadas automaticamente. .{data-version:3.2.4} - -Como dizer ao Nette para nos retornar os dados como objetos desta classe? Mais fácil do que você pensa. Basta apenas indicar a classe como o tipo do parâmetro `$data` no método manipulador: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $name é uma instância de RegistrationFormData - $name = $data->name; - // ... -} -``` - -Como tipo, também pode ser indicado `array` e então os dados são passados como um array. - -Da mesma forma, pode-se usar o método `getValues()`, ao qual o nome da classe ou o objeto a ser hidratado é passado como parâmetro: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Se os formulários formarem uma estrutura multinível composta por contêineres, crie uma classe separada para cada um: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -O mapeamento então, a partir do tipo da propriedade `$person`, reconhece que deve mapear o contêiner para a classe `PersonFormData`. Se a propriedade contivesse um array de contêineres, indique o tipo `array` e passe a classe para mapeamento diretamente para o contêiner: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Você pode ter o design da classe de dados do formulário gerado usando o método `Nette\Forms\Blueprint::dataClass($form)`, que o imprime na página do navegador. O código então só precisa ser clicado, marcado e copiado para o projeto. .{data-version:3.1.15} - - -Vários botões -============= - -Se o formulário tiver mais de um botão, geralmente precisamos distinguir qual deles foi pressionado. Podemos criar nossa própria função manipuladora para cada botão. Definimo-la como um handler para o [evento |nette:glossary#Eventos] `onClick`: - -```php -$form->addSubmit('save', 'Salvar') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Excluir') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Esses handlers são chamados apenas no caso de um formulário validamente preenchido, assim como no caso do evento `onSuccess`. A diferença é que, como primeiro parâmetro, em vez do formulário, pode ser passado o botão de envio, dependendo do tipo que você indicar: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Quando o formulário é enviado com a tecla <kbd>Enter</kbd>, considera-se como se tivesse sido enviado pelo primeiro botão. - - -Evento onAnchor -=============== - -Quando, no método de fábrica (como `createComponentRegistrationForm`), montamos o formulário, ele ainda não sabe se foi enviado, nem com quais dados. Mas há casos em que precisamos conhecer os valores enviados, talvez a forma adicional do formulário dependa deles, ou precisemos deles para selectboxes dependentes, etc. - -Portanto, a parte do código que monta o formulário pode ser deixada para ser chamada apenas no momento em que ele está, por assim dizer, ancorado, ou seja, já está conectado ao presenter e conhece seus dados enviados. Tal código é passado para o array `$onAnchor`: - -```php -$country = $form->addSelect('country', 'País:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Cidade:'); - -$form->onAnchor[] = function () use ($country, $city) { - // esta função será chamada quando o formulário souber se foi enviado e com quais dados - // portanto, é possível usar o método getValue() - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Proteção contra vulnerabilidades .{proteção-contra-vulnerabilidades} -==================================================================== - -O Nette Framework dá grande ênfase à segurança e, portanto, cuida meticulosamente da boa segurança dos formulários. Faz isso de forma totalmente transparente e não requer nenhuma configuração manual. - -Além de proteger os formulários contra ataques de [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] e [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], ele realiza muitas pequenas proteções nas quais você não precisa mais pensar. - -Por exemplo, ele filtra todos os caracteres de controle das entradas e verifica a validade da codificação UTF-8, para que os dados do formulário estejam sempre limpos. Para caixas de seleção e listas de rádio, ele verifica se os itens selecionados estavam realmente entre os oferecidos e não foram falsificados. Já mencionamos que, para entradas de texto de linha única, ele remove os caracteres de fim de linha que um invasor poderia ter enviado. Para entradas multilinha, ele normaliza os caracteres de fim de linha. E assim por diante. - -O Nette resolve para você riscos de segurança que muitos programadores nem sabem que existem. - -O ataque CSRF mencionado consiste em um invasor atrair a vítima para uma página que, discretamente no navegador da vítima, executa uma requisição ao servidor no qual a vítima está logada, e o servidor acredita que a requisição foi feita pela vítima por vontade própria. Portanto, o Nette impede o envio de formulários POST de outro domínio. Se, por algum motivo, você quiser desativar a proteção e permitir o envio do formulário de outro domínio, use: - -```php -$form->allowCrossOrigin(); // ATENÇÃO! Desativa a proteção! -``` - -Esta proteção utiliza um cookie SameSite chamado `_nss`. A proteção usando o cookie SameSite pode não ser 100% confiável, por isso é aconselhável ativar também a proteção por token: - -```php -$form->addProtection(); -``` - -Recomendamos proteger assim os formulários na parte administrativa do site, que alteram dados sensíveis na aplicação. O framework se defende contra o ataque CSRF gerando e verificando um token de autorização, que é armazenado na sessão. Portanto, é necessário ter a sessão aberta antes de exibir o formulário. Na parte administrativa do site, a sessão geralmente já está iniciada devido ao login do usuário. Caso contrário, inicie a sessão com o método `Nette\Http\Session::start()`. - - -Mesmo formulário em vários presenters -===================================== - -Se você precisar usar o mesmo formulário em vários presenters, recomendamos criar uma fábrica para ele, que você então passará para o presenter. Um local adequado para tal classe é, por exemplo, o diretório `app/Forms`. - -A classe de fábrica pode parecer assim: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Nome:'); - $form->addSubmit('send', 'Entrar'); - return $form; - } -} -``` - -Pedimos à classe para produzir o formulário no método de fábrica para componentes no presenter: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // podemos modificar o formulário, aqui por exemplo mudamos o rótulo no botão - $form['send']->setCaption('Continuar'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // e adicionamos o handler - return $form; -} -``` - -O handler para processamento do formulário também pode ser fornecido pela fábrica: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Nome:'); - $form->addSubmit('send', 'Entrar'); - $form->onSuccess[] = function (Form $form, $data): void { - // aqui realizamos o processamento do formulário - }; - return $form; - } -} -``` - -Então, tivemos uma introdução rápida aos formulários no Nette. Tente dar uma olhada no diretório [examples|https://github.com/nette/forms/tree/master/examples] na distribuição, onde encontrará mais inspiração. diff --git a/forms/pt/rendering.texy b/forms/pt/rendering.texy deleted file mode 100644 index 520d0fb787..0000000000 --- a/forms/pt/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Renderização de Formulários -*************************** - -A aparência dos formulários pode variar muito. Na prática, podemos encontrar dois extremos. Por um lado, há a necessidade de renderizar vários formulários na aplicação que são visualmente tão semelhantes quanto dois ovos, e apreciamos a renderização fácil sem um template usando `$form->render()`. Este é geralmente o caso das interfaces administrativas. - -Por outro lado, existem formulários diversos onde a regra é: cada peça é um original. A sua forma é melhor descrita usando a linguagem HTML no template do formulário. E, claro, além dos dois extremos mencionados, encontraremos muitos formulários que se situam algures entre eles. - - -Renderização usando Latte -========================= - -O [Sistema de templates Latte|latte:] facilita fundamentalmente a renderização de formulários e dos seus controlos. Primeiro, mostraremos como renderizar formulários manualmente, controlo por controlo, obtendo assim controlo total sobre o código. Mais tarde, mostraremos como essa renderização pode ser [automatizada |#Renderização automática]. - -Pode gerar o design do template Latte do formulário usando o método `Nette\Forms\Blueprint::latte($form)`, que o imprime na página do navegador. Depois, basta clicar para selecionar o código e copiá-lo para o seu projeto. .{data-version:3.1.15} - - -`{control}` ------------ - -A maneira mais simples de renderizar um formulário é escrever no template: - -```latte -{control signInForm} -``` - -A aparência do formulário renderizado desta forma pode ser influenciada configurando o [#Renderer] e os [controlos individuais |#Atributos HTML]. - - -`n:name` --------- - -A definição do formulário no código PHP pode ser ligada de forma extremamente fácil ao código HTML. Basta adicionar atributos `n:name`. É tão fácil! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Tem controlo total sobre a forma do código HTML resultante. Se usar o atributo `n:name` nos elementos `<select>`, `<button>` ou `<textarea>`, o seu conteúdo interno será preenchido automaticamente. Além disso, a tag `<form n:name>` cria uma variável local `$form` com o objeto do formulário a ser desenhado, e a tag de fecho `</form>` renderiza todos os controlos ocultos não renderizados (o mesmo se aplica a `{form} ... {/form}`). - -No entanto, não devemos esquecer de renderizar possíveis mensagens de erro. Tanto aquelas que foram adicionadas aos controlos individuais pelo método `addError()` (usando `{inputError}`), como aquelas adicionadas diretamente ao formulário (retornadas por `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Controlos de formulário mais complexos, como RadioList ou CheckboxList, podem ser renderizados item por item desta forma: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Não quer pensar em que elemento HTML usar no template para cada controlo, seja `<input>`, `<textarea>`, etc.? A solução é a tag universal `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Password: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Se o formulário usar um tradutor, o texto dentro das tags `{label}` será traduzido. - -Mesmo neste caso, controlos de formulário mais complexos, como RadioList ou CheckboxList, podem ser renderizados item por item: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Para renderizar apenas o `<input>` no controlo Checkbox, use `{input myCheckbox:}`. Neste caso, separe sempre os atributos HTML com uma vírgula `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Exibe a mensagem de erro para um controlo de formulário, se houver alguma. A mensagem geralmente é envolvida num elemento HTML para estilização. Evitar a renderização de um elemento vazio, se não houver mensagem, pode ser feito elegantemente usando `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -A presença de um erro pode ser verificada com o método `hasErrors()` e, com base nisso, definir a classe do elemento pai: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -As tags `{form signInForm}...{/form}` são uma alternativa a `<form n:name="signInForm">...</form>`. - - -Renderização automática ------------------------ - -Graças às tags `{input}` e `{label}`, podemos facilmente criar um template genérico para qualquer formulário. Ele iterará e renderizará sequencialmente todos os seus controlos, exceto os controlos ocultos, que são renderizados automaticamente ao fechar o formulário com a tag `</form>`. O nome do formulário a ser renderizado será esperado na variável `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -As tags de par auto-fechadas `{label .../}` usadas exibem os rótulos provenientes da definição do formulário no código PHP. - -Guarde este template genérico, por exemplo, no ficheiro `basic-form.latte` e para renderizar o formulário, basta incluí-lo e passar o nome (ou instância) do formulário para o parâmetro `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Se quiser intervir na forma de um formulário específico durante a renderização e, por exemplo, renderizar um controlo de forma diferente, o caminho mais simples é preparar blocos no template que possam ser sobrescritos posteriormente. Os blocos também podem ter [nomes dinâmicos |latte:template-inheritance#Nomes de Blocos Dinâmicos], pelo que pode inserir o nome do controlo a ser renderizado neles. Por exemplo: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Para o controlo, por exemplo, `username`, será criado o bloco `input-username`, que pode ser facilmente sobrescrito usando a tag [{embed} |latte:template-inheritance#Herança de Unidade]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Alternativamente, todo o conteúdo do template `basic-form.latte` pode ser [definido |latte:template-inheritance#Definições] como um bloco, incluindo o parâmetro `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Graças a isso, a sua chamada será ligeiramente mais simples: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -O bloco só precisa ser importado num único local, no início do template de layout: - -```latte -{import basic-form.latte} -``` - - -Casos especiais ---------------- - -Se precisar de renderizar apenas a parte interna do formulário sem as tags HTML `<form>`, por exemplo, ao enviar snippets, oculte-as usando o atributo `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Username: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -A tag `{formContainer}` ajuda na renderização de controlos dentro de um contêiner de formulário. - -```latte -<p>Quais notícias deseja receber:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Renderização sem Latte -====================== - -A maneira mais simples de renderizar um formulário é chamar: - -```php -$form->render(); -``` - -A aparência do formulário renderizado desta forma pode ser influenciada configurando o [#Renderer] e os [controlos individuais |#Atributos HTML]. - - -Renderização manual -------------------- - -Cada controlo de formulário possui métodos que geram o código HTML do campo de formulário e do rótulo. Podem retorná-lo como uma string ou como um objeto [Nette\Utils\Html|utils:html-elements]: - -- `getControl(): Html|string` retorna o código HTML do controlo -- `getLabel($caption = null): Html|string|null` retorna o código HTML do rótulo, se existir - -O formulário pode, assim, ser renderizado controlo por controlo: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -Enquanto para alguns controlos `getControl()` retorna um único elemento HTML (por exemplo, `<input>`, `<select>`, etc.), para outros retorna um pedaço inteiro de código HTML (CheckboxList, RadioList). Nesse caso, pode usar métodos que geram inputs e rótulos individuais, para cada item separadamente: - -- `getControlPart($key = null): ?Html` retorna o código HTML de um item -- `getLabelPart($key = null): ?Html` retorna o código HTML do rótulo de um item - -.[note] -Estes métodos têm o prefixo `get` por razões históricas, mas `generate` seria melhor, pois a cada chamada criam e retornam um novo elemento `Html`. - - -Renderer -======== - -É um objeto que garante a renderização do formulário. Pode ser definido pelo método `$form->setRenderer`. O controlo é passado para ele quando o método `$form->render()` é chamado. - -Se não definirmos o nosso próprio renderizador, será usado o renderizador padrão [api:Nette\Forms\Rendering\DefaultFormRenderer]. Ele renderiza os controlos do formulário na forma de uma tabela HTML. A saída parece-se com isto: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Nome:</label></th> - - <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">Idade:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Sexo:</label></th> - ... -``` - -Se usar ou não uma tabela para a estrutura do formulário é discutível, e muitos web designers preferem outra marcação. Por exemplo, uma lista de definição. Reconfiguraremos, portanto, o `DefaultFormRenderer` para que ele renderize o formulário na forma de uma lista. A configuração é feita editando o array [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. O primeiro índice representa sempre a área e o segundo o seu atributo. As áreas individuais são mostradas na imagem: - -[* defaultformrenderer.webp *] - -Por padrão, o grupo de controlos `controls` é envolvido por uma tabela `<table>`, cada `pair` representa uma linha da tabela `<tr>` e o par `label` e `control` são células `<th>` e `<td>`. Agora mudaremos os elementos envolventes. Inseriremos a área `controls` num contêiner `<dl>`, deixaremos a área `pair` sem contêiner, inseriremos `label` em `<dt>` e, finalmente, envolveremos `control` com as tags `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -O resultado é este código HTML: - -```latte -<dl> - <dt><label class="required" for="frm-name">Nome:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Idade:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Sexo:</label></dt> - ... -</dl> -``` - -No array wrappers, é possível influenciar toda uma gama de outros atributos: - -- adicionar classes CSS a tipos individuais de controlos de formulário -- distinguir linhas pares e ímpares com classes CSS -- distinguir visualmente itens obrigatórios e opcionais -- determinar se as mensagens de erro serão exibidas diretamente nos controlos ou acima do formulário - - -Options -------- - -O comportamento do Renderer também pode ser controlado definindo *options* nos controlos de formulário individuais. Assim, pode-se definir um rótulo que será exibido ao lado do campo de entrada: - -```php -$form->addText('phone', 'Número:') - ->setOption('description', 'Este número permanecerá oculto'); -``` - -Se quisermos colocar conteúdo HTML nele, usamos a classe [Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Número:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Termos de armazenamento do seu número</a>') - ); -``` - -.[tip] -O elemento Html também pode ser usado em vez de um rótulo: `$form->addCheckbox('conditions', $label)`. - - -Agrupamento de controlos ------------------------- - -O Renderer permite agrupar controlos em grupos visuais (fieldsets): - -```php -$form->addGroup('Dados Pessoais'); -``` - -Após criar um novo grupo, ele torna-se ativo e cada controlo recém-adicionado também é adicionado a ele. Assim, o formulário pode ser construído desta forma: - -```php -$form = new Form; -$form->addGroup('Dados Pessoais'); -$form->addText('name', 'Seu nome:'); -$form->addInteger('age', 'Sua idade:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Endereço de entrega'); -$form->addCheckbox('send', 'Enviar para o endereço'); -$form->addText('street', 'Rua:'); -$form->addText('city', 'Cidade:'); -$form->addSelect('country', 'País:', $countries); -``` - -O Renderer primeiro renderiza os grupos e só depois os controlos que não pertencem a nenhum grupo. - - -Suporte para Bootstrap ----------------------- - -[Nos exemplos |https://github.com/nette/forms/tree/master/examples] encontrará exemplos de como configurar o Renderer para [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] e [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -Atributos HTML -============== - -Para definir quaisquer atributos HTML para controlos de formulário, usamos o método `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Número:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Ordenar por:', ['preço', 'nome']) - ->setHtmlAttribute('onchange', 'submit()'); // enviar ao alterar - - -// Para definir atributos do próprio <form> -$form->setHtmlAttribute('id', 'myForm'); -``` - -Especificação do tipo de controlo: - -```php -$form->addText('tel', 'Seu telefone:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'escreva o telefone'); -``` - -.[warning] -A definição do tipo e outros atributos servem apenas para fins visuais. A verificação da correção das entradas deve ocorrer no servidor, o que é garantido pela escolha do [controlo de formulário|controls] apropriado e pela indicação das [regras de validação|validation]. - -Para itens individuais em listas de rádio ou checkbox, podemos definir um atributo HTML com valores diferentes para cada um deles. Observe os dois pontos após `style:`, que garantem a seleção do valor pela chave: - -```php -$colors = ['r' => 'vermelho', 'g' => 'verde', 'b' => 'azul']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Cores:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Exibe: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">vermelho</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">verde</label> -<label><input type="checkbox" name="colors[]" value="b">azul</label> -``` - -Para definir atributos lógicos, como `readonly`, podemos usar a notação com ponto de interrogação: - -```php -$form->addCheckboxList('colors', 'Cores:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // para mais chaves use um array, ex. ['r', 'g'] -``` - -Exibe: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">vermelho</label> -<label><input type="checkbox" name="colors[]" value="g">verde</label> -<label><input type="checkbox" name="colors[]" value="b">azul</label> -``` - -No caso de selectboxes, o método `setHtmlAttribute()` define os atributos do elemento `<select>`. Se quisermos definir atributos para os `<option>` individuais, usamos o método `setOptionAttribute()`. As notações com dois pontos e ponto de interrogação mencionadas acima também funcionam: - -```php -$form->addSelect('colors', 'Cores:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Exibe: - -```latte -<select name="colors"> - <option value="r" style="background:red">vermelho</option> - <option value="g" style="background:green">verde</option> - <option value="b">azul</option> -</select> -``` - - -Protótipos ----------- - -Uma maneira alternativa de definir atributos HTML consiste em modificar o modelo a partir do qual o elemento HTML é gerado. O modelo é um objeto `Html` e é retornado pelo método `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Número:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -Desta forma, também é possível modificar o modelo do rótulo, que é retornado por `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -Para os controlos Checkbox, CheckboxList e RadioList, pode influenciar o modelo do elemento que envolve todo o controlo. Ele é retornado por `getContainerPrototype()`. No estado padrão, é um elemento "vazio", então nada é renderizado, mas ao definir um nome para ele, ele será renderizado: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -No caso de CheckboxList e RadioList, também é possível influenciar o modelo do separador dos itens individuais, que é retornado pelo método `getSeparatorPrototype()`. No estado padrão, é o elemento `<br>`. Se o alterar para um elemento de par, ele envolverá os itens individuais em vez de separá-los. E, além disso, é possível influenciar o modelo do elemento HTML do rótulo nos itens individuais, que é retornado por `getItemLabelPrototype()`. - - -Tradução -======== - -Se está a programar uma aplicação multilíngue, provavelmente precisará renderizar o formulário em diferentes versões de idioma. O Nette Framework define uma interface para tradução para este propósito [api:Nette\Localization\Translator]. No Nette, não há implementação padrão, pode escolher de acordo com as suas necessidades entre várias soluções prontas que encontra no [Componette |https://componette.org/search/localization]. Na sua documentação, aprenderá como configurar o tradutor. - -Os formulários suportam a exibição de textos através do tradutor. Passamos para eles usando o método `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -A partir deste momento, não apenas todos os rótulos, mas também todas as mensagens de erro ou itens de caixas de seleção serão traduzidos para outro idioma. - -Para controlos de formulário individuais, é possível definir um tradutor diferente ou desativar completamente a tradução com o valor `null`: - -```php -$form->addSelect('carModel', 'Modelo:', $cars) - ->setTranslator(null); -``` - -Para [regras de validação|validation], parâmetros específicos também são passados ao tradutor, por exemplo, para a regra: - -```php -$form->addPassword('password', 'Senha:') - ->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8); -``` - -o tradutor é chamado com estes parâmetros: - -```php -$translator->translate('A senha deve ter pelo menos %d caracteres', 8); -``` - -e, portanto, pode escolher a forma plural correta da palavra `caracteres` de acordo com o número. - - -Evento onRender -=============== - -Pouco antes de o formulário ser renderizado, podemos deixar o nosso código ser chamado. Ele pode, por exemplo, adicionar classes HTML aos controlos do formulário para exibição correta. Adicionamos o código ao array `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/pt/standalone.texy b/forms/pt/standalone.texy deleted file mode 100644 index d11c0202c9..0000000000 --- a/forms/pt/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Formulários Usados Sozinhos -*************************** - -.[perex] -Os Nette Forms facilitam muito a criação e o processamento de formulários web. Pode usá-los nas suas aplicações de forma totalmente independente do restante do framework, como mostraremos neste capítulo. - -No entanto, se usa Nette Application e presenters, o guia para [uso em presenters|in-presenter] é para si. - - -Primeiro formulário -=================== - -Vamos tentar escrever um formulário de registo simples. O código será o seguinte ("código completo":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Nome:'); -$form->addPassword('password', 'Senha:'); -$form->addSubmit('send', 'Registar'); -``` - -Podemos renderizá-lo facilmente: - -```php -$form->render(); -``` - -e no navegador ele será exibido assim: - -[* form-cs.webp *] - -O formulário é um objeto da classe `Nette\Forms\Form` (a classe `Nette\Application\UI\Form` é usada em presenters). Adicionamos a ele os chamados controlos nome, senha e um botão de envio. - -E agora vamos dar vida ao formulário. Perguntando `$form->isSuccess()`, descobrimos se o formulário foi enviado e se foi preenchido de forma válida. Se sim, exibimos os dados. Após a definição do formulário, adicionamos: - -```php -if ($form->isSuccess()) { - echo 'Formulário foi preenchido corretamente e enviado'; - $data = $form->getValues(); - // $data->name contém o nome - // $data->password contém a senha - var_dump($data); -} -``` - -O método `getValues()` retorna os dados enviados na forma de um objeto [ArrayHash |utils:arrays#ArrayHash]. Mostraremos como alterar isso [mais tarde |#Mapeamento para classes]. O objeto `$data` contém as chaves `name` e `password` com os dados que o utilizador preencheu. - -Normalmente, enviamos os dados diretamente para processamento posterior, que pode ser, por exemplo, inserção no banco de dados. No entanto, durante o processamento, pode ocorrer um erro, por exemplo, o nome de utilizador já está em uso. Nesse caso, passamos o erro de volta para o formulário usando `addError()` e o deixamos renderizar novamente, junto com a mensagem de erro. - -```php -$form->addError('Desculpe, este nome de utilizador já está em uso.'); -``` - -Após processar o formulário, redirecionamos para a próxima página. Isso evita o reenvio indesejado do formulário pelo botão *atualizar*, *voltar* ou movendo-se no histórico do navegador. - -O formulário é enviado por padrão pelo método POST para a mesma página. Ambos podem ser alterados: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -E isso é basicamente tudo :-) Temos um formulário funcional e perfeitamente [seguro |#Proteção contra vulnerabilidades]. - -Tente adicionar também outros [controlos de formulário|controls]. - - -Acesso aos controlos -==================== - -Chamamos o formulário e os seus controlos individuais de componentes. Eles formam uma árvore de componentes, onde a raiz é o formulário. Podemos aceder aos controlos individuais do formulário desta forma: - -```php -$input = $form->getComponent('name'); -// sintaxe alternativa: $input = $form['name']; - -$button = $form->getComponent('send'); -// sintaxe alternativa: $button = $form['send']; -``` - -Os controlos são removidos usando unset: - -```php -unset($form['name']); -``` - - -Regras de validação -=================== - -A palavra *válido* foi mencionada, mas o formulário ainda não possui regras de validação. Vamos corrigir isso. - -O nome será obrigatório, então marcamo-lo com o método `setRequired()`, cujo argumento é o texto da mensagem de erro que será exibida se o utilizador não preencher o nome. Se o argumento não for fornecido, a mensagem de erro padrão será usada. - -```php -$form->addText('name', 'Nome:') - ->setRequired('Por favor, insira o nome'); -``` - -Tente enviar o formulário sem preencher o nome e verá que uma mensagem de erro será exibida e o navegador ou servidor o rejeitará até que preencha o campo. - -Ao mesmo tempo, não enganará o sistema digitando, por exemplo, apenas espaços no campo. De jeito nenhum. Nette remove automaticamente os espaços à esquerda e à direita. Experimente. É algo que deveria sempre fazer com cada input de linha única, mas muitas vezes é esquecido. Nette faz isso automaticamente. (Pode tentar enganar o formulário e enviar uma string de várias linhas como nome. Mesmo aqui, Nette não se deixa enganar e transforma as quebras de linha em espaços.) - -O formulário é sempre validado no lado do servidor, mas também é gerada uma validação JavaScript, que ocorre instantaneamente e o utilizador é informado sobre o erro imediatamente, sem a necessidade de enviar o formulário ao servidor. Isso é feito pelo script `netteForms.js`. Insira-o na página: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Se olhar o código-fonte da página com o formulário, poderá notar que Nette insere os controlos obrigatórios em elementos com a classe CSS `required`. Tente adicionar a seguinte folha de estilo ao template e o rótulo "Nome" ficará vermelho. Desta forma, marcamos elegantemente os controlos obrigatórios para os utilizadores: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Adicionamos outras regras de validação com o método `addRule()`. O primeiro parâmetro é a regra, o segundo é novamente o texto da mensagem de erro e pode haver ainda um argumento da regra de validação. O que isso significa? - -Vamos estender o formulário com um novo campo opcional "idade", que deve ser um número inteiro (`addInteger()`) e, além disso, dentro de um intervalo permitido (`$form::Range`). E aqui usaremos o terceiro parâmetro do método `addRule()`, pelo qual passamos o intervalo desejado ao validador como um par `[de, até]`: - -```php -$form->addInteger('age', 'Idade:') - ->addRule($form::Range, 'A idade deve ser entre 18 e 120', [18, 120]); -``` - -.[tip] -Se o utilizador não preencher o campo, as regras de validação não serão verificadas, pois o controlo é opcional. - -Aqui surge espaço para uma pequena refatoração. Na mensagem de erro e no terceiro parâmetro, os números são listados em duplicidade, o que não é ideal. Se estivéssemos a criar [formulários multilíngues |rendering#Tradução] e a mensagem contendo números fosse traduzida para vários idiomas, uma eventual alteração dos valores seria dificultada. Por esse motivo, é possível usar os placeholders `%d` e Nette preencherá os valores: - -```php - ->addRule($form::Range, 'A idade deve ser entre %d e %d anos', [18, 120]); -``` - -Voltemos ao controlo `password`, que também tornaremos obrigatório e ainda verificaremos o comprimento mínimo da senha (`$form::MinLength`), novamente usando o placeholder: - -```php -$form->addPassword('password', 'Senha:') - ->setRequired('Escolha uma senha') - ->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8); -``` - -Adicionamos ao formulário também o campo `passwordVerify`, onde o utilizador digita a senha novamente, para verificação. Usando regras de validação, verificamos se ambas as senhas são iguais (`$form::Equal`). E como parâmetro, damos uma referência à primeira senha usando [colchetes |#Acesso aos controlos]: - -```php -$form->addPassword('passwordVerify', 'Senha para verificação:') - ->setRequired('Por favor, digite a senha novamente para verificação') - ->addRule($form::Equal, 'As senhas não coincidem', $form['password']) - ->setOmitted(); -``` - -Usando `setOmitted()`, marcamos o controlo cujo valor realmente não nos importa e que existe apenas para fins de validação. O valor não é passado para `$data`. - -Com isso, temos um formulário totalmente funcional com validação em PHP e JavaScript. As capacidades de validação de Nette são muito mais amplas, é possível criar condições, exibir e ocultar partes da página com base nelas, etc. Aprenderá tudo no capítulo sobre [validação de formulários|validation]. - - -Valores padrão -============== - -Normalmente, definimos valores padrão para os controlos do formulário: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Muitas vezes, é útil definir valores padrão para todos os controlos simultaneamente. Por exemplo, quando o formulário é usado para editar registos. Lemos o registo do banco de dados e definimos os valores padrão: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Chame `setDefaults()` após a definição dos controlos. - - -Renderização do formulário -========================== - -Por padrão, o formulário é renderizado como uma tabela. Os controlos individuais cumprem a regra básica de acessibilidade - todos os rótulos são escritos como `<label>` e vinculados ao controlo de formulário correspondente. Ao clicar no rótulo, o cursor aparece automaticamente no campo do formulário. - -Podemos definir atributos HTML arbitrários para cada controlo. Por exemplo, adicionar um placeholder: - -```php -$form->addInteger('age', 'Idade:') - ->setHtmlAttribute('placeholder', 'Por favor, preencha a idade'); -``` - -Existem realmente muitas maneiras de renderizar um formulário, então há um [capítulo separado sobre renderização|rendering] dedicado a isso. - - -Mapeamento para classes -======================= - -Voltemos ao processamento dos dados do formulário. O método `getValues()` retornou-nos os dados enviados como um objeto `ArrayHash`. Como é uma classe genérica, algo como `stdClass`, sentiremos falta de certo conforto ao trabalhar com ela, como sugestões de propriedades em editores ou análise estática de código. Isso poderia ser resolvido tendo uma classe específica para cada formulário, cujas propriedades representam os controlos individuais. Por exemplo: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativamente, pode usar o construtor: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -As propriedades da classe de dados também podem ser enums e serão mapeadas automaticamente. .{data-version:3.2.4} - -Como dizer ao Nette para nos retornar os dados como objetos desta classe? Mais fácil do que pensa. Basta fornecer o nome da classe ou o objeto a ser hidratado como parâmetro: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Também é possível fornecer `'array'` como parâmetro e, em seguida, os dados serão retornados como um array. - -Se os formulários formarem uma estrutura multinível composta por contêineres, crie uma classe separada para cada um: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -O mapeamento então reconhece pelo tipo da propriedade `$person` que deve mapear o contêiner para a classe `PersonFormData`. Se a propriedade contiver um array de contêineres, especifique o tipo `array` e passe a classe para mapeamento diretamente para o contêiner: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Pode gerar o design da classe de dados do formulário usando o método `Nette\Forms\Blueprint::dataClass($form)`, que o exibirá na página do navegador. Em seguida, basta clicar para selecionar o código e copiá-lo para o projeto. .{data-version:3.1.15} - - -Múltiplos botões -================ - -Se o formulário tiver mais de um botão, geralmente precisamos distinguir qual deles foi pressionado. Essa informação é retornada pelo método `isSubmittedBy()` do botão: - -```php -$form->addSubmit('save', 'Salvar'); -$form->addSubmit('delete', 'Excluir'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Não pule a verificação `$form->isSuccess()`, ela verifica a validade dos dados. - -Quando o formulário é enviado pressionando a tecla <kbd>Enter</kbd>, é considerado como se tivesse sido enviado pelo primeiro botão. - - -Proteção contra vulnerabilidades -================================ - -O Nette Framework dá grande ênfase à segurança e, portanto, cuida meticulosamente da boa segurança dos formulários. - -Além de proteger os formulários contra ataques [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] e [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], ele realiza muitas pequenas medidas de segurança com as quais já não precisa de se preocupar. - -Por exemplo, ele filtra todos os caracteres de controlo das entradas e verifica a validade da codificação UTF-8, para que os dados do formulário estejam sempre limpos. Para caixas de seleção e listas de rádio, ele verifica se os itens selecionados eram realmente das opções oferecidas e se não houve falsificação. Já mencionamos que, para entradas de texto de linha única, ele remove os caracteres de fim de linha que um invasor poderia ter enviado. Para entradas de várias linhas, ele normaliza os caracteres de fim de linha. E assim por diante. - -Nette resolve para si riscos de segurança que muitos programadores nem sabem que existem. - -O ataque CSRF mencionado consiste no facto de que o invasor atrai a vítima para uma página que, discretamente no navegador da vítima, executa uma requisição ao servidor no qual a vítima está logada, e o servidor acredita que a requisição foi executada pela vítima por sua própria vontade. Portanto, Nette impede o envio de formulários POST de outro domínio. Se, por algum motivo, quiser desativar a proteção e permitir o envio de formulários de outro domínio, use: - -```php -$form->allowCrossOrigin(); // ATENÇÃO! Desativa a proteção! -``` - -Esta proteção usa um cookie SameSite chamado `_nss`. Portanto, crie o objeto do formulário antes de enviar a primeira saída, para que o cookie possa ser enviado. - -A proteção usando o cookie SameSite pode não ser 100% confiável, por isso é aconselhável ativar também a proteção por token: - -```php -$form->addProtection(); -``` - -Recomendamos proteger desta forma os formulários na parte administrativa do site, que alteram dados sensíveis na aplicação. O framework defende-se contra o ataque CSRF gerando e verificando um token de autorização, que é armazenado na sessão. Portanto, é necessário ter a sessão aberta antes de exibir o formulário. Na parte administrativa do site, a sessão geralmente já está iniciada devido ao login do utilizador. Caso contrário, inicie a sessão com o método `Nette\Http\Session::start()`. - -Então, passamos por uma rápida introdução aos formulários em Nette. Tente dar uma olhada no diretório [examples|https://github.com/nette/forms/tree/master/examples] na distribuição, onde encontrará mais inspiração. diff --git a/forms/pt/validation.texy b/forms/pt/validation.texy deleted file mode 100644 index 3e4f378d64..0000000000 --- a/forms/pt/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Validação de formulários -************************ - - -Controlos obrigatórios -====================== - -Marcamos os controlos obrigatórios com o método `setRequired()`, cujo argumento é o texto da [mensagem de erro |#Mensagens de erro], que será exibida se o utilizador não preencher o controlo. Se o argumento não for fornecido, a mensagem de erro padrão será usada. - -```php -$form->addText('name', 'Nome:') - ->setRequired('Por favor, insira o nome'); -``` - - -Regras -====== - -Adicionamos regras de validação aos controlos usando o método `addRule()`. O primeiro parâmetro é a regra, o segundo é o texto da [mensagem de erro |#Mensagens de erro] e o terceiro é o argumento da regra de validação. - -```php -$form->addPassword('password', 'Senha:') - ->addRule($form::MinLength, 'A senha deve ter pelo menos %d caracteres', 8); -``` - -**As regras de validação são verificadas apenas se o utilizador preencher o controlo.** - -Nette vem com uma série de regras predefinidas, cujos nomes são constantes da classe `Nette\Forms\Form`. Podemos usar estas regras para todos os controlos: - -| constante | descrição | tipo de argumento -|------- -| `Required` | controlo obrigatório, alias para `setRequired()` | - -| `Filled` | controlo obrigatório, alias para `setRequired()` | - -| `Blank` | o controlo não deve ser preenchido | - -| `Equal` | o valor é igual ao parâmetro | `mixed` -| `NotEqual` | o valor não é igual ao parâmetro | `mixed` -| `IsIn` | o valor é igual a um dos itens no array | `array` -| `IsNotIn` | o valor não é igual a nenhum item no array | `array` -| `Valid` | o controlo está preenchido corretamente? (para [#Condições]) | - - - -Entradas de texto ------------------ - -Para os controlos `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()`, algumas das seguintes regras também podem ser usadas: - -| `MinLength` | comprimento mínimo do texto | `int` -| `MaxLength` | comprimento máximo do texto | `int` -| `Length` | comprimento no intervalo ou comprimento exato | par `[int, int]` ou `int` -| `Email` | endereço de e-mail válido | - -| `URL` | URL absoluta | - -| `Pattern` | corresponde à expressão regular | `string` -| `PatternInsensitive` | como `Pattern`, mas insensível a maiúsculas/minúsculas | `string` -| `Integer` | valor inteiro | - -| `Numeric` | alias para `Integer` | - -| `Float` | número | - -| `Min` | valor mínimo do controlo numérico | `int\|float` -| `Max` | valor máximo do controlo numérico | `int\|float` -| `Range` | valor no intervalo | par `[int\|float, int\|float]` - -As regras de validação `Integer`, `Numeric` e `Float` convertem diretamente o valor para inteiro ou float, respetivamente. Além disso, a regra `URL` também aceita um endereço sem esquema (por exemplo, `nette.org`) e adiciona o esquema (`https://nette.org`). A expressão em `Pattern` e `PatternIcase` deve corresponder a todo o valor, ou seja, como se estivesse envolvida pelos caracteres `^` e `$`. - - -Número de itens ---------------- - -Para os controlos `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()`, as seguintes regras também podem ser usadas para limitar o número de itens selecionados ou ficheiros enviados: - -| `MinLength` | número mínimo | `int` -| `MaxLength` | número máximo | `int` -| `Length` | número no intervalo ou número exato | par `[int, int]` ou `int` - - -Upload de ficheiros -------------------- - -Para os controlos `addUpload()`, `addMultiUpload()`, as seguintes regras também podem ser usadas: - -| `MaxFileSize` | tamanho máximo do ficheiro em bytes | `int` -| `MimeType` | tipo MIME, curingas permitidos (`'video/*'`) | `string\|string[]` -| `Image` | imagem JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | nome do ficheiro corresponde à expressão regular | `string` -| `PatternInsensitive` | como `Pattern`, mas insensível a maiúsculas/minúsculas | `string` - -`MimeType` e `Image` exigem a extensão PHP `fileinfo`. Elas detetam se um ficheiro ou imagem é do tipo desejado com base na sua assinatura e **não verificam a integridade de todo o ficheiro.** Se uma imagem não está danificada pode ser verificado, por exemplo, tentando [carregá-la |http:request#toImage]. - - -Mensagens de erro -================= - -Todas as regras predefinidas, exceto `Pattern` e `PatternInsensitive`, têm uma mensagem de erro padrão, então ela pode ser omitida. No entanto, fornecer e formular todas as mensagens sob medida tornará o formulário mais amigável ao utilizador. - -Pode alterar as mensagens padrão na [configuração|forms:configuration], editando os textos no array `Nette\Forms\Validator::$messages` ou usando um [tradutor |rendering#Tradução]. - -No texto das mensagens de erro, podem ser usadas as seguintes strings de placeholder: - -| `%d` | substitui sequencialmente pelos argumentos da regra -| `%n$d` | substitui pelo n-ésimo argumento da regra -| `%label` | substitui pelo rótulo do controlo (sem dois pontos) -| `%name` | substitui pelo nome do controlo (por exemplo, `name`) -| `%value` | substitui pelo valor inserido pelo utilizador - -```php -$form->addText('name', 'Nome:') - ->setRequired('Preencha por favor %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'pelo menos %d e no máximo %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'no máximo %2$d e pelo menos %1$d', [5, 10]); -``` - - -Condições -========= - -Além das regras, também é possível adicionar condições. Elas são escritas de forma semelhante às regras, mas em vez de `addRule()`, usamos o método `addCondition()` e, obviamente, não fornecemos nenhuma mensagem de erro (a condição apenas pergunta): - -```php -$form->addPassword('password', 'Senha:') - // se a senha não tiver mais de 8 caracteres - ->addCondition($form::MaxLength, 8) - // então deve conter um dígito - ->addRule($form::Pattern, 'Deve conter um dígito', '.*[0-9].*'); -``` - -A condição também pode ser vinculada a outro controlo que não o atual, usando `addConditionOn()`. Como primeiro parâmetro, fornecemos uma referência ao controlo. Neste exemplo, o e-mail será obrigatório apenas se a caixa de seleção for marcada (o seu valor será true): - -```php -$form->addCheckbox('newsletters', 'enviar-me newsletters'); - -$form->addEmail('email', 'E-mail:') - // se a caixa de seleção estiver marcada - ->addConditionOn($form['newsletters'], $form::Equal, true) - // então exija o e-mail - ->setRequired('Insira o endereço de e-mail'); -``` - -É possível criar estruturas complexas a partir de condições usando `elseCondition()` e `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // se a primeira condição for atendida - ->addConditionOn(/* ... */) // e a segunda condição em outro controlo - ->addRule(/* ... */) // exija esta regra - ->elseCondition() // se a segunda condição não for atendida - ->addRule(/* ... */) // exija estas regras - ->addRule(/* ... */) - ->endCondition() // voltamos à primeira condição - ->addRule(/* ... */); -``` - -Em Nette, é muito fácil reagir ao cumprimento ou não cumprimento de uma condição também no lado do JavaScript usando o método `toggle()`, veja [#JavaScript dinâmico]. - - -Referência a outro controlo -=========================== - -Como argumento de uma regra ou condição, também é possível passar outro controlo do formulário. A regra então usará o valor inserido posteriormente pelo utilizador no navegador. Desta forma, é possível, por exemplo, validar dinamicamente que o controlo `password` contém a mesma string que o controlo `password_confirm`: - -```php -$form->addPassword('password', 'Senha'); -$form->addPassword('password_confirm', 'Confirme a senha') - ->addRule($form::Equal, 'As senhas inseridas não coincidem', $form['password']); -``` - - -Regras e condições personalizadas -================================= - -Ocasionalmente, chegamos a uma situação em que as regras de validação incorporadas em Nette não são suficientes e precisamos validar os dados do utilizador à nossa maneira. Em Nette, isso é muito simples! - -Aos métodos `addRule()` ou `addCondition()`, é possível passar qualquer callback como primeiro parâmetro. Ele recebe o próprio controlo como primeiro parâmetro e retorna um valor booleano indicando se a validação foi bem-sucedida. Ao adicionar uma regra usando `addRule()`, é possível fornecer argumentos adicionais, que são então passados como segundo parâmetro. - -Podemos criar o nosso próprio conjunto de validadores como uma classe com métodos estáticos: - -```php -class MyValidators -{ - // testa se o valor é divisível pelo argumento - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // outros validadores - } -} -``` - -O uso é então muito simples: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'O valor deve ser um múltiplo de %d', - 8, - ); -``` - -Regras de validação personalizadas também podem ser adicionadas ao JavaScript. A condição é que a regra seja um método estático. O seu nome para o validador JavaScript é formado pela junção do nome da classe sem barras invertidas `\`, um sublinhado `_` e o nome do método. Por exemplo, `App\MyValidators::validateDivisibility` é escrito como `AppMyValidators_validateDivisibility` e adicionado ao objeto `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Evento onValidate -================= - -Após o envio do formulário, a validação é realizada, onde as regras individuais adicionadas via `addRule()` são verificadas e, em seguida, o [evento |nette:glossary#Eventos] `onValidate` é disparado. O seu handler pode ser usado para validação adicional, tipicamente para verificar a combinação correta de valores em múltiplos controlos do formulário. - -Se um erro for detetado, passamos para o formulário usando o método `addError()`. Ele pode ser chamado num controlo específico ou diretamente no formulário. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Esta combinação não é possível.'); - } -} -``` - - -Erros durante o processamento -============================= - -Em muitos casos, descobrimos um erro apenas no momento em que estamos a processar um formulário válido, por exemplo, ao inserir um novo item no banco de dados e encontrar uma duplicidade de chaves. Nesse caso, passamos novamente o erro para o formulário usando o método `addError()`. Ele pode ser chamado num controlo específico ou diretamente no formulário: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Senha inválida.'); - } -} -``` - -Se possível, recomendamos anexar o erro diretamente ao controlo do formulário, pois ele será exibido ao lado dele ao usar o renderizador padrão. - -```php -$form['date']->addError('Desculpe, mas esta data já está ocupada.'); -``` - -Pode chamar `addError()` repetidamente para passar várias mensagens de erro ao formulário ou controlo. Pode obtê-las usando `getErrors()`. - -Atenção, `$form->getErrors()` retorna um resumo de todas as mensagens de erro, incluindo aquelas que foram passadas diretamente para controlos individuais, não apenas diretamente para o formulário. Mensagens de erro passadas apenas para o formulário podem ser obtidas via `$form->getOwnErrors()`. - - -Modificação da entrada -====================== - -Usando o método `addFilter()`, podemos modificar o valor inserido pelo utilizador. Neste exemplo, toleraremos e removeremos espaços no código postal: - -```php -$form->addText('zip', 'Código Postal:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // removemos espaços do código postal - }) - ->addRule($form::Pattern, 'Código Postal não está no formato de cinco dígitos', '\d{5}'); -``` - -O filtro é integrado entre as regras de validação e condições, portanto, a ordem dos métodos importa, ou seja, o filtro e a regra são chamados na mesma ordem que os métodos `addFilter()` e `addRule()`. - - -Validação JavaScript -==================== - -A linguagem para formular condições e regras é muito poderosa. Todas as construções funcionam tanto no lado do servidor quanto no lado do JavaScript. Elas são transferidas em atributos HTML `data-nette-rules` como JSON. A validação em si é então realizada por um script que captura o evento `submit` do formulário, percorre os controlos individuais e executa a validação apropriada. - -Esse script é `netteForms.js` e está disponível em várias fontes possíveis: - -Pode inserir o script diretamente na página HTML a partir de um CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ou copiá-lo localmente para a pasta pública do projeto (por exemplo, de `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Ou instalar via [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -E, em seguida, carregar e executar: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Alternativamente, pode carregá-lo diretamente da pasta `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -JavaScript dinâmico -=================== - -Quer exibir os campos para inserir o endereço apenas se o utilizador escolher enviar o produto pelo correio? Sem problemas. A chave é o par de métodos `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Este código diz que quando a condição é atendida, ou seja, quando a caixa de seleção está marcada, o elemento HTML `#address-container` será visível. E vice-versa. Assim, colocamos os controlos do formulário com o endereço do destinatário num contêiner com este ID e, ao clicar na caixa de seleção, eles serão ocultados ou exibidos. Isso é garantido pelo script `netteForms.js`. - -Como argumento do método `toggle()`, é possível passar qualquer seletor. Por razões históricas, uma string alfanumérica sem outros caracteres especiais é entendida como o ID do elemento, ou seja, da mesma forma que se fosse precedida pelo caractere `#`. O segundo parâmetro opcional permite inverter o comportamento, ou seja, se usássemos `toggle('#address-container', false)`, o elemento seria exibido apenas se a caixa de seleção não estivesse marcada. - -A implementação padrão em JavaScript altera a propriedade `hidden` dos elementos. No entanto, podemos facilmente alterar o comportamento, por exemplo, adicionando uma animação. Basta sobrescrever o método `Nette.toggle` em JavaScript com a sua própria solução: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // ocultamos ou exibimos 'el' de acordo com o valor 'visible' - }); -}; -``` - - -Desativação da validação -======================== - -Às vezes, pode ser útil desativar a validação. Se o pressionamento de um botão de envio não deve realizar a validação (adequado para botões *Cancelar* ou *Visualizar*), desativamo-la com o método `$submit->setValidationScope([])`. Se deve realizar apenas validação parcial, podemos especificar quais campos ou contêineres de formulário devem ser validados. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Valida o formulário inteiro -$form->addSubmit('send2') - ->setValidationScope([]); // Não valida nada -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Valida apenas o controlo name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Valida apenas o controlo age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Valida o contêiner details -``` - -`setValidationScope` não afeta o [#evento onValidate] no formulário, que será chamado sempre. O evento `onValidate` num contêiner será disparado apenas se este contêiner estiver marcado para validação parcial. diff --git a/forms/ro/@home.texy b/forms/ro/@home.texy deleted file mode 100644 index 8890cf4096..0000000000 --- a/forms/ro/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Nette Forms a revoluționat crearea formularelor web. Dintr-o dată, a fost suficient să scrieți câteva rânduri de cod clare și aveați un formular complet, inclusiv randare, validare JavaScript și pe server, și, în plus, extrem de securizat. Vom arăta cum: - -- să creați formulare prietenoase -- să validați datele trimise -- să randati elementele exact după nevoie - -</div> - - -Utilizând Nette Forms, veți evita o serie întreagă de sarcini de rutină, cum ar fi scrierea validării (în plus, dublă, pe partea de server și client), veți minimiza probabilitatea apariției erorilor și a găurilor de securitate. - -Formularele pot fi utilizate fie ca parte a Nette Application (adică în presenteri), fie complet independent. Deoarece în ambele cazuri utilizarea diferă puțin, am pregătit pentru dvs. două tutoriale: - -<div class="wiki-buttons"> -<div> "Formulare în presenteri .[wiki-button]":in-presenter </div> -<div> "Formulare independent .[wiki-button]":standalone </div> -</div> - - -Instalare ---------- - -Descărcați și instalați biblioteca folosind [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/ro/@left-menu.texy b/forms/ro/@left-menu.texy deleted file mode 100644 index a361d6d5de..0000000000 --- a/forms/ro/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Introducere |@home] -- [Formulare în presenteri|in-presenter] -- [Formulare independent|standalone] -- [Elemente de formular |controls] -- [Validare |validation] -- [Randare |rendering] -- [Configurație |configuration] - - -Lectură suplimentară -******************** -- [Tutoriale și proceduri |best-practices:] diff --git a/forms/ro/@meta.texy b/forms/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/forms/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/forms/ro/configuration.texy b/forms/ro/configuration.texy deleted file mode 100644 index bdbed9ef7a..0000000000 --- a/forms/ro/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Configurarea formularelor -************************* - -.[perex] -În configurație se pot modifica [mesajele de eroare implicite ale formularelor|validation]. - -```neon -forms: - messages: - 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.' - 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.' -``` - -Aici este traducerea în română: - -```neon -forms: - messages: - Equal: 'Introduceți %s.' - NotEqual: 'Această valoare nu ar trebui să fie %s.' - Filled: 'Acest câmp este obligatoriu.' - Blank: 'Acest câmp ar trebui să fie gol.' - MinLength: 'Introduceți cel puțin %d caractere.' - MaxLength: 'Introduceți maximum %d caractere.' - Length: 'Introduceți o valoare între %d și %d caractere.' - Email: 'Introduceți o adresă de e-mail validă.' - URL: 'Introduceți un URL valid.' - Integer: 'Introduceți un număr întreg valid.' - Float: 'Introduceți un număr valid.' - Min: 'Introduceți o valoare mai mare sau egală cu %d.' - Max: 'Introduceți o valoare mai mică sau egală cu %d.' - Range: 'Introduceți o valoare între %d și %d.' - MaxFileSize: 'Dimensiunea fișierului încărcat poate fi de maximum %d octeți.' - MaxPostSize: 'Datele încărcate depășesc limita de %d octeți.' - MimeType: 'Fișierul încărcat nu este în formatul așteptat.' - Image: 'Fișierul încărcat trebuie să fie o imagine în format JPEG, GIF, PNG, WebP sau AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Selectați o opțiune validă.' - Nette\Forms\Controls\UploadControl::Valid: 'A apărut o eroare la încărcarea fișierului.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Sesiunea dvs. a expirat. Vă rugăm să reveniți la pagina principală și să încercați din nou.' -``` - -Dacă nu utilizați întregul framework și deci nici fișierele de configurare, puteți modifica mesajele de eroare implicite direct în array-ul `Nette\Forms\Validator::$messages`. diff --git a/forms/ro/controls.texy b/forms/ro/controls.texy deleted file mode 100644 index 4305128641..0000000000 --- a/forms/ro/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Elemente de formular -******************** - -.[perex] -Prezentare generală a elementelor de formular standard. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Adaugă un câmp text pe o singură linie (clasa [TextInput |api:Nette\Forms\Controls\TextInput]). Dacă utilizatorul nu completează câmpul, returnează un șir gol `''`, sau folosind `setNullable()` se poate specifica să returneze `null`. - -```php -$form->addText('name', 'Nume:') - ->setRequired() - ->setNullable(); -``` - -Validează automat UTF-8, elimină spațiile de la început și sfârșit și elimină sfârșiturile de linie pe care un atacator le-ar putea trimite. Parametrul `$cols` este depreciat și nu este utilizat. - -Lungimea maximă poate fi limitată folosind `setMaxLength()`. Modificarea valorii introduse de utilizator este posibilă prin [addFilter() |validation#Modificarea intrării]. - -Folosind `setHtmlType()` se poate schimba caracterul vizual al câmpului text la tipuri precum `search`, `tel` sau `url` vezi [specificație|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Rețineți că schimbarea tipului este doar vizuală și nu înlocuiește funcția de validare. Pentru tipul `url` este recomandat să se adauge o [regulă de validare specifică URL |validation#Intrări de text]. - -.[note] -Pentru alte tipuri de intrări, cum ar fi `number`, `range`, `email`, `date`, `datetime-local`, `time` și `color`, utilizați metode specializate precum [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] și [#addColor], care asigură validarea pe server. Tipurile `month` și `week` nu sunt încă pe deplin suportate în toate browserele. - -Elementului i se poate seta așa-numita empty-value, care este ceva asemănător valorii implicite, dar dacă utilizatorul nu o schimbă, elementul returnează un șir gol sau `null`. - -```php -$form->addText('phone', 'Telefon:') - ->setHtmlType('tel') - ->setEmptyValue('+40'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Adaugă un câmp pentru introducerea textului multilinie (clasa [TextArea |api:Nette\Forms\Controls\TextArea]). Dacă utilizatorul nu completează câmpul, returnează un șir gol `''`, sau folosind `setNullable()` se poate specifica să returneze `null`. - -```php -$form->addTextArea('note', 'Notă:') - ->addRule($form::MaxLength, 'Nota este prea lungă', 10000); -``` - -Validează automat UTF-8 și normalizează separatorii de linie la `\n`. Spre deosebire de câmpul de intrare pe o singură linie, nu are loc nicio eliminare a spațiilor. - -Lungimea maximă poate fi limitată folosind `setMaxLength()`. Modificarea valorii introduse de utilizator este posibilă prin [addFilter() |validation#Modificarea intrării]. Se poate seta așa-numita empty-value folosind `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Adaugă un câmp pentru introducerea unui număr întreg (clasa [TextInput |api:Nette\Forms\Controls\TextInput]). Returnează fie un integer, fie `null`, dacă utilizatorul nu introduce nimic. - -```php -$form->addInteger('year', 'An:') - ->addRule($form::Range, 'Anul trebuie să fie în intervalul de la %d la %d.', [1900, 2023]); -``` - -Elementul se randează ca `<input type="number">`. Folosind metoda `setHtmlType()` se poate schimba tipul la `range` pentru afișare sub formă de glisor, sau la `text`, dacă preferați un câmp text standard fără comportamentul special al tipului `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Adaugă un câmp pentru introducerea unui număr zecimal (clasa [TextInput |api:Nette\Forms\Controls\TextInput]). Returnează fie un float, fie `null`, dacă utilizatorul nu introduce nimic. - -```php -$form->addFloat('level', 'Nivel:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Nivelul trebuie să fie în intervalul de la %d la %d.', [0, 100]); -``` - -Elementul se randează ca `<input type="number">`. Folosind metoda `setHtmlType()` se poate schimba tipul la `range` pentru afișare sub formă de glisor, sau la `text`, dacă preferați un câmp text standard fără comportamentul special al tipului `number`. - -Nette și browserul Chrome acceptă atât virgula, cât și punctul ca separator zecimal. Pentru ca această funcționalitate să fie disponibilă și în Firefox, se recomandă setarea atributului `lang` fie pentru elementul respectiv, fie pentru întreaga pagină, de exemplu `<html lang="ro">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Adaugă un câmp pentru introducerea unei adrese de e-mail (clasa [TextInput |api:Nette\Forms\Controls\TextInput]). Dacă utilizatorul nu completează câmpul, returnează un șir gol `''`, sau folosind `setNullable()` se poate specifica să returneze `null`. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Verifică dacă valoarea este o adresă de e-mail validă. Nu se verifică dacă domeniul există efectiv, se verifică doar sintaxa. Validează automat UTF-8, elimină spațiile de la început și sfârșit. - -Lungimea maximă poate fi limitată folosind `setMaxLength()`. Modificarea valorii introduse de utilizator este posibilă prin [addFilter() |validation#Modificarea intrării]. Se poate seta așa-numita empty-value folosind `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Adaugă un câmp pentru introducerea parolei (clasa [TextInput |api:Nette\Forms\Controls\TextInput]). Parametrul `$cols` este depreciat și nu este utilizat. - -```php -$form->addPassword('password', 'Parolă:') - ->setRequired() - ->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8) - ->addRule($form::Pattern, 'Trebuie să conțină o cifră', '.*[0-9].*'); -``` - -La reafișarea formularului, câmpul va fi gol. Validează automat UTF-8, elimină spațiile de la început și sfârșit și elimină sfârșiturile de linie pe care un atacator le-ar putea trimite. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Adaugă o căsuță de bifat (clasa [Checkbox |api:Nette\Forms\Controls\Checkbox]). Returnează valoarea `true` sau `false`, în funcție de dacă este bifată. - -```php -$form->addCheckbox('agree', 'Sunt de acord cu termenii') - ->setRequired('Este necesar să fiți de acord cu termenii'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Adaugă căsuțe de bifat pentru selectarea mai multor elemente (clasa [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Returnează un array al cheilor elementelor selectate. Metoda `getSelectedItems()` returnează valorile în loc de chei. - -```php -$form->addCheckboxList('colors', 'Culori:', [ - 'r' => 'roșu', - 'g' => 'verde', - 'b' => 'albastru', -]); -``` - -Array-ul de elemente oferite îl transmitem ca al treilea parametru sau prin metoda `setItems()`. - -Folosind `setDisabled(['r', 'g'])` se pot dezactiva elemente individuale. - -Elementul verifică automat că nu a avut loc o falsificare și că elementele selectate sunt într-adevăr unele dintre cele oferite și nu au fost dezactivate. Prin metoda `getRawValue()` se pot obține elementele trimise fără această verificare importantă. - -La setarea elementelor selectate implicit, verifică de asemenea că acestea sunt unele dintre cele oferite, altfel aruncă o excepție. Această verificare poate fi dezactivată folosind `checkDefaultValue(false)`. - -Dacă trimiteți formularul prin metoda `GET`, puteți alege un mod mai compact de transmitere a datelor, care economisește dimensiunea query string-ului. Se activează prin setarea atributului HTML al formularului: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Adaugă butoane radio (clasa [RadioList |api:Nette\Forms\Controls\RadioList]). Returnează cheia elementului selectat, sau `null`, dacă utilizatorul nu a selectat nimic. Metoda `getSelectedItem()` returnează valoarea în loc de cheie. - -```php -$sex = [ - 'm' => 'bărbat', - 'f' => 'femeie', -]; -$form->addRadioList('gender', 'Sex:', $sex); -``` - -Array-ul de elemente oferite îl transmitem ca al treilea parametru sau prin metoda `setItems()`. - -Folosind `setDisabled(['m', 'f'])` se pot dezactiva elemente individuale. - -Elementul verifică automat că nu a avut loc o falsificare și că elementul selectat este într-adevăr unul dintre cele oferite și nu a fost dezactivat. Prin metoda `getRawValue()` se poate obține elementul trimis fără această verificare importantă. - -La setarea elementului selectat implicit, verifică de asemenea că acesta este unul dintre cele oferite, altfel aruncă o excepție. Această verificare poate fi dezactivată folosind `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Adaugă un select box (clasa [SelectBox |api:Nette\Forms\Controls\SelectBox]). Returnează cheia elementului selectat, sau `null`, dacă utilizatorul nu a selectat nimic. Metoda `getSelectedItem()` returnează valoarea în loc de cheie. - -```php -$countries = [ - 'RO' => 'România', - 'MD' => 'Moldova', - 'GB' => 'Marea Britanie', -]; - -$form->addSelect('country', 'Țara:', $countries) - ->setDefaultValue('RO'); -``` - -Array-ul de elemente oferite îl transmitem ca al treilea parametru sau prin metoda `setItems()`. Elementele pot fi și un array bidimensional: - -```php -$countries = [ - 'Europe' => [ - 'RO' => 'România', - 'MD' => 'Moldova', - 'GB' => 'Marea Britanie', - ], - 'CA' => 'Canada', - 'US' => 'SUA', - '?' => 'altă', -]; -``` - -La select box-uri, adesea primul element are o semnificație specială, servește ca îndemn la acțiune. Pentru adăugarea unui astfel de element servește metoda `setPrompt()`. - -```php -$form->addSelect('country', 'Țara:', $countries) - ->setPrompt('Alegeți țara'); -``` - -Folosind `setDisabled(['RO', 'MD'])` se pot dezactiva elemente individuale. - -Elementul verifică automat că nu a avut loc o falsificare și că elementul selectat este într-adevăr unul dintre cele oferite și nu a fost dezactivat. Prin metoda `getRawValue()` se poate obține elementul trimis fără această verificare importantă. - -La setarea elementului selectat implicit, verifică de asemenea că acesta este unul dintre cele oferite, altfel aruncă o excepție. Această verificare poate fi dezactivată folosind `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Adaugă un select box pentru selectarea mai multor elemente (clasa [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Returnează un array al cheilor elementelor selectate. Metoda `getSelectedItems()` returnează valorile în loc de chei. - -```php -$form->addMultiSelect('countries', 'Țări:', $countries); -``` - -Array-ul de elemente oferite îl transmitem ca al treilea parametru sau prin metoda `setItems()`. Elementele pot fi și un array bidimensional. - -Folosind `setDisabled(['RO', 'MD'])` se pot dezactiva elemente individuale. - -Elementul verifică automat că nu a avut loc o falsificare și că elementele selectate sunt într-adevăr unele dintre cele oferite și nu au fost dezactivate. Prin metoda `getRawValue()` se pot obține elementele trimise fără această verificare importantă. - -La setarea elementelor selectate implicit, verifică de asemenea că acestea sunt unele dintre cele oferite, altfel aruncă o excepție. Această verificare poate fi dezactivată folosind `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Adaugă un câmp pentru încărcarea fișierului (clasa [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returnează un obiect [FileUpload |http:request#FileUpload], chiar și în cazul în care utilizatorul nu a trimis niciun fișier, ceea ce se poate verifica prin metoda `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Avatarul trebuie să fie JPEG, PNG, GIF, WebP sau AVIF.') - ->addRule($form::MaxFileSize, 'Dimensiunea maximă este 1 MB.', 1024 * 1024); -``` - -Dacă fișierul nu reușește să se încarce corect, formularul nu este trimis cu succes și se afișează o eroare. Adică, la trimiterea cu succes nu este nevoie să se verifice metoda `FileUpload::isOk()`. - -Nu aveți niciodată încredere în numele original al fișierului returnat de metoda `FileUpload::getName()`, clientul ar fi putut trimite un nume de fișier malițios cu intenția de a deteriora sau hackui aplicația dvs. - -Regulile `MimeType` și `Image` detectează tipul solicitat pe baza semnăturii fișierului și nu verifică integritatea acestuia. Dacă imaginea nu este deteriorată se poate verifica, de exemplu, prin încercarea de a o [încărca |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Adaugă un câmp pentru încărcarea mai multor fișiere simultan (clasa [UploadControl |api:Nette\Forms\Controls\UploadControl]). Returnează un array de obiecte [FileUpload |http:request#FileUpload]. Metoda `FileUpload::hasFile()` pentru fiecare dintre ele va returna `true`. - -```php -$form->addMultiUpload('files', 'Fișiere:') - ->addRule($form::MaxLength, 'Maxim se pot încărca %d fișiere', 10); -``` - -Dacă vreun fișier nu reușește să se încarce corect, formularul nu este trimis cu succes și se afișează o eroare. Adică, la trimiterea cu succes nu este nevoie să se verifice metoda `FileUpload::isOk()`. - -Nu aveți niciodată încredere în numele originale ale fișiierelor returnate de metoda `FileUpload::getName()`, clientul ar fi putut trimite un nume de fișier malițios cu intenția de a deteriora sau hackui aplicația dvs. - -Regulile `MimeType` și `Image` detectează tipul solicitat pe baza semnăturii fișierului și nu verifică integritatea acestuia. Dacă imaginea nu este deteriorată se poate verifica, de exemplu, prin încercarea de a o [încărca |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Adaugă un câmp care permite utilizatorului să introducă ușor o dată formată din an, lună și zi (clasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ca valoare implicită acceptă fie obiecte care implementează interfața `DateTimeInterface`, un șir cu timpul, fie un număr reprezentând timestamp UNIX. Același lucru este valabil și pentru argumentele regulilor `Min`, `Max` sau `Range`, care definesc data minimă și maximă permisă. - -```php -$form->addDate('date', 'Data:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Data trebuie să fie cu cel puțin o lună în urmă.', new DateTime('-1 month')); -``` - -Standard returnează un obiect `DateTimeImmutable`, prin metoda `setFormat()` puteți specifica [formatul text|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] sau timestamp: - -```php -$form->addDate('date', 'Data:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Adaugă un câmp care permite utilizatorului să introducă ușor un timp format din ore, minute și opțional secunde (clasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ca valoare implicită acceptă fie obiecte care implementează interfața `DateTimeInterface`, un șir cu timpul, fie un număr reprezentând timestamp UNIX. Din aceste intrări este utilizată doar informația de timp, data este ignorată. Același lucru este valabil și pentru argumentele regulilor `Min`, `Max` sau `Range`, care definesc timpul minim și maxim permis. Dacă valoarea minimă setată este mai mare decât cea maximă, se creează un interval de timp care depășește miezul nopții. - -```php -$form->addTime('time', 'Ora:', withSeconds: true) - ->addRule($form::Range, 'Ora trebuie să fie în intervalul de la %d la %d.', ['12:30', '13:30']); -``` - -Standard returnează un obiect `DateTimeImmutable` (cu data 1 ianuarie anul 1), prin metoda `setFormat()` puteți specifica [formatul text|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Ora:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Adaugă un câmp care permite utilizatorului să introducă ușor data și ora formate din an, lună, zi, ore, minute și opțional secunde (clasa [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Ca valoare implicită acceptă fie obiecte care implementează interfața `DateTimeInterface`, un șir cu timpul, fie un număr reprezentând timestamp UNIX. Același lucru este valabil și pentru argumentele regulilor `Min`, `Max` sau `Range`, care definesc data minimă și maximă permisă. - -```php -$form->addDateTime('datetime', 'Data și ora:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Data trebuie să fie cu cel puțin o lună în urmă.', new DateTime('-1 month')); -``` - -Standard returnează un obiect `DateTimeImmutable`, prin metoda `setFormat()` puteți specifica [formatul text|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] sau timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Adaugă un câmp pentru selectarea culorii (clasa [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Culoarea este un șir în formatul `#rrggbb`. Dacă utilizatorul nu face nicio selecție, se returnează culoarea neagră `#000000`. - -```php -$form->addColor('color', 'Culoare:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Adaugă un câmp ascuns (clasa [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -Folosind `setNullable()` se poate seta să returneze `null` în loc de șir gol. Modificarea valorii trimise este posibilă prin [addFilter() |validation#Modificarea intrării]. - -Deși elementul este ascuns, este **important să rețineți** că valoarea poate fi totuși modificată sau falsificată de un atacator. Verificați și validați întotdeauna cu atenție toate valorile primite pe partea serverului pentru a preveni riscurile de securitate asociate cu manipularea datelor. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Adaugă un buton de trimitere (clasa [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Trimite'); -``` - -În formular este posibil să aveți și mai multe butoane de trimitere: - -```php -$form->addSubmit('register', 'Înregistrează-te'); -$form->addSubmit('cancel', 'Anulează'); -``` - -Pentru a afla pe care dintre ele s-a făcut clic, utilizați: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Dacă nu doriți să validați întregul formular la apăsarea butonului (de exemplu, la butoanele *Anulează* sau *Previzualizare*), utilizați [setValidationScope() |validation#Dezactivarea validării]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Adaugă un buton (clasa [Button |api:Nette\Forms\Controls\Button]), care nu are funcție de trimitere. Poate fi deci utilizat pentru o altă funcție, de ex. apelarea unei funcții JavaScript la clic. - -```php -$form->addButton('raise', 'Mărește salariul') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Adaugă un buton de trimitere sub formă de imagine (clasa [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image'); -``` - -La utilizarea mai multor butoane de trimitere se poate afla pe care s-a făcut clic folosind `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Adaugă un subformular (clasa [Container|api:Nette\Forms\Container]), adică un container, în care se pot adăuga alte elemente în același mod în care le adăugăm în formular. Funcționează și metodele `setDefaults()` sau `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Numele dvs.:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Numele dvs.:'); -$sub2->addEmail('email', 'Email:'); -``` - -Datele trimise le returnează apoi ca o structură multidimensională: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Prezentare generală a setărilor -=============================== - -La toate elementele putem apela următoarele metode (prezentare completă în [documentația API|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | setează valoarea implicită -| `getValue()` | obține valoarea curentă -| `setOmitted()` | [#Omiterea valorii] -| `setDisabled()` | [#Dezactivarea elementelor] - -Randare: -.[table-form-methods language-php] -| `setCaption($caption)` | schimbă eticheta elementului -| `setTranslator($translator)` | setează [traducătorul |rendering#Traducere] -| `setHtmlAttribute($name, $value)` | setează [atributul HTML |rendering#Atribute HTML] al elementului -| `setHtmlId($id)` | setează atributul HTML `id` -| `setHtmlType($type)` | setează atributul HTML `type` -| `setHtmlName($name)` | setează atributul HTML `name` -| `setOption($key, $value)` | [opțiuni de randare |rendering#Opțiuni] - -Validare: -.[table-form-methods language-php] -| `setRequired()` | [element obligatoriu |validation] -| `addRule()` | setarea [regulii de validare |validation#Reguli] -| `addCondition()`, `addConditionOn()` | setează [condiția de validare |validation#Condiții] -| `addError($message)` | [transmiterea mesajului de eroare |validation#Erori în timpul procesării] - -La elementele `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` se pot apela următoarele metode: - -.[table-form-methods language-php] -| `setNullable()` | setează dacă getValue() va returna `null` în loc de șir gol -| `setEmptyValue($value)` | setează o valoare specială care este considerată șir gol -| `setMaxLength($length)` | setează numărul maxim de caractere permise -| `addFilter($filter)` | [modificarea intrării |validation#Modificarea intrării] - - -Omiterea valorii -================ - -Dacă valoarea completată de utilizator nu ne interesează, o putem omite din rezultatul metodei `$form->getValues()` sau din datele transmise handlerilor folosind `setOmitted()`. Acest lucru este util pentru diverse parole de verificare, elemente antispam etc. - -```php -$form->addPassword('passwordVerify', 'Parola pentru verificare:') - ->setRequired('Vă rugăm introduceți parola încă o dată pentru verificare') - ->addRule($form::Equal, 'Parolele nu se potrivesc', $form['password']) - ->setOmitted(); -``` - - -Dezactivarea elementelor -======================== - -Elementele pot fi dezactivate folosind `setDisabled()`. Un astfel de element nu poate fi editat de utilizator. - -```php -$form->addText('username', 'Nume utilizator:') - ->setDisabled(); -``` - -Elementele dezactivate nu sunt trimise deloc de browser către server, deci nu le veți găsi nici în datele returnate de funcția `$form->getValues()`. Dacă însă setați `setOmitted(false)`, Nette va include în aceste date valoarea lor implicită. - -La apelarea `setDisabled()`, din motive de securitate **se șterge valoarea elementului**. Dacă setați o valoare implicită, este necesar să o faceți după dezactivarea acestuia: - -```php -$form->addText('username', 'Nume utilizator:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -O alternativă la elementele dezactivate sunt elementele cu atributul HTML `readonly`, pe care browserul le trimite la server. Deși elementul este doar pentru citire, este **important să rețineți** că valoarea sa poate fi totuși modificată sau falsificată de un atacator. - - -Elemente personalizate -====================== - -Pe lângă gama largă de elemente de formular încorporate, puteți adăuga elemente personalizate în formular în acest mod: - -```php -$form->addComponent(new DateInput('Data:'), 'date'); -// sintaxă alternativă: $form['date'] = new DateInput('Data:'); -``` - -.[note] -Formularul este un descendent al clasei [Container |component-model:#Container], iar elementele individuale sunt descendenți ai [Component |component-model:#Component]. - -Există o modalitate de a defini noi metode ale formularului care servesc la adăugarea elementelor personalizate (de ex. `$form->addZip()`). Este vorba de așa-numitele extension methods. Dezavantajul este că sugestiile din editori nu vor funcționa pentru ele. - -```php -use Nette\Forms\Container; - -// adăugăm metoda 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, 'Cel puțin 5 cifre', '[0-9]{5}'); -}); - -// utilizare -$form->addZip('zip', 'Cod poștal:'); -``` - - -Elemente de nivel scăzut -======================== - -Se pot utiliza și elemente pe care le scriem doar în șablon și nu le adăugăm în formular prin una dintre metodele `$form->addXyz()`. De exemplu, când afișăm înregistrări din baza de date și nu știm dinainte câte vor fi și ce ID-uri vor avea, și dorim să afișăm la fiecare rând un checkbox sau un radio button, este suficient să îl codăm în șablon: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -Și după trimitere aflăm valoarea: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -unde primul parametru este tipul elementului (`DataFile` pentru `type=file`, `DataLine` pentru intrări pe o singură linie precum `text`, `password`, `email` etc. și `DataText` pentru toate celelalte) iar al doilea parametru `sel[]` corespunde atributului HTML name. Tipul elementului îl putem combina cu valoarea `DataKeys`, care păstrează cheile elementelor. Acest lucru este util în special pentru `select`, `radioList` și `checkboxList`. - -Esențial este că `getHttpData()` returnează o valoare sanitarizată, în acest caz va fi întotdeauna un array de șiruri UTF-8 valide, indiferent ce ar încerca un atacator să strecoare serverului. Este o analogie a lucrului direct cu `$_POST` sau `$_GET`, dar cu diferența esențială că returnează întotdeauna date curate, așa cum sunteți obișnuiți la elementele standard ale formularelor Nette. diff --git a/forms/ro/in-presenter.texy b/forms/ro/in-presenter.texy deleted file mode 100644 index 98aed25f44..0000000000 --- a/forms/ro/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Formulare în presentere -*********************** - -.[perex] -Nette Forms facilitează enorm crearea și procesarea formularelor web. În acest capitol, veți învăța cum să utilizați formularele în interiorul presenterelor. - -Dacă sunteți interesat de cum să le utilizați complet independent, fără restul framework-ului, ghidul pentru [utilizare independentă|standalone] este pentru dumneavoastră. - - -Primul formular -=============== - -Să încercăm să scriem un formular simplu de înregistrare. Codul său va fi următorul: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Nume:'); -$form->addPassword('password', 'Parolă:'); -$form->addSubmit('send', 'Înregistrează-te'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -și în browser se va afișa astfel: - -[* form-cs.webp *] - -Formularul în presenter este un obiect al clasei `Nette\Application\UI\Form`, predecesorul său `Nette\Forms\Form` este destinat utilizării independente. Am adăugat în el elementele numite nume, parolă și un buton de trimitere. Și în final, linia cu `$form->onSuccess` spune că după trimitere și validare reușită, trebuie apelată metoda `$this->formSucceeded()`. - -Din perspectiva presenterului, formularul este o componentă obișnuită. Prin urmare, este tratat ca o componentă și îl vom integra în presenter folosind [metode factory |application:components#Metode factory]. Va arăta astfel: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Nume:'); - $form->addPassword('password', 'Parolă:'); - $form->addSubmit('send', 'Înregistrează-te'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // aici procesăm datele trimise de formular - // $data->name conține numele - // $data->password conține parola - $this->flashMessage('Ați fost înregistrat cu succes.'); - $this->redirect('Home:'); - } -} -``` - -Și în șablon, randăm formularul cu tag-ul `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Înregistrare</h1> - -{control registrationForm} -``` - -Și asta e tot :-) Avem un formular funcțional și perfect [securizat |#Protecția împotriva vulnerabilităților]. - -Și acum probabil vă gândiți că a fost prea rapid, vă întrebați cum este posibil să fie apelată metoda `formSucceeded()` și care sunt parametrii pe care îi primește. Sigur, aveți dreptate, acest lucru merită o explicație. - -Nette vine cu un mecanism proaspăt, pe care îl numim [Stil Hollywood |application:components#Stilul Hollywood]. În loc ca dumneavoastră, ca dezvoltator, să trebuiască să întrebați constant dacă s-a întâmplat ceva („a fost trimis formularul?”, „a fost trimis valid?” și „nu a fost falsificat?”), spuneți framework-ului „când formularul este completat valid, apelează această metodă” și lăsați restul muncii pe seama lui. Dacă programați în JavaScript, acest stil de programare vă este familiar. Scrieți funcții care sunt apelate atunci când are loc un anumit [eveniment |nette:glossary#Evenimente]. Și limbajul le transmite argumentele corespunzătoare. - -Exact așa este construit și codul presenterului de mai sus. Array-ul `$form->onSuccess` reprezintă o listă de callback-uri PHP, pe care Nette le apelează în momentul în care formularul este trimis și completat corect (adică este valid). În cadrul [ciclului de viață al presenterului |application:presenters#Ciclul de viață al presenterului], este vorba despre un așa-numit semnal, deci sunt apelate după metoda `action*` și înainte de metoda `render*`. Și fiecărui callback îi transmite ca prim parametru formularul însuși și ca al doilea, datele trimise sub forma unui obiect [ArrayHash |utils:arrays#ArrayHash]. Primul parametru poate fi omis dacă nu aveți nevoie de obiectul formularului. Iar al doilea parametru poate fi mai inteligent, dar despre asta [mai târziu |#Maparea la clase]. - -Obiectul `$data` conține proprietățile `name` și `password` cu datele completate de utilizator. De obicei, trimitem datele direct pentru procesare ulterioară, ceea ce poate fi, de exemplu, inserarea în baza de date. În timpul procesării, însă, poate apărea o eroare, de exemplu, numele de utilizator este deja ocupat. În acest caz, transmitem eroarea înapoi în formular folosind `addError()` și îl lăsăm să fie randat din nou, inclusiv cu mesajul de eroare. - -```php -$form->addError('Ne pare rău, numele de utilizator este deja folosit de altcineva.'); -``` - -Pe lângă `onSuccess`, mai există și `onSubmit`: callback-urile sunt apelate întotdeauna după trimiterea formularului, chiar și atunci când nu este completat corect. Și, de asemenea, `onError`: callback-urile sunt apelate doar dacă trimiterea nu este validă. Sunt apelate chiar și atunci când în `onSuccess` sau `onSubmit` invalidăm formularul folosind `addError()`. - -După procesarea formularului, redirecționăm către pagina următoare. Acest lucru previne retrimiterea nedorită a formularului prin butonul *reîmprospătare*, *înapoi* sau prin navigarea în istoricul browserului. - -Încercați să adăugați și alte [elemente de formular|controls]. - - -Accesul la elemente -=================== - -Formularul este o componentă a presenterului, în cazul nostru numită `registrationForm` (după numele metodei factory `createComponentRegistrationForm`), așa că oriunde în presenter puteți accesa formularul folosind: - -```php -$form = $this->getComponent('registrationForm'); -// sintaxă alternativă: $form = $this['registrationForm']; -``` - -Componentele sunt și elementele individuale ale formularului, de aceea le puteți accesa în același mod: - -```php -$input = $form->getComponent('name'); // sau $input = $form['name']; -$button = $form->getComponent('send'); // sau $button = $form['send']; -``` - -Elementele se elimină folosind `unset`: - -```php -unset($form['name']); -``` - - -Reguli de validare -================== - -S-a menționat cuvântul *valid*, dar formularul nu are încă nicio regulă de validare. Să remediem acest lucru. - -Numele va fi obligatoriu, de aceea îl marcăm cu metoda `setRequired()`, al cărei argument este textul mesajului de eroare care se afișează dacă utilizatorul nu completează numele. Dacă nu specificăm argumentul, se va folosi mesajul de eroare implicit. - -```php -$form->addText('name', 'Nume:') - ->setRequired('Vă rugăm să introduceți numele'); -``` - -Încercați să trimiteți formularul fără a completa numele și veți vedea că se afișează un mesaj de eroare, iar browserul sau serverul îl va respinge până când completați câmpul. - -În același timp, nu puteți păcăli sistemul scriind, de exemplu, doar spații în câmp. Nici vorbă. Nette elimină automat spațiile de la începutul și sfârșitul șirului. Încercați. Este un lucru pe care ar trebui să-l faceți întotdeauna cu fiecare input de o singură linie, dar adesea se uită. Nette o face automat. (Puteți încerca să păcăliți formularul și să trimiteți un șir multilinie ca nume. Nici aici Nette nu se lasă păcălit și transformă sfârșiturile de rând în spații.) - -Formularul este întotdeauna validat pe partea de server, dar se generează și o validare JavaScript, care se execută instantaneu, iar utilizatorul află despre eroare imediat, fără a fi nevoie să trimită formularul la server. Acest lucru este gestionat de scriptul `netteForms.js`. Inserați-l în șablonul de layout: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Dacă vă uitați la codul sursă al paginii cu formularul, puteți observa că Nette inserează elementele obligatorii în elemente cu clasa CSS `required`. Încercați să adăugați următorul stil în șablon și eticheta „Nume” va fi roșie. Astfel, marcăm elegant elementele obligatorii pentru utilizatori: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Alte reguli de validare le adăugăm cu metoda `addRule()`. Primul parametru este regula, al doilea este din nou textul mesajului de eroare și poate urma un argument al regulii de validare. Ce înseamnă asta? - -Vom extinde formularul cu un nou câmp opțional „vârstă”, care trebuie să fie un număr întreg (`addInteger()`) și, în plus, într-un interval permis (`$form::Range`). Și aici vom folosi al treilea parametru al metodei `addRule()`, prin care transmitem validatorului intervalul dorit ca o pereche `[de la, până la]`: - -```php -$form->addInteger('age', 'Vârsta:') - ->addRule($form::Range, 'Vârsta trebuie să fie între 18 și 120', [18, 120]); -``` - -.[tip] -Dacă utilizatorul nu completează câmpul, regulile de validare nu vor fi verificate, deoarece elementul este opțional. - -Aici apare spațiu pentru o mică refactorizare. În mesajul de eroare și în al treilea parametru, numerele sunt menționate duplicat, ceea ce nu este ideal. Dacă am crea [formulare multilingve |rendering#Traducere] și mesajul care conține numere ar fi tradus în mai multe limbi, o eventuală modificare a valorilor ar fi dificilă. Din acest motiv, este posibil să folosim substituenți `%d` și Nette va completa valorile: - -```php - ->addRule($form::Range, 'Vârsta trebuie să fie între %d și %d ani', [18, 120]); -``` - -Să ne întoarcem la elementul `password`, pe care îl vom face, de asemenea, obligatoriu și vom verifica lungimea minimă a parolei (`$form::MinLength`), din nou folosind substituentul: - -```php -$form->addPassword('password', 'Parolă:') - ->setRequired('Alegeți o parolă') - ->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8); -``` - -Adăugăm în formular și câmpul `passwordVerify`, unde utilizatorul introduce parola încă o dată, pentru verificare. Folosind regulile de validare, verificăm dacă ambele parole sunt identice (`$form::Equal`). Și ca parametru, dăm o referință la prima parolă folosind [paranteze drepte |#Accesul la elemente]: - -```php -$form->addPassword('passwordVerify', 'Parola pentru verificare:') - ->setRequired('Vă rugăm introduceți parola încă o dată pentru verificare') - ->addRule($form::Equal, 'Parolele nu se potrivesc', $form['password']) - ->setOmitted(); -``` - -Cu `setOmitted()`, am marcat elementul a cărui valoare nu ne interesează de fapt și care există doar în scopul validării. Valoarea nu se transmite în `$data`. - -Astfel, avem un formular complet funcțional cu validare în PHP și JavaScript. Capacitățile de validare ale Nette sunt mult mai largi, se pot crea condiții, se pot afișa și ascunde părți ale paginii în funcție de acestea etc. Veți afla totul în capitolul despre [validarea formularelor|validation]. - - -Valori implicite -================ - -Elementelor formularului le setăm în mod obișnuit valori implicite: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Adesea este util să setăm valori implicite pentru toate elementele simultan. De exemplu, când formularul servește pentru editarea înregistrărilor. Citim înregistrarea din baza de date și setăm valorile implicite: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Apelați `setDefaults()` după definirea elementelor. - - -Randarea formularului -===================== - -În mod standard, formularul se randează ca un tabel. Elementele individuale respectă regula de bază a accesibilității - toate etichetele sunt scrise ca `<label>` și legate de elementul de formular corespunzător. La clic pe etichetă, cursorul apare automat în câmpul formularului. - -Fiecărui element îi putem seta atribute HTML arbitrare. De exemplu, adăugăm un placeholder: - -```php -$form->addInteger('age', 'Vârsta:') - ->setHtmlAttribute('placeholder', 'Vă rugăm să completați vârsta'); -``` - -Există într-adevăr o mare varietate de moduri de a randa un formular, așa că există un [capitol separat despre randare|rendering] dedicat acestui subiect. - - -Maparea la clase -================ - -Să ne întoarcem la metoda `formSucceeded()`, care în al doilea parametru `$data` primește datele trimise ca obiect `ArrayHash`. Deoarece este o clasă generică, ceva de genul `stdClass`, ne va lipsi un anumit confort în lucrul cu ea, cum ar fi sugerarea proprietăților în editori sau analiza statică a codului. Acest lucru ar putea fi rezolvat având o clasă specifică pentru fiecare formular, ale cărei proprietăți reprezintă elementele individuale. De ex.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativ, puteți utiliza constructorul: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Proprietățile clasei de date pot fi, de asemenea, enum-uri și vor fi mapate automat. .{data-version:3.2.4} - -Cum să spunem Nette să ne returneze datele ca obiecte ale acestei clase? Mai ușor decât credeți. Este suficient doar să specificați clasa ca tip al parametrului `$data` în metoda handler: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $data este o instanță a RegistrationFormData - $name = $data->name; - // ... -} -``` - -Ca tip se poate specifica și `array` și atunci datele vor fi transmise ca array. - -În mod similar, se poate utiliza și metoda `getValues()`, căreia îi transmitem numele clasei sau obiectul de hidratat ca parametru: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Dacă formularele formează o structură multinivel compusă din containere, creați o clasă separată pentru fiecare: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Maparea va recunoaște apoi din tipul proprietății `$person` că trebuie să mapeze containerul la clasa `PersonFormData`. Dacă proprietatea ar conține un array de containere, specificați tipul `array` și transmiteți clasa pentru mapare direct containerului: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Puteți genera designul clasei de date a formularului folosind metoda `Nette\Forms\Blueprint::dataClass($form)`, care o va afișa în pagina browserului. Apoi, este suficient să selectați codul cu un clic și să-l copiați în proiect. .{data-version:3.1.15} - - -Mai multe butoane -================= - -Dacă formularul are mai mult de un buton, de obicei trebuie să distingem care dintre ele a fost apăsat. Putem crea o funcție handler proprie pentru fiecare buton. O setăm ca handler pentru [evenimentul |nette:glossary#Evenimente] `onClick`: - -```php -$form->addSubmit('save', 'Salvează') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Șterge') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Acești handleri sunt apelați doar în cazul unui formular completat valid, la fel ca în cazul evenimentului `onSuccess`. Diferența este că, în loc de formular, ca prim parametru se poate transmite butonul de trimitere, depinde de tipul pe care îl specificați: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Când formularul este trimis cu tasta <kbd>Enter</kbd>, se consideră ca și cum ar fi fost trimis cu primul buton. - - -Evenimentul onAnchor -==================== - -Când construim formularul în metoda factory (cum ar fi `createComponentRegistrationForm`), acesta nu știe încă dacă a fost trimis, nici cu ce date. Dar există cazuri în care avem nevoie să cunoaștem valorile trimise, de exemplu, forma ulterioară a formularului depinde de ele, sau avem nevoie de ele pentru selectbox-uri dependente etc. - -O parte a codului care construiește formularul poate fi, prin urmare, lăsată să fie apelată doar în momentul în care este așa-numit ancorat, adică este deja conectat la presenter și cunoaște datele sale trimise. Un astfel de cod îl transmitem în array-ul `$onAnchor`: - -```php -$country = $form->addSelect('country', 'Țara:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Oraș:'); - -$form->onAnchor[] = function () use ($country, $city) { - // această funcție va fi apelată doar când formularul știe dacă a fost trimis și cu ce date - // deci se poate folosi metoda getValue() - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Protecția împotriva vulnerabilităților -====================================== - -Nette Framework pune un accent deosebit pe securitate și, prin urmare, acordă o atenție deosebită securizării formularelor. Face acest lucru complet transparent și nu necesită setări manuale. - -Pe lângă faptul că protejează formularele împotriva atacurilor [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] și [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], realizează o mulțime de mici măsuri de securitate la care nu mai trebuie să vă gândiți. - -De exemplu, filtrează toate caracterele de control din intrări și verifică validitatea codificării UTF-8, astfel încât datele din formular vor fi întotdeauna curate. La select box-uri și liste radio, verifică dacă elementele selectate au fost într-adevăr dintre cele oferite și nu a avut loc o falsificare. Am menționat deja că la intrările text de o singură linie elimină caracterele de sfârșit de rând, pe care un atacator le-ar fi putut trimite. La intrările multilinie, normalizează caracterele de sfârșit de rând. Și așa mai departe. - -Nette rezolvă pentru dumneavoastră riscurile de securitate despre care mulți programatori nici nu bănuiesc că există. - -Atacul CSRF menționat constă în faptul că atacatorul atrage victima pe o pagină care execută discret în browserul victimei o cerere către serverul pe care victima este autentificată, iar serverul crede că cererea a fost executată de victimă din proprie voință. De aceea, Nette împiedică trimiterea formularului POST de pe un alt domeniu. Dacă, din anumite motive, doriți să dezactivați protecția și să permiteți trimiterea formularului de pe un alt domeniu, utilizați: - -```php -$form->allowCrossOrigin(); // ATENȚIE! Dezactivează protecția! -``` - -Această protecție utilizează un cookie SameSite numit `_nss`. Protecția prin cookie SameSite poate să nu fie 100% fiabilă, de aceea este recomandat să activați și protecția prin token: - -```php -$form->addProtection(); -``` - -Recomandăm protejarea în acest mod a formularelor din partea de administrare a site-ului, care modifică date sensibile în aplicație. Framework-ul se apără împotriva atacului CSRF prin generarea și verificarea unui token de autorizare, care se stochează în sesiune (session). De aceea, este necesar ca sesiunea să fie deschisă înainte de afișarea formularului. În partea de administrare a site-ului, de obicei, sesiunea este deja pornită datorită autentificării utilizatorului. Altfel, porniți sesiunea cu metoda `Nette\Http\Session::start()`. - - -Același formular în mai multe presentere -======================================== - -Dacă aveți nevoie să utilizați același formular în mai multe presentere, vă recomandăm să creați o fabrică pentru acesta, pe care apoi să o transmiteți presenterului. O locație potrivită pentru o astfel de clasă este, de exemplu, directorul `app/Forms`. - -Clasa fabrică poate arăta, de exemplu, astfel: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Nume:'); - $form->addSubmit('send', 'Conectează-te'); - return $form; - } -} -``` - -Solicităm clasei să producă formularul în metoda factory pentru componente din presenter: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // putem modifica formularul, aici de exemplu schimbăm eticheta butonului - $form['send']->setCaption('Continuă'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // și adăugăm handler - return $form; -} -``` - -Handlerul pentru procesarea formularului poate fi, de asemenea, furnizat deja din fabrică: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Nume:'); - $form->addSubmit('send', 'Conectează-te'); - $form->onSuccess[] = function (Form $form, $data): void { - // aici efectuăm procesarea formularului - }; - return $form; - } -} -``` - -Așadar, am parcurs o introducere rapidă în formularele din Nette. Încercați să vă uitați și în directorul [examples|https://github.com/nette/forms/tree/master/examples] din distribuție, unde veți găsi mai multă inspirație. diff --git a/forms/ro/rendering.texy b/forms/ro/rendering.texy deleted file mode 100644 index 21ed2448b7..0000000000 --- a/forms/ro/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Randarea formularelor -********************* - -Aspectul formularelor poate fi foarte divers. În practică, putem întâlni două extreme. Pe de o parte, există nevoia de a randa în aplicație o serie de formulare care sunt vizual asemănătoare ca două picături de apă și apreciem randarea ușoară fără șablon folosind `$form->render()`. Acesta este de obicei cazul interfețelor de administrare. - -Pe de altă parte, există formulare diverse, unde regula este: fiecare piesă este originală. Forma lor este cel mai bine descrisă folosind limbajul HTML în șablonul formularului. Și, desigur, pe lângă cele două extreme menționate, vom întâlni multe formulare care se situează undeva între. - - -Randarea cu Latte -================= - -[Sistemul de șabloane Latte|latte:] facilitează fundamental randarea formularelor și a elementelor acestora. Mai întâi, vom arăta cum să randăm formularele manual, element cu element, obținând astfel control deplin asupra codului. Mai târziu, vom arăta cum se poate [automatiza |#Randare automată] o astfel de randare. - -Puteți genera designul șablonului Latte al formularului folosind metoda `Nette\Forms\Blueprint::latte($form)`, care îl va afișa în pagina browserului. Apoi, este suficient să selectați codul cu un clic și să-l copiați în proiect. .{data-version:3.1.15} - - -`{control}` ------------ - -Cel mai simplu mod de a randa un formular este să scrieți în șablon: - -```latte -{control signInForm} -``` - -Puteți influența aspectul formularului randat astfel prin configurarea [Rendererului |#Renderer] și a [elementelor individuale |#Atribute HTML]. - - -`n:name` --------- - -Definirea formularului în codul PHP poate fi extrem de ușor legată de codul HTML. Este suficient doar să adăugați atributele `n:name`. Atât de simplu este! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Aveți control deplin asupra aspectului codului HTML rezultat. Dacă utilizați atributul `n:name` la elementele `<select>`, `<button>` sau `<textarea>`, conținutul lor intern se va completa automat. Tag-ul `<form n:name>` creează, în plus, o variabilă locală `$form` cu obiectul formularului randat, iar tag-ul de închidere `</form>` randează toate elementele hidden nerandate (același lucru este valabil și pentru `{form} ... {/form}`). - -Nu trebuie însă să uităm de randarea posibilelor mesaje de eroare. Atât cele care au fost adăugate la elementele individuale prin metoda `addError()` (folosind `{inputError}`), cât și cele adăugate direct la formular (returnate de `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Elementele de formular mai complexe, cum ar fi RadioList sau CheckboxList, pot fi randate astfel, element cu element: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Nu doriți să vă gândiți la fiecare element ce element HTML să folosiți pentru el în șablon, dacă `<input>`, `<textarea>` etc.? Soluția este tag-ul universal `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Password: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Dacă formularul utilizează un translator, textul din interiorul tag-urilor `{label}` va fi tradus. - -Chiar și în acest caz, elementele de formular mai complexe, cum ar fi RadioList sau CheckboxList, pot fi randate element cu element: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Pentru a randa doar `<input>` în elementul Checkbox, utilizați `{input myCheckbox:}`. Atributele HTML în acest caz separați-le întotdeauna cu virgulă `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Afișează mesajul de eroare pentru elementul formularului, dacă are unul. Mesajul este de obicei încapsulat într-un element HTML pentru stilizare. Puteți preveni randarea elementului gol, dacă nu există mesaj, elegant folosind `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -Putem verifica prezența unei erori cu metoda `hasErrors()` și, în funcție de aceasta, seta clasa elementului părinte: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -Tag-urile `{form signInForm}...{/form}` sunt o alternativă la `<form n:name="signInForm">...</form>`. - - -Randare automată ----------------- - -Datorită tag-urilor `{input}` și `{label}`, putem crea cu ușurință un șablon generic pentru orice formular. Acesta va itera și va randa succesiv toate elementele sale, cu excepția elementelor hidden, care se randează automat la închiderea formularului cu tag-ul `</form>`. Numele formularului randat va fi așteptat în variabila `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -Tag-urile pereche auto-închise `{label .../}` utilizate afișează etichetele provenite din definirea formularului în codul PHP. - -Salvați acest șablon generic, de exemplu, în fișierul `basic-form.latte` și pentru a randa formularul, este suficient să-l includeți și să transmiteți numele (sau instanța) formularului în parametrul `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Dacă doriți să interveniți în aspectul unui anumit formular în timpul randării și, de exemplu, să randați un element diferit, cea mai simplă cale este să pregătiți blocuri în șablon, care vor putea fi ulterior suprascrise. Blocurile pot avea și [nume dinamice |latte:template-inheritance#Denumiri dinamice de blocuri], astfel încât se poate insera în ele și numele elementului randat. De exemplu: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Pentru elementul, de exemplu, `username`, se va crea astfel blocul `input-username`, care poate fi ușor suprascris folosind tag-ul [{embed} |latte:template-inheritance#Moștenirea unitară embed]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Alternativ, întregul conținut al șablonului `basic-form.latte` poate fi [definit |latte:template-inheritance#Definiții define] ca un bloc, inclusiv parametrul `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Datorită acestui fapt, apelarea sa va fi puțin mai simplă: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -Blocul trebuie importat într-un singur loc, și anume la începutul șablonului de layout: - -```latte -{import basic-form.latte} -``` - - -Cazuri speciale ---------------- - -Dacă aveți nevoie să randați doar partea interioară a formularului fără tag-urile HTML `<form>`, de exemplu la trimiterea snippet-urilor, ascundeți-le folosind atributul `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Username: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -Tag-ul `{formContainer}` ajută la randarea elementelor din interiorul containerului formularului. - -```latte -<p>Ce știri doriți să primiți:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Randare fără Latte -================== - -Cel mai simplu mod de a randa un formular este să apelați: - -```php -$form->render(); -``` - -Puteți influența aspectul formularului randat astfel prin configurarea [Rendererului |#Renderer] și a [elementelor individuale |#Atribute HTML]. - - -Randare manuală ---------------- - -Fiecare element de formular dispune de metode care generează codul HTML al câmpului formularului și al etichetei. Îl pot returna fie ca șir, fie ca obiect [Nette\Utils\Html|utils:html-elements]: - -- `getControl(): Html|string` returnează codul HTML al elementului -- `getLabel($caption = null): Html|string|null` returnează codul HTML al etichetei, dacă există - -Formularul poate fi astfel randat element cu element: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -În timp ce la unele elemente `getControl()` returnează un singur element HTML (de ex. `<input>`, `<select>` etc.), la altele returnează o întreagă bucată de cod HTML (CheckboxList, RadioList). Într-un astfel de caz, puteți utiliza metode care generează input-uri și etichete individuale, pentru fiecare element în parte: - -- `getControlPart($key = null): ?Html` returnează codul HTML al unui element -- `getLabelPart($key = null): ?Html` returnează codul HTML al etichetei unui element - -.[note] -Aceste metode au din motive istorice prefixul `get`, dar mai bun ar fi `generate`, deoarece la fiecare apelare creează și returnează un nou element `Html`. - - -Renderer -======== - -Este un obiect care asigură randarea formularului. Acesta poate fi setat cu metoda `$form->setRenderer`. I se transmite controlul la apelarea metodei `$form->render()`. - -Dacă nu setăm propriul renderer, va fi utilizat rendererul implicit [api:Nette\Forms\Rendering\DefaultFormRenderer]. Acesta randează elementele formularului sub formă de tabel HTML. Ieșirea arată astfel: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Nume:</label></th> - - <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ârstă:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Gen:</label></th> - ... -``` - -Dacă să folosim sau nu un tabel pentru structura formularului este discutabil, iar mulți webdesigneri preferă alt markup. De exemplu, o listă de definiții. Vom reconfigura, prin urmare, `DefaultFormRenderer` astfel încât să randeze formularul sub formă de listă. Configurarea se realizează prin editarea array-ului [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Primul index reprezintă întotdeauna zona, iar al doilea atributul său. Zonele individuale sunt ilustrate în imagine: - -[* defaultformrenderer.webp *] - -În mod standard, grupul de elemente `controls` este încapsulat într-un tabel `<table>`, fiecare `pair` reprezintă un rând de tabel `<tr>`, iar perechea `label` și `control` sunt celule `<th>` și `<td>`. Acum vom schimba elementele încapsulatoare. Vom insera zona `controls` într-un container `<dl>`, vom lăsa zona `pair` fără container, vom insera `label` în `<dt>` și, în final, vom încapsula `control` cu tag-urile `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Rezultatul este acest cod HTML: - -```latte -<dl> - <dt><label class="required" for="frm-name">Nume:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Vârstă:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Gen:</label></dt> - ... -</dl> -``` - -În array-ul wrappers se pot influența multe alte atribute: - -- adăugarea claselor CSS la tipurile individuale de elemente de formular -- distingerea rândurilor pare și impare prin clasa CSS -- diferențierea vizuală a elementelor obligatorii și opționale -- determinarea dacă mesajele de eroare se afișează direct lângă elemente sau deasupra formularului - - -Opțiuni -------- - -Comportamentul Rendererului poate fi controlat și prin setarea *opțiunilor* pe elementele individuale ale formularului. Astfel, se poate seta o descriere care se va afișa lângă câmpul de intrare: - -```php -$form->addText('phone', 'Număr:') - ->setOption('description', 'Acest număr va rămâne ascuns'); -``` - -Dacă dorim să plasăm conținut HTML în el, utilizăm clasa [Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Număr:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Condițiile de păstrare a numărului dumneavoastră</a>') - ); -``` - -.[tip] -Elementul Html poate fi utilizat și în locul etichetei: `$form->addCheckbox('conditions', $label)`. - - -Gruparea elementelor --------------------- - -Rendererul permite gruparea elementelor în grupuri vizuale (fieldset-uri): - -```php -$form->addGroup('Date personale'); -``` - -După crearea unui nou grup, acesta devine activ și fiecare element nou adăugat este, de asemenea, adăugat în el. Deci, formularul poate fi construit în acest mod: - -```php -$form = new Form; -$form->addGroup('Date personale'); -$form->addText('name', 'Numele dumneavoastră:'); -$form->addInteger('age', 'Vârsta dumneavoastră:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Adresa de livrare'); -$form->addCheckbox('send', 'Livrează la adresă'); -$form->addText('street', 'Stradă:'); -$form->addText('city', 'Oraș:'); -$form->addSelect('country', 'Țara:', $countries); -``` - -Rendererul randează mai întâi grupurile și abia apoi elementele care nu aparțin niciunui grup. - - -Suport pentru Bootstrap ------------------------ - -[În exemple |https://github.com/nette/forms/tree/master/examples] veți găsi exemple despre cum să configurați Rendererul pentru [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] și [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -Atribute HTML -============= - -Pentru a seta atribute HTML arbitrare ale elementelor formularului, folosim metoda `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Număr:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Sortare după:', ['preț', 'nume']) - ->setHtmlAttribute('onchange', 'submit()'); // trimite la modificare - - -// Pentru a seta atributele formularului <form> în sine -$form->setHtmlAttribute('id', 'myForm'); -``` - -Specificarea tipului elementului: - -```php -$form->addText('tel', 'Telefonul dumneavoastră:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'scrieți telefonul'); -``` - -.[warning] -Setarea tipului și a altor atribute servește doar în scopuri vizuale. Verificarea corectitudinii intrărilor trebuie să aibă loc pe server, ceea ce asigurați prin alegerea [elementului de formular|controls] adecvat și specificarea [regulilor de validare|validation]. - -Elementelor individuale din listele radio sau checkbox le putem seta un atribut HTML cu valori diferite pentru fiecare dintre ele. Observați două puncte după `style:`, care asigură alegerea valorii după cheie: - -```php -$colors = ['r' => 'roșu', 'g' => 'verde', 'b' => 'albastru']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Culori:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Afișează: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">roșu</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">verde</label> -<label><input type="checkbox" name="colors[]" value="b">albastru</label> -``` - -Pentru a seta atribute logice, cum ar fi `readonly`, putem folosi notația cu semn de întrebare: - -```php -$form->addCheckboxList('colors', 'Culori:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // pentru mai multe chei utilizați un array, de ex. ['r', 'g'] -``` - -Afișează: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">roșu</label> -<label><input type="checkbox" name="colors[]" value="g">verde</label> -<label><input type="checkbox" name="colors[]" value="b">albastru</label> -``` - -În cazul selectbox-urilor, metoda `setHtmlAttribute()` setează atributele elementului `<select>`. Dacă dorim să setăm atributele elementelor individuale `<option>`, folosim metoda `setOptionAttribute()`. Funcționează și notațiile cu două puncte și semn de întrebare menționate mai sus: - -```php -$form->addSelect('colors', 'Culori:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Afișează: - -```latte -<select name="colors"> - <option value="r" style="background:red">roșu</option> - <option value="g" style="background:green">verde</option> - <option value="b">albastru</option> -</select> -``` - - -Prototipuri ------------ - -O modalitate alternativă de setare a atributelor HTML constă în modificarea șablonului din care se generează elementul HTML. Șablonul este un obiect `Html` și este returnat de metoda `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Număr:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -În acest mod se poate modifica și șablonul etichetei, returnat de `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -La elementele Checkbox, CheckboxList și RadioList puteți influența șablonul elementului care încapsulează întregul element. Acesta este returnat de `getContainerPrototype()`. În starea implicită, este un element „gol”, deci nu se randează nimic, dar prin setarea numelui său, va începe să se randeze: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -În cazul CheckboxList și RadioList, se poate influența și șablonul separatorului elementelor individuale, returnat de metoda `getSeparatorPrototype()`. În starea implicită, este elementul `<br>`. Dacă îl schimbați într-un element pereche, va încapsula elementele individuale în loc să le separe. Și, în plus, se poate influența șablonul elementului HTML al etichetei la elementele individuale, returnat de `getItemLabelPrototype()`. - - -Traducere -========= - -Dacă programați o aplicație multilingvă, probabil veți avea nevoie să randați formularul în diferite versiuni lingvistice. Nette Framework definește în acest scop o interfață pentru traducere [api:Nette\Localization\Translator]. În Nette nu există o implementare implicită, puteți alege în funcție de nevoile dumneavoastră din mai multe soluții gata făcute, pe care le găsiți pe [Componette |https://componette.org/search/localization]. În documentația lor veți afla cum să configurați translatorul. - -Formularele suportă afișarea textelor prin translator. Îl transmitem folosind metoda `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -De acum înainte, nu numai toate etichetele, ci și toate mesajele de eroare sau elementele select box-urilor vor fi traduse într-o altă limbă. - -La elementele individuale ale formularului este posibil să setați un alt traducător sau să dezactivați complet traducerea cu valoarea `null`: - -```php -$form->addSelect('carModel', 'Model:', $cars) - ->setTranslator(null); -``` - -La [regulile de validare|validation], translatorului i se transmit și parametri specifici, de exemplu la regula: - -```php -$form->addPassword('password', 'Parolă:') - ->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8); -``` - -se apelează translatorul cu acești parametri: - -```php -$translator->translate('Parola trebuie să aibă cel puțin %d caractere', 8); -``` - -și, prin urmare, poate alege forma corectă de plural a cuvântului `caractere` în funcție de număr. - - -Evenimentul onRender -==================== - -Chiar înainte ca formularul să fie randat, putem lăsa să fie apelat codul nostru. Acesta poate, de exemplu, să completeze elementele formularului cu clase HTML pentru afișarea corectă. Adăugăm codul în array-ul `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/ro/standalone.texy b/forms/ro/standalone.texy deleted file mode 100644 index 58562a54a5..0000000000 --- a/forms/ro/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Formulare utilizate independent -******************************* - -.[perex] -Nette Forms facilitează enorm crearea și procesarea formularelor web. Le puteți utiliza în aplicațiile dvs. complet independent de restul framework-ului, așa cum vom demonstra în acest capitol. - -Dacă însă utilizați Nette Application și presentere, ghidul pentru [utilizarea în presentere|in-presenter] este destinat dvs. - - -Primul formular -=============== - -Vom încerca să scriem un formular simplu de înregistrare. Codul său va fi următorul ("codul complet":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Nume:'); -$form->addPassword('password', 'Parolă:'); -$form->addSubmit('send', 'Înregistrare'); -``` - -Îl putem randa foarte ușor: - -```php -$form->render(); -``` - -și în browser se va afișa astfel: - -[* form-cs.webp *] - -Formularul este un obiect al clasei `Nette\Forms\Form` (clasa `Nette\Application\UI\Form` se utilizează în presentere). Am adăugat în el așa-numitele elemente nume, parolă și buton de trimitere. - -Acum vom anima formularul. Prin interogarea `$form->isSuccess()` vom afla dacă formularul a fost trimis și dacă a fost completat valid. Dacă da, vom afișa datele. După definiția formularului, vom adăuga: - -```php -if ($form->isSuccess()) { - echo 'Formularul a fost completat corect și trimis'; - $data = $form->getValues(); - // $data->name conține numele - // $data->password conține parola - var_dump($data); -} -``` - -Metoda `getValues()` returnează datele trimise sub forma unui obiect [ArrayHash |utils:arrays#ArrayHash]. Cum să schimbăm acest lucru vom arăta [mai târziu |#Maparea pe clase]. Obiectul `$data` conține cheile `name` și `password` cu datele completate de utilizator. - -De obicei, trimitem datele direct pentru procesare ulterioară, cum ar fi inserarea într-o bază de date. Însă, în timpul procesării poate apărea o eroare, de exemplu, numele de utilizator este deja ocupat. În acest caz, transmitem eroarea înapoi formularului folosind `addError()` și îl lăsăm să se randeze din nou, inclusiv cu mesajul de eroare. - -```php -$form->addError('Ne pare rău, acest nume de utilizator este deja folosit.'); -``` - -După procesarea formularului, redirecționăm către pagina următoare. Acest lucru previne retrimiterea nedorită a formularului prin butonul *reîmprospătare*, *înapoi* sau prin navigarea în istoricul browserului. - -Formularul se trimite standard prin metoda POST și către aceeași pagină. Ambele pot fi modificate: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -Și cam asta e tot :-) Avem un formular funcțional și perfect [securizat |#Protecția împotriva vulnerabilităților]. - -Încercați să adăugați și alte [elemente de formular|controls]. - - -Accesul la elemente -=================== - -Formularul și elementele sale individuale le numim componente. Ele formează un arbore de componente, unde rădăcina este chiar formularul. Putem accesa elementele individuale ale formularului în acest mod: - -```php -$input = $form->getComponent('name'); -// sintaxă alternativă: $input = $form['name']; - -$button = $form->getComponent('send'); -// sintaxă alternativă: $button = $form['send']; -``` - -Elementele se elimină folosind unset: - -```php -unset($form['name']); -``` - - -Reguli de validare -================== - -Am menționat cuvântul *valid*, dar formularul nu are încă nicio regulă de validare. Să remediem acest lucru. - -Numele va fi obligatoriu, așa că îl vom marca cu metoda `setRequired()`, al cărei argument este textul mesajului de eroare care se va afișa dacă utilizatorul nu completează numele. Dacă nu specificăm argumentul, se va utiliza mesajul de eroare implicit. - -```php -$form->addText('name', 'Nume:') - ->setRequired('Vă rugăm să introduceți numele'); -``` - -Încercați să trimiteți formularul fără a completa numele și veți vedea că se afișează un mesaj de eroare, iar browserul sau serverul îl va respinge până când nu completați câmpul. - -În același timp, nu puteți păcăli sistemul scriind, de exemplu, doar spații în câmp. Nici vorbă. Nette elimină automat spațiile de la începutul și sfârșitul șirului. Încercați. Este un lucru pe care ar trebui să-l faceți întotdeauna cu fiecare input de o singură linie, dar adesea se uită. Nette o face automat. (Puteți încerca să păcăliți formularul și să trimiteți un șir multilinie ca nume. Nici aici Nette nu se lasă păcălit și transformă sfârșiturile de linie în spații.) - -Formularul se validează întotdeauna pe partea de server, dar se generează și o validare JavaScript, care se execută instantaneu, iar utilizatorul află despre eroare imediat, fără a fi nevoie să trimită formularul la server. Acest lucru este gestionat de scriptul `netteForms.js`. Inserați-l în pagină: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Dacă vă uitați la codul sursă al paginii cu formularul, puteți observa că Nette inserează elementele obligatorii în elemente cu clasa CSS `required`. Încercați să adăugați următorul stil în șablon și eticheta „Nume” va fi roșie. Astfel, marcăm elegant elementele obligatorii pentru utilizatori: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Alte reguli de validare le adăugăm cu metoda `addRule()`. Primul parametru este regula, al doilea este din nou textul mesajului de eroare și poate urma un argument al regulii de validare. Ce înseamnă asta? - -Vom extinde formularul cu un nou câmp opțional „vârstă”, care trebuie să fie un număr întreg (`addInteger()`) și, în plus, într-un interval permis (`$form::Range`). Și aici vom folosi al treilea parametru al metodei `addRule()`, prin care transmitem validatorului intervalul dorit ca pereche `[de la, până la]`: - -```php -$form->addInteger('age', 'Vârstă:') - ->addRule($form::Range, 'Vârsta trebuie să fie între 18 și 120', [18, 120]); -``` - -.[tip] -Dacă utilizatorul nu completează câmpul, regulile de validare nu vor fi verificate, deoarece elementul este opțional. - -Aici apare spațiu pentru o mică refactorizare. În mesajul de eroare și în al treilea parametru, numerele sunt menționate duplicat, ceea ce nu este ideal. Dacă am crea [formulare multilingve |rendering#Traducere] și mesajul care conține numere ar fi tradus în mai multe limbi, o eventuală modificare a valorilor ar fi dificilă. Din acest motiv, este posibil să folosim substituenții `%d`, iar Nette va completa valorile: - -```php - ->addRule($form::Range, 'Vârsta trebuie să fie între %d și %d ani', [18, 120]); -``` - -Să revenim la elementul `password`, pe care îl vom face, de asemenea, obligatoriu și vom verifica lungimea minimă a parolei (`$form::MinLength`), folosind din nou substituentul: - -```php -$form->addPassword('password', 'Parolă:') - ->setRequired('Alegeți o parolă') - ->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8); -``` - -Vom adăuga în formular și câmpul `passwordVerify`, unde utilizatorul introduce parola încă o dată, pentru verificare. Folosind regulile de validare, vom verifica dacă ambele parole sunt identice (`$form::Equal`). Și ca parametru vom da o referință la prima parolă folosind [paranteze drepte |#Accesul la elemente]: - -```php -$form->addPassword('passwordVerify', 'Parola pentru verificare:') - ->setRequired('Vă rugăm să introduceți parola din nou pentru verificare') - ->addRule($form::Equal, 'Parolele nu se potrivesc', $form['password']) - ->setOmitted(); -``` - -Folosind `setOmitted()`, am marcat elementul a cărui valoare nu ne interesează de fapt și care există doar în scopul validării. Valoarea nu se va transmite în `$data`. - -Astfel, avem un formular complet funcțional cu validare în PHP și JavaScript. Capacitățile de validare ale Nette sunt mult mai extinse, se pot crea condiții, se pot afișa și ascunde părți ale paginii în funcție de acestea etc. Veți afla totul în capitolul despre [validarea formularelor|validation]. - - -Valori implicite -================ - -Elementelor formularului le setăm în mod obișnuit valori implicite: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Adesea este util să setăm valori implicite pentru toate elementele simultan. De exemplu, când formularul servește la editarea înregistrărilor. Citim înregistrarea din baza de date și setăm valorile implicite: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Apelați `setDefaults()` după definirea elementelor. - - -Randarea formularului -===================== - -Standard, formularul se randează ca un tabel. Elementele individuale respectă regula de bază a accesibilității - toate etichetele sunt scrise ca `<label>` și legate de elementul de formular corespunzător. La clic pe etichetă, cursorul apare automat în câmpul formularului. - -Fiecărui element îi putem seta atribute HTML arbitrare. De exemplu, adăugăm un placeholder: - -```php -$form->addInteger('age', 'Vârstă:') - ->setHtmlAttribute('placeholder', 'Vă rugăm să completați vârsta'); -``` - -Există într-adevăr o mare varietate de moduri de a randa un formular, așa că acestui subiect i se dedică un [capitol separat despre randare|rendering]. - - -Maparea pe clase -================ - -Să revenim la procesarea datelor formularului. Metoda `getValues()` ne returna datele trimise ca obiect `ArrayHash`. Deoarece este o clasă generică, ceva de genul `stdClass`, ne va lipsi un anumit confort în lucrul cu ea, cum ar fi sugestiile de proprietăți în editori sau analiza statică a codului. Acest lucru ar putea fi rezolvat având o clasă specifică pentru fiecare formular, ale cărei proprietăți reprezintă elementele individuale. De ex.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativ, puteți utiliza constructorul: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Proprietățile clasei de date pot fi, de asemenea, enumuri și vor fi mapate automat. .{data-version:3.2.4} - -Cum să spunem Nette să ne returneze datele ca obiecte ale acestei clase? Mai ușor decât credeți. Este suficient să specificați numele clasei sau obiectul de hidratat ca parametru: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Ca parametru se poate specifica și `'array'`, iar datele vor fi returnate ca array. - -Dacă formularele formează o structură multinivel compusă din containere, creați o clasă separată pentru fiecare: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Maparea va recunoaște apoi din tipul proprietății `$person` că trebuie să mapeze containerul la clasa `PersonFormData`. Dacă proprietatea ar conține un array de containere, specificați tipul `array` și transmiteți clasa pentru mapare direct containerului: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Puteți genera schița clasei de date a formularului folosind metoda `Nette\Forms\Blueprint::dataClass($form)`, care o va afișa în pagina browserului. Apoi, este suficient să selectați codul cu un clic și să îl copiați în proiect. .{data-version:3.1.15} - - -Mai multe butoane -================= - -Dacă formularul are mai mult de un buton, de obicei trebuie să distingem care dintre ele a fost apăsat. Această informație ne este returnată de metoda `isSubmittedBy()` a butonului: - -```php -$form->addSubmit('save', 'Salvare'); -$form->addSubmit('delete', 'Ștergere'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Nu omiteți interogarea `$form->isSuccess()`, aceasta verifică validitatea datelor. - -Când formularul este trimis cu tasta <kbd>Enter</kbd>, se consideră ca și cum ar fi fost trimis cu primul buton. - - -Protecția împotriva vulnerabilităților -====================================== - -Nette Framework acordă o mare importanță securității și, prin urmare, are grijă deosebită de securizarea formularelor. - -Pe lângă protejarea formularelor împotriva atacurilor [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] și [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], realizează o mulțime de mici măsuri de securitate la care nu mai trebuie să vă gândiți. - -De exemplu, filtrează toate caracterele de control din intrări și verifică validitatea codificării UTF-8, astfel încât datele din formular vor fi întotdeauna curate. Pentru casetele de selecție și listele radio, verifică dacă elementele selectate au fost într-adevăr dintre cele oferite și nu a avut loc o falsificare. Am menționat deja că pentru intrările de text de o singură linie elimină caracterele de sfârșit de linie pe care un atacator le-ar fi putut trimite. Pentru intrările multilinie, normalizează caracterele de sfârșit de linie. Și așa mai departe. - -Nette rezolvă pentru dvs. riscurile de securitate despre care mulți programatori nici nu știu că există. - -Atacul CSRF menționat constă în faptul că atacatorul atrage victima pe o pagină care execută discret în browserul victimei o cerere către serverul pe care victima este autentificată, iar serverul crede că cererea a fost executată de victimă din proprie inițiativă. De aceea, Nette previne trimiterea formularelor POST de pe un alt domeniu. Dacă, din anumite motive, doriți să dezactivați protecția și să permiteți trimiterea formularului de pe un alt domeniu, utilizați: - -```php -$form->allowCrossOrigin(); // ATENȚIE! Dezactivează protecția! -``` - -Această protecție utilizează un cookie SameSite numit `_nss`. Prin urmare, creați obiectul formularului înainte de a trimite prima ieșire, pentru a putea trimite cookie-ul. - -Protecția prin cookie SameSite poate să nu fie 100% fiabilă, de aceea este recomandat să activați și protecția prin token: - -```php -$form->addProtection(); -``` - -Recomandăm protejarea în acest mod a formularelor din partea de administrare a site-ului, care modifică date sensibile în aplicație. Framework-ul se apără împotriva atacului CSRF prin generarea și verificarea unui token de autorizare, care este stocat în sesiune. Prin urmare, este necesar să aveți sesiunea deschisă înainte de afișarea formularului. În partea de administrare a site-ului, sesiunea este de obicei deja pornită datorită autentificării utilizatorului. Altfel, porniți sesiunea cu metoda `Nette\Http\Session::start()`. - -Așadar, am parcurs o introducere rapidă în formularele Nette. Încercați să consultați și directorul [examples|https://github.com/nette/forms/tree/master/examples] din distribuție, unde veți găsi mai multă inspirație. diff --git a/forms/ro/validation.texy b/forms/ro/validation.texy deleted file mode 100644 index 8bad54f2d4..0000000000 --- a/forms/ro/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Validarea formularelor -********************** - - -Elemente obligatorii -==================== - -Elementele obligatorii le marcăm cu metoda `setRequired()`, al cărei argument este textul [mesajului de eroare |#Mesaje de eroare], care se afișează dacă utilizatorul nu completează elementul. Dacă nu specificăm argumentul, se va utiliza mesajul de eroare implicit. - -```php -$form->addText('name', 'Nume:') - ->setRequired('Vă rugăm să introduceți numele'); -``` - - -Reguli -====== - -Regulile de validare le adăugăm elementelor cu metoda `addRule()`. Primul parametru este regula, al doilea este textul [mesajului de eroare |#Mesaje de eroare] și al treilea este argumentul regulii de validare. - -```php -$form->addPassword('password', 'Parolă:') - ->addRule($form::MinLength, 'Parola trebuie să aibă cel puțin %d caractere', 8); -``` - -**Regulile de validare se verifică doar în cazul în care utilizatorul a completat elementul.** - -Nette vine cu o serie întreagă de reguli predefinite, ale căror nume sunt constante ale clasei `Nette\Forms\Form`. Pentru toate elementele putem folosi aceste reguli: - -| constantă | descriere | tip argument -|------- -| `Required` | element obligatoriu, alias pentru `setRequired()` | - -| `Filled` | element obligatoriu, alias pentru `setRequired()` | - -| `Blank` | elementul nu trebuie completat | - -| `Equal` | valoarea este egală cu parametrul | `mixed` -| `NotEqual` | valoarea nu este egală cu parametrul | `mixed` -| `IsIn` | valoarea este egală cu unul dintre elementele din array | `array` -| `IsNotIn` | valoarea nu este egală cu niciun element din array | `array` -| `Valid` | elementul este completat corect? (pentru [#condiții]) | - - - -Intrări de text ---------------- - -Pentru elementele `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` se pot utiliza și unele dintre următoarele reguli: - -| `MinLength` | lungimea minimă a textului | `int` -| `MaxLength` | lungimea maximă a textului | `int` -| `Length` | lungimea în interval sau lungimea exactă | pereche `[int, int]` sau `int` -| `Email` | adresă de e-mail validă | - -| `URL` | URL absolut | - -| `Pattern` | se potrivește expresiei regulate | `string` -| `PatternInsensitive` | ca `Pattern`, dar independent de majuscule/minuscule | `string` -| `Integer` | valoare întreagă | - -| `Numeric` | alias pentru `Integer` | - -| `Float` | număr | - -| `Min` | valoarea minimă a elementului numeric | `int\|float` -| `Max` | valoarea maximă a elementului numeric | `int\|float` -| `Range` | valoarea în interval | pereche `[int\|float, int\|float]` - -Regulile de validare `Integer`, `Numeric` și `Float` convertesc direct valoarea la integer, respectiv float. Mai mult, regula `URL` acceptă și o adresă fără schemă (de ex. `nette.org`) și completează schema (`https://nette.org`). Expresia din `Pattern` și `PatternIcase` trebuie să fie valabilă pentru întreaga valoare, adică ca și cum ar fi încadrată de caracterele `^` și `$`. - - -Numărul de elemente -------------------- - -Pentru elementele `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` se pot utiliza și următoarele reguli pentru a limita numărul de elemente selectate, respectiv fișiere încărcate: - -| `MinLength` | număr minim | `int` -| `MaxLength` | număr maxim | `int` -| `Length` | numărul în interval sau numărul exact | pereche `[int, int]` sau `int` - - -Încărcarea fișierelor ---------------------- - -Pentru elementele `addUpload()`, `addMultiUpload()` se pot utiliza și următoarele reguli: - -| `MaxFileSize` | dimensiunea maximă a fișierului în octeți | `int` -| `MimeType` | tip MIME, permise caractere wildcard (`'video/*'`) | `string\|string[]` -| `Image` | imagine JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | numele fișierului se potrivește expresiei regulate | `string` -| `PatternInsensitive` | ca `Pattern`, dar independent de majuscule/minuscule | `string` - -`MimeType` și `Image` necesită extensia PHP `fileinfo`. Faptul că fișierul sau imaginea este de tipul dorit este detectat pe baza semnăturii sale și **nu verifică integritatea întregului fișier.** Dacă imaginea nu este deteriorată, se poate afla, de exemplu, încercând să o [încărcați |http:request#toImage]. - - -Mesaje de eroare -================ - -Toate regulile predefinite, cu excepția `Pattern` și `PatternInsensitive`, au un mesaj de eroare implicit, deci acesta poate fi omis. Cu toate acestea, specificarea și formularea tuturor mesajelor personalizate va face formularul mai prietenos pentru utilizator. - -Puteți modifica mesajele implicite în [configurație|forms:configuration], editând textele din array-ul `Nette\Forms\Validator::$messages` sau folosind un [translator |rendering#Traducere]. - -În textul mesajelor de eroare se pot utiliza următorii substituenți: - -| `%d` | înlocuiește succesiv cu argumentele regulii -| `%n$d` | înlocuiește cu al n-lea argument al regulii -| `%label` | înlocuiește cu eticheta elementului (fără două puncte) -| `%name` | înlocuiește cu numele elementului (de ex. `name`) -| `%value` | înlocuiește cu valoarea introdusă de utilizator - -```php -$form->addText('name', 'Nume:') - ->setRequired('Vă rugăm să completați %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'cel puțin %d și cel mult %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'cel mult %2$d și cel puțin %1$d', [5, 10]); -``` - - -Condiții -======== - -Pe lângă reguli, se pot adăuga și condiții. Acestea se scriu similar cu regulile, doar că în loc de `addRule()` folosim metoda `addCondition()` și, desigur, nu specificăm niciun mesaj de eroare (condiția doar întreabă): - -```php -$form->addPassword('password', 'Parolă:') - // dacă parola nu este mai lungă de 8 caractere - ->addCondition($form::MaxLength, 8) - // atunci trebuie să conțină o cifră - ->addRule($form::Pattern, 'Trebuie să conțină o cifră', '.*[0-9].*'); -``` - -Condiția poate fi legată și de alt element decât cel curent folosind `addConditionOn()`. Ca prim parametru, specificăm referința la element. În acest exemplu, e-mailul va fi obligatoriu doar dacă se bifează checkbox-ul (valoarea sa va fi true): - -```php -$form->addCheckbox('newsletters', 'trimiteți-mi newslettere'); - -$form->addEmail('email', 'E-mail:') - // dacă checkbox-ul este bifat - ->addConditionOn($form['newsletters'], $form::Equal, true) - // atunci solicită e-mail - ->setRequired('Introduceți adresa de e-mail'); -``` - -Din condiții se pot crea structuri complexe folosind `elseCondition()` și `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // dacă prima condiție este îndeplinită - ->addConditionOn(/* ... */) // și a doua condiție pe un alt element - ->addRule(/* ... */) // solicită această regulă - ->elseCondition() // dacă a doua condiție nu este îndeplinită - ->addRule(/* ... */) // solicită aceste reguli - ->addRule(/* ... */) - ->endCondition() // ne întoarcem la prima condiție - ->addRule(/* ... */); -``` - -În Nette se poate reacționa foarte ușor la îndeplinirea sau neîndeplinirea condiției și pe partea de JavaScript folosind metoda `toggle()`, vezi [#javascript-dinamic]. - - -Referință la alt element -======================== - -Ca argument al regulii sau condiției se poate transmite și alt element al formularului. Regula va folosi atunci valoarea introdusă ulterior de utilizator în browser. Astfel se poate valida dinamic, de exemplu, că elementul `password` conține același șir ca elementul `password_confirm`: - -```php -$form->addPassword('password', 'Parolă'); -$form->addPassword('password_confirm', 'Confirmați parola') - ->addRule($form::Equal, 'Parolele introduse nu se potrivesc', $form['password']); -``` - - -Reguli și condiții personalizate -================================ - -Uneori ajungem în situația în care regulile de validare încorporate în Nette nu sunt suficiente și trebuie să validăm datele de la utilizator în felul nostru. În Nette este foarte simplu! - -Metodelor `addRule()` sau `addCondition()` li se poate transmite ca prim parametru orice callback. Acesta primește ca prim parametru elementul însuși și returnează o valoare booleană care indică dacă validarea a avut loc cu succes. La adăugarea unei reguli folosind `addRule()`, se pot specifica și alte argumente, acestea fiind apoi transmise ca al doilea parametru. - -Putem crea astfel propriul set de validatori ca o clasă cu metode statice: - -```php -class MyValidators -{ - // testează dacă valoarea este divizibilă cu argumentul - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // alți validatori - } -} -``` - -Utilizarea este apoi foarte simplă: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Valoarea trebuie să fie un multiplu al numărului %d', - 8, - ); -``` - -Regulile de validare personalizate pot fi adăugate și în JavaScript. Condiția este ca regula să fie o metodă statică. Numele său pentru validatorul JavaScript se formează prin concatenarea numelui clasei fără backslash-uri `\`, a unui underscore `_` și a numelui metodei. De ex. `App\MyValidators::validateDivisibility` se scrie ca `AppMyValidators_validateDivisibility` și se adaugă la obiectul `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Evenimentul onValidate -====================== - -După trimiterea formularului, se efectuează validarea, în timpul căreia se verifică regulile individuale adăugate folosind `addRule()` și apoi se declanșează [evenimentul |nette:glossary#Evenimente] `onValidate`. Handler-ul său poate fi utilizat pentru validare suplimentară, de obicei verificarea combinației corecte de valori în mai multe elemente ale formularului. - -Dacă se descoperă o eroare, o transmitem formularului prin metoda `addError()`. Aceasta poate fi apelată fie pe un element specific, fie direct pe formular. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Această combinație nu este posibilă.'); - } -} -``` - - -Erori în timpul procesării -========================== - -În multe cazuri, aflăm despre eroare abia în momentul în care procesăm formularul valid, de exemplu, scriem un nou element în baza de date și întâlnim o duplicitate a cheilor. În acest caz, transmitem din nou eroarea formularului prin metoda `addError()`. Aceasta poate fi apelată fie pe un element specific, fie direct pe formular: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Parolă invalidă.'); - } -} -``` - -Dacă este posibil, recomandăm atașarea erorii direct la elementul formularului, deoarece aceasta va fi afișată lângă el la utilizarea renderer-ului implicit. - -```php -$form['date']->addError('Ne pare rău, dar această dată este deja ocupată.'); -``` - -Puteți apela `addError()` în mod repetat pentru a transmite formularului sau elementului mai multe mesaje de eroare. Le puteți obține folosind `getErrors()`. - -Atenție, `$form->getErrors()` returnează un sumar al tuturor mesajelor de eroare, inclusiv cele transmise direct elementelor individuale, nu doar direct formularului. Mesajele de eroare transmise doar formularului le puteți obține prin `$form->getOwnErrors()`. - - -Modificarea intrării -==================== - -Folosind metoda `addFilter()`, putem modifica valoarea introdusă de utilizator. În acest exemplu, vom tolera și elimina spațiile din codul poștal: - -```php -$form->addText('zip', 'Cod poștal:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // eliminăm spațiile din codul poștal - }) - ->addRule($form::Pattern, 'Codul poștal nu este în format de cinci cifre', '\d{5}'); -``` - -Filtrul se integrează între regulile și condițiile de validare, deci ordinea metodelor contează, adică filtrul și regula se apelează în ordinea în care sunt metodele `addFilter()` și `addRule()`. - - -Validare JavaScript -=================== - -Limbajul pentru formularea condițiilor și regulilor este foarte puternic. Toate construcțiile funcționează atât pe partea de server, cât și pe partea de JavaScript. Acestea sunt transmise în atributele HTML `data-nette-rules` ca JSON. Validarea propriu-zisă este apoi efectuată de un script care interceptează evenimentul `submit` al formularului, parcurge elementele individuale și efectuează validarea corespunzătoare. - -Acest script este `netteForms.js` și este disponibil din mai multe surse posibile: - -Puteți include scriptul direct în pagina HTML de pe CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Sau copiați-l local în folderul public al proiectului (de ex. din `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Sau instalați-l prin [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -Și apoi încărcați-l și rulați-l: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Alternativ, îl puteți încărca direct din folderul `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -JavaScript dinamic -================== - -Doriți să afișați câmpurile pentru introducerea adresei doar dacă utilizatorul alege să primească produsul prin poștă? Nicio problemă. Cheia este perechea de metode `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Acest cod spune că atunci când condiția este îndeplinită, adică atunci când checkbox-ul este bifat, elementul HTML `#address-container` va fi vizibil. Și invers. Elementele formularului cu adresa destinatarului le vom plasa astfel într-un container cu acest ID, iar la clic pe checkbox, acestea se vor ascunde sau afișa. Acest lucru este asigurat de scriptul `netteForms.js`. - -Ca argument al metodei `toggle()` se poate transmite orice selector. Din motive istorice, un șir alfanumeric fără alte caractere speciale este înțeles ca ID-ul elementului, adică la fel ca și cum ar fi precedat de caracterul `#`. Al doilea parametru opțional permite inversarea comportamentului, adică dacă am folosi `toggle('#address-container', false)`, elementul s-ar afișa doar dacă checkbox-ul nu ar fi bifat. - -Implementarea implicită în JavaScript modifică proprietatea `hidden` a elementelor. Putem însă schimba ușor comportamentul, de exemplu, adăugând o animație. Este suficient să suprascriem în JavaScript metoda `Nette.toggle` cu propria soluție: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // ascundem sau afișăm 'el' în funcție de valoarea 'visible' - }); -}; -``` - - -Dezactivarea validării -====================== - -Uneori poate fi util să dezactivăm validarea. Dacă apăsarea butonului de trimitere nu trebuie să efectueze validarea (potrivit pentru butoanele *Cancel* sau *Preview*), o dezactivăm cu metoda `$submit->setValidationScope([])`. Dacă trebuie să efectueze doar o validare parțială, putem specifica ce câmpuri sau containere de formular trebuie validate. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Validează întregul formular -$form->addSubmit('send2') - ->setValidationScope([]); // Nu validează deloc -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Validează doar elementul name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Validează doar elementul age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Validează containerul details -``` - -`setValidationScope` nu afectează [#evenimentul onValidate] al formularului, care va fi apelat întotdeauna. Evenimentul `onValidate` al containerului va fi declanșat doar dacă acest container este marcat pentru validare parțială. diff --git a/forms/sl/@home.texy b/forms/sl/@home.texy deleted file mode 100644 index b71b6a213f..0000000000 --- a/forms/sl/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Nette Forms so prinesli revolucijo v ustvarjanje spletnih obrazcev. Naenkrat je bilo dovolj napisati nekaj razumljivih vrstic kode in imeli ste pripravljen obrazec, vključno z izrisovanjem, JavaScript in strežniško validacijo ter poleg tega vrhunsko zaščiten. Pokazali bomo, kako - -- ustvarjati prijazne obrazce -- validirati poslane podatke -- izrisovati elemente natančno po potrebi - -</div> - - -Z uporabo Nette Forms se izognete celi vrsti rutinskih nalog, kot je na primer pisanje validacije (poleg tega dvojne, na strani strežnika in odjemalca), minimizirate verjetnost nastanka napak in varnostnih lukenj. - -Obrazce lahko uporabljate bodisi kot del Nette Aplikacije (torej v presenterjih), bodisi popolnoma samostojno. Ker se v obeh primerih uporaba nekoliko razlikuje, smo za vas pripravili dva navodila: - -<div class="wiki-buttons"> -<div> "Obrazci v presenterjih .[wiki-button]":in-presenter </div> -<div> "Obrazci samostojno .[wiki-button]":standalone </div> -</div> - - -Namestitev ----------- - -Knjižnico prenesete in namestite z orodjem [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/sl/@left-menu.texy b/forms/sl/@left-menu.texy deleted file mode 100644 index 10d9d69543..0000000000 --- a/forms/sl/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Uvod |@home] -- [Obrazci v presenterjih|in-presenter] -- [Obrazci samostojno|standalone] -- [Elementi obrazca |controls] -- [Validacija |validation] -- [Izrisovanje |rendering] -- [Konfiguracija |configuration] - - -Nadaljnje branje -**************** -- [Navodila in postopki |best-practices:] diff --git a/forms/sl/@meta.texy b/forms/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/forms/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/forms/sl/configuration.texy b/forms/sl/configuration.texy deleted file mode 100644 index 6ca404d63c..0000000000 --- a/forms/sl/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Konfiguracija obrazcev -********************** - -.[perex] -V konfiguraciji lahko spremenite privzeta [sporočila o napakah obrazcev|validation]. - -```neon -forms: - messages: - 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.' - 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.' -``` - -Tukaj je slovenski prevod: - -```neon -forms: - messages: - Equal: 'Vnesite %s.' - NotEqual: 'Ta vrednost ne sme biti %s.' - Filled: 'To polje je obvezno.' - Blank: 'To polje mora biti prazno.' - MinLength: 'Vnesite vsaj %d znakov.' - MaxLength: 'Vnesite največ %d znakov.' - Length: 'Vnesite vrednost dolžine med %d in %d znakov.' - Email: 'Vnesite veljaven e-poštni naslov.' - URL: 'Vnesite veljaven URL.' - Integer: 'Vnesite veljavno celo število.' - Float: 'Vnesite veljavno število.' - Min: 'Vnesite vrednost večjo ali enako %d.' - Max: 'Vnesite vrednost manjšo ali enako %d.' - Range: 'Vnesite vrednost med %d in %d.' - MaxFileSize: 'Velikost naložene datoteke je lahko največ %d bajtov.' - MaxPostSize: 'Naloženi podatki presegajo omejitev %d bajtov.' - MimeType: 'Naložena datoteka ni v pričakovanem formatu.' - Image: 'Naložena datoteka mora biti slika v formatu JPEG, GIF, PNG, WebP ali AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Izberite veljavno možnost.' - Nette\Forms\Controls\UploadControl::Valid: 'Pri nalaganju datoteke je prišlo do napake.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Vaša seja je potekla. Vrnite se na domačo stran in poskusite znova.' -``` - -Če ne uporabljate celotnega ogrodja in torej niti konfiguracijskih datotek, lahko spremenite privzeta sporočila o napakah neposredno v polju `Nette\Forms\Validator::$messages`. diff --git a/forms/sl/controls.texy b/forms/sl/controls.texy deleted file mode 100644 index b03b9e0978..0000000000 --- a/forms/sl/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Elementi obrazca -**************** - -.[perex] -Pregled standardnih elementov obrazca. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Doda enovrstično besedilno polje (razred [TextInput |api:Nette\Forms\Controls\TextInput]). Če uporabnik polja ne izpolni, vrne prazen niz `''`, ali pa s pomočjo `setNullable()` lahko določite, da vrne `null`. - -```php -$form->addText('name', 'Ime:') - ->setRequired() - ->setNullable(); -``` - -Samodejno validira UTF-8, obreže leve in desne presledke ter odstrani prelome vrstic, ki bi jih lahko poslal napadalec. - -Maksimalno dolžino lahko omejite s pomočjo `setMaxLength()`. Spreminjanje vrednosti, ki jo vnese uporabnik, omogoča [addFilter() |validation#Spreminjanje vnosa]. - -S pomočjo `setHtmlType()` lahko spremenite vizualni značaj besedilnega polja na tipe, kot so `search`, `tel` ali `url`, glej [specifikacijo|https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Ne pozabite, da je sprememba tipa le vizualna in ne nadomešča funkcije validacije. Za tip `url` je priporočljivo dodati specifično validacijsko [pravilo URL |validation#Besedilni vnosi]. - -.[note] -Za druge tipe vnosov, kot so `number`, `range`, `email`, `date`, `datetime-local`, `time` in `color`, uporabite specializirane metode, kot so [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] in [#addColor], ki zagotavljajo strežniško validacijo. Tipi `month` in `week` še niso popolnoma podprti v vseh brskalnikih. - -Elementu lahko nastavite t.i. empty-value, kar je nekaj podobnega privzeti vrednosti, a če je uporabnik ne spremeni, element vrne prazen niz ali `null`. - -```php -$form->addText('phone', 'Telefon:') - ->setHtmlType('tel') - ->setEmptyValue('+386'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Doda polje za vnos večvrstičnega besedila (razred [TextArea |api:Nette\Forms\Controls\TextArea]). Če uporabnik polja ne izpolni, vrne prazen niz `''`, ali pa s pomočjo `setNullable()` lahko določite, da vrne `null`. - -```php -$form->addTextArea('note', 'Opomba:') - ->addRule($form::MaxLength, 'Opomba je predolga', 10000); -``` - -Samodejno validira UTF-8 in normalizira ločila vrstic na `\n`. V nasprotju z enovrstičnim vnosnim poljem ne pride do obrezovanja presledkov. - -Maksimalno dolžino lahko omejite s pomočjo `setMaxLength()`. Spreminjanje vrednosti, ki jo vnese uporabnik, omogoča [addFilter() |validation#Spreminjanje vnosa]. Lahko nastavite t.i. empty-value s pomočjo `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Doda polje za vnos celega števila (razred [TextInput |api:Nette\Forms\Controls\TextInput]). Vrne bodisi integer ali `null`, če uporabnik ničesar ne vnese. - -```php -$form->addInteger('year', 'Leto:') - ->addRule($form::Range, 'Leto mora biti v obsegu od %d do %d.', [1900, 2023]); -``` - -Element se izriše kot `<input type="number">`. Z uporabo metode `setHtmlType()` lahko spremenite tip na `range` za prikaz v obliki drsnika ali na `text`, če preferirate standardno besedilno polje brez posebnega obnašanja tipa `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Doda polje za vnos decimalnega števila (razred [TextInput |api:Nette\Forms\Controls\TextInput]). Vrne bodisi float ali `null`, če uporabnik ničesar ne vnese. - -```php -$form->addFloat('level', 'Raven:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Raven mora biti v obsegu od %d do %d.', [0, 100]); -``` - -Element se izriše kot `<input type="number">`. Z uporabo metode `setHtmlType()` lahko spremenite tip na `range` za prikaz v obliki drsnika ali na `text`, če preferirate standardno besedilno polje brez posebnega obnašanja tipa `number`. - -Nette in brskalnik Chrome sprejemata kot ločilo decimalnih mest tako vejico kot piko. Da bi bila ta funkcionalnost na voljo tudi v Firefoxu, je priporočljivo nastaviti atribut `lang` bodisi za dani element ali za celotno stran, na primer `<html lang="sl">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Doda polje za vnos e-poštnega naslova (razred [TextInput |api:Nette\Forms\Controls\TextInput]). Če uporabnik polja ne izpolni, vrne prazen niz `''`, ali pa s pomočjo `setNullable()` lahko določite, da vrne `null`. - -```php -$form->addEmail('email', 'E-pošta:'); -``` - -Preveri, ali je vrednost veljaven e-poštni naslov. Ne preverja se, ali domena dejansko obstaja, preverja se le sintaksa. Samodejno validira UTF-8, obreže leve in desne presledke. - -Maksimalno dolžino lahko omejite s pomočjo `setMaxLength()`. Spreminjanje vrednosti, ki jo vnese uporabnik, omogoča [addFilter() |validation#Spreminjanje vnosa]. Lahko nastavite t.i. empty-value s pomočjo `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Doda polje za vnos gesla (razred [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Geslo:') - ->setRequired() - ->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8) - ->addRule($form::Pattern, 'Mora vsebovati števko', '.*[0-9].*'); -``` - -Pri ponovnem prikazu obrazca bo polje prazno. Samodejno validira UTF-8, obreže leve in desne presledke ter odstrani prelome vrstic, ki bi jih lahko poslal napadalec. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Doda potrditveno polje (razred [Checkbox |api:Nette\Forms\Controls\Checkbox]). Vrne vrednost bodisi `true` ali `false`, glede na to, ali je označeno. - -```php -$form->addCheckbox('agree', 'Strinjam se s pogoji') - ->setRequired('Potrebno je strinjanje s pogoji'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Doda potrditvena polja za izbiro več postavk (razred [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Vrne polje ključev izbranih postavk. Metoda `getSelectedItems()` vrne vrednosti namesto ključev. - -```php -$form->addCheckboxList('colors', 'Barve:', [ - 'r' => 'rdeča', - 'g' => 'zelena', - 'b' => 'modra', -]); -``` - -Polje ponujenih postavk predamo kot tretji parameter ali z metodo `setItems()`. - -S pomočjo `setDisabled(['r', 'g'])` lahko deaktivirate posamezne postavke. - -Element samodejno preverja, da ni prišlo do ponarejanja in da so izbrane postavke dejansko ene izmed ponujenih in niso bile deaktivirane. Z metodo `getRawValue()` lahko pridobite poslane postavke brez tega pomembnega preverjanja. - -Pri nastavitvi privzetih izbranih postavk tudi preverja, da gre za ene izmed ponujenih, sicer vrže izjemo. To preverjanje lahko izklopite s pomočjo `checkDefaultValue(false)`. - -Če pošiljate obrazec z metodo `GET`, lahko izberete kompaktnejši način prenosa podatkov, ki prihrani velikost poizvedbenega niza (query string). Aktivira se z nastavitvijo HTML atributa obrazca: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Doda izbirne gumbe (radio buttons) (razred [RadioList |api:Nette\Forms\Controls\RadioList]). Vrne ključ izbrane postavke ali `null`, če uporabnik ničesar ni izbral. Metoda `getSelectedItem()` vrne vrednost namesto ključa. - -```php -$sex = [ - 'm' => 'moški', - 'f' => 'ženska', -]; -$form->addRadioList('gender', 'Spol:', $sex); -``` - -Polje ponujenih postavk predamo kot tretji parameter ali z metodo `setItems()`. - -S pomočjo `setDisabled(['m', 'f'])` lahko deaktivirate posamezne postavke. - -Element samodejno preverja, da ni prišlo do ponarejanja in da je izbrana postavka dejansko ena izmed ponujenih in ni bila deaktivirana. Z metodo `getRawValue()` lahko pridobite poslano postavko brez tega pomembnega preverjanja. - -Pri nastavitvi privzete izbrane postavke tudi preverja, da gre za eno izmed ponujenih, sicer vrže izjemo. To preverjanje lahko izklopite s pomočjo `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Doda izbirno polje (select box) (razred [SelectBox |api:Nette\Forms\Controls\SelectBox]). Vrne ključ izbrane postavke ali `null`, če uporabnik ničesar ni izbral. Metoda `getSelectedItem()` vrne vrednost namesto ključa. - -```php -$countries = [ - 'CZ' => 'Češka Republika', - 'SK' => 'Slovaška', - 'GB' => 'Velika Britanija', -]; - -$form->addSelect('country', 'Država:', $countries) - ->setDefaultValue('SK'); -``` - -Polje ponujenih postavk predamo kot tretji parameter ali z metodo `setItems()`. Postavke so lahko tudi dvodimenzionalno polje: - -```php -$countries = [ - 'Europe' => [ - 'CZ' => 'Češka Republika', - 'SK' => 'Slovaška', - 'GB' => 'Velika Britanija', - ], - 'CA' => 'Kanada', - 'US' => 'ZDA', - '?' => 'druga', -]; -``` - -Pri izbirnih poljih ima pogosto prva postavka poseben pomen, služi kot poziv k akciji. Za dodajanje takšne postavke služi metoda `setPrompt()`. - -```php -$form->addSelect('country', 'Država:', $countries) - ->setPrompt('Izberite državo'); -``` - -S pomočjo `setDisabled(['CZ', 'SK'])` lahko deaktivirate posamezne postavke. - -Element samodejno preverja, da ni prišlo do ponarejanja in da je izbrana postavka dejansko ena izmed ponujenih in ni bila deaktivirana. Z metodo `getRawValue()` lahko pridobite poslano postavko brez tega pomembnega preverjanja. - -Pri nastavitvi privzete izbrane postavke tudi preverja, da gre za eno izmed ponujenih, sicer vrže izjemo. To preverjanje lahko izklopite s pomočjo `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Doda izbirno polje za izbiro več postavk (razred [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Vrne polje ključev izbranih postavk. Metoda `getSelectedItems()` vrne vrednosti namesto ključev. - -```php -$form->addMultiSelect('countries', 'Države:', $countries); -``` - -Polje ponujenih postavk predamo kot tretji parameter ali z metodo `setItems()`. Postavke so lahko tudi dvodimenzionalno polje. - -S pomočjo `setDisabled(['CZ', 'SK'])` lahko deaktivirate posamezne postavke. - -Element samodejno preverja, da ni prišlo do ponarejanja in da so izbrane postavke dejansko ene izmed ponujenih in niso bile deaktivirane. Z metodo `getRawValue()` lahko pridobite poslane postavke brez tega pomembnega preverjanja. - -Pri nastavitvi privzetih izbranih postavk tudi preverja, da gre za ene izmed ponujenih, sicer vrže izjemo. To preverjanje lahko izklopite s pomočjo `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Doda polje za nalaganje datoteke (razred [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrne objekt [FileUpload |http:request#FileUpload], in to tudi v primeru, da uporabnik nobene datoteke ni poslal, kar lahko ugotovite z metodo `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Avatar:') - ->addRule($form::Image, 'Avatar mora biti JPEG, PNG, GIF, WebP ali AVIF.') - ->addRule($form::MaxFileSize, 'Maksimalna velikost je 1 MB.', 1024 * 1024); -``` - -Če se datoteka ne uspe pravilno naložiti, obrazec ni uspešno poslan in prikaže se napaka. Tj. pri uspešni oddaji ni treba preverjati metode `FileUpload::isOk()`. - -Nikoli ne zaupajte originalnemu imenu datoteke, vrnjenemu z metodo `FileUpload::getName()`, klient bi lahko poslal škodljivo ime datoteke z namenom poškodovati ali vdreti v vašo aplikacijo. - -Pravili `MimeType` in `Image` zaznata zahtevani tip na podlagi signature datoteke in ne preverjata njene integritete. Ali slika ni poškodovana, lahko ugotovite na primer s poskusom njenega [nalaganja |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Doda polje za nalaganje več datotek hkrati (razred [UploadControl |api:Nette\Forms\Controls\UploadControl]). Vrne polje objektov [FileUpload |http:request#FileUpload]. Metoda `FileUpload::hasFile()` pri vsakem od njih bo vračala `true`. - -```php -$form->addMultiUpload('files', 'Datoteke:') - ->addRule($form::MaxLength, 'Maksimalno lahko naložite %d datotek', 10); -``` - -Če se katera koli datoteka ne uspe pravilno naložiti, obrazec ni uspešno poslan in prikaže se napaka. Tj. pri uspešni oddaji ni treba preverjati metode `FileUpload::isOk()`. - -Nikoli ne zaupajte originalnim imenom datotek, vrnjenim z metodo `FileUpload::getName()`, klient bi lahko poslal škodljivo ime datoteke z namenom poškodovati ali vdreti v vašo aplikacijo. - -Pravili `MimeType` in `Image` zaznata zahtevani tip na podlagi signature datoteke in ne preverjata njene integritete. Ali slika ni poškodovana, lahko ugotovite na primer s poskusom njenega [nalaganja |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Doda polje, ki uporabniku omogoča enostaven vnos datuma, sestavljenega iz leta, meseca in dneva (razred [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Kot privzeto vrednost sprejema bodisi objekte, ki implementirajo vmesnik `DateTimeInterface`, niz s časom ali število, ki predstavlja UNIX časovni žig. Enako velja za argumente pravil `Min`, `Max` ali `Range`, ki definirajo minimalni in maksimalni dovoljeni datum. - -```php -$form->addDate('date', 'Datum:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Datum mora biti vsaj en mesec star.', new DateTime('-1 month')); -``` - -Standardno vrne objekt `DateTimeImmutable`, z metodo `setFormat()` lahko specificirate [besedilni format|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ali časovni žig: - -```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} -=============================================================================================================== - -Doda polje, ki uporabniku omogoča enostaven vnos časa, sestavljenega iz ur, minut in opcijsko tudi sekund (razred [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Kot privzeto vrednost sprejema bodisi objekte, ki implementirajo vmesnik `DateTimeInterface`, niz s časom ali število, ki predstavlja UNIX časovni žig. Iz teh vnosov je uporabljena le časovna informacija, datum je ignoriran. Enako velja za argumente pravil `Min`, `Max` ali `Range`, ki definirajo minimalni in maksimalni dovoljeni čas. Če je nastavljena minimalna vrednost višja od maksimalne, se ustvari časovni obseg, ki presega polnoč. - -```php -$form->addTime('time', 'Čas:', withSeconds: true) - ->addRule($form::Range, 'Čas mora biti v obsegu od %d do %d.', ['12:30', '13:30']); -``` - -Standardno vrne objekt `DateTimeImmutable` (z datumom 1. januarja leta 1), z metodo `setFormat()` lahko specificirate [besedilni format|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} -=================================================================================================================== - -Doda polje, ki uporabniku omogoča enostaven vnos datuma in časa, sestavljenega iz leta, meseca, dneva, ur, minut in opcijsko tudi sekund (razred [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Kot privzeto vrednost sprejema bodisi objekte, ki implementirajo vmesnik `DateTimeInterface`, niz s časom ali število, ki predstavlja UNIX časovni žig. Enako velja za argumente pravil `Min`, `Max` ali `Range`, ki definirajo minimalni in maksimalni dovoljeni datum. - -```php -$form->addDateTime('datetime', 'Datum in čas:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Datum mora biti vsaj en mesec star.', new DateTime('-1 month')); -``` - -Standardno vrne objekt `DateTimeImmutable`, z metodo `setFormat()` lahko specificirate [besedilni format|https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] ali časovni žig: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Doda polje za izbiro barve (razred [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Barva je niz v obliki `#rrggbb`. Če uporabnik izbire ne opravi, se vrne črna barva `#000000`. - -```php -$form->addColor('color', 'Barva:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Doda skrito polje (razred [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -S pomočjo `setNullable()` lahko nastavite, da vrne `null` namesto praznega niza. Spreminjanje poslane vrednosti omogoča [addFilter() |validation#Spreminjanje vnosa]. - -Čeprav je element skrit, je **pomembno se zavedati**, da lahko vrednost še vedno spremeni ali ponaredi napadalec. Vedno temeljito preverjajte in validirajte vse prejete vrednosti na strežniški strani, da preprečite varnostna tveganja, povezana z manipulacijo podatkov. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Doda gumb za oddajo (razred [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Pošlji'); -``` - -V obrazcu je mogoče imeti tudi več gumbov za oddajo: - -```php -$form->addSubmit('register', 'Registriraj'); -$form->addSubmit('cancel', 'Prekliči'); -``` - -Za ugotovitev, na katerega od njih je bilo kliknjeno, uporabite: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Če ne želite validirati celotnega obrazca ob pritisku na gumb (na primer pri gumbih *Prekliči* ali *Predogled*), uporabite [setValidationScope() |validation#Izklop validacije]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Doda gumb (razred [Button |api:Nette\Forms\Controls\Button]), ki nima funkcije oddaje. Lahko ga torej uporabite za kakšno drugo funkcijo, npr. klic JavaScript funkcije ob kliku. - -```php -$form->addButton('raise', 'Zvišaj plačo') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Doda gumb za oddajo v obliki slike (razred [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/pot/do/slike'); -``` - -Pri uporabi več gumbov za oddajo lahko ugotovite, na katerega je bilo kliknjeno, s pomočjo `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Doda podobrazec (razred [Container|api:Nette\Forms\Container]), ali vsebnik, v katerega lahko dodajate druge elemente na enak način, kot jih dodajamo v obrazec. Delujejo tudi metode `setDefaults()` ali `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Vaše ime:'); -$sub1->addEmail('email', 'E-pošta:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Vaše ime:'); -$sub2->addEmail('email', 'E-pošta:'); -``` - -Poslani podatki se nato vrnejo kot večdimenzionalna struktura: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Pregled nastavitev -================== - -Pri vseh elementih lahko kličemo naslednje metode (popoln pregled v [API dokumentaciji|https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | nastavi privzeto vrednost -| `getValue()` | pridobi trenutno vrednost -| `setOmitted()` | [#Izpustitev vrednosti] -| `setDisabled()` | [#Deaktivacija elementov] - -Izrisovanje: -.[table-form-methods language-php] -| `setCaption($caption)` | spremeni oznako elementa -| `setTranslator($translator)` | nastavi [prevajalnik |rendering#Prevajanje] -| `setHtmlAttribute($name, $value)` | nastavi [HTML atribut |rendering#HTML atributi] elementa -| `setHtmlId($id)` | nastavi HTML atribut `id` -| `setHtmlType($type)` | nastavi HTML atribut `type` -| `setHtmlName($name)` | nastavi HTML atribut `name` -| `setOption($key, $value)` | [nastavitve za izrisovanje |rendering#Možnosti Options] - -Validacija: -.[table-form-methods language-php] -| `setRequired()` | [obvezni element |validation] -| `addRule()` | nastavitev [validacijsko pravilo |validation#Pravila] -| `addCondition()`, `addConditionOn()` | nastavi [validacijski pogoj |validation#Pogoji] -| `addError($message)` | [predaja sporočila o napaki |validation#Napake pri obdelavi] - -Pri elementih `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` lahko kličemo naslednje metode: - -.[table-form-methods language-php] -| `setNullable()` | nastavi, ali getValue() vrne `null` namesto praznega niza -| `setEmptyValue($value)` | nastavi posebno vrednost, ki se šteje za prazen niz -| `setMaxLength($length)` | nastavi maksimalno število dovoljenih znakov -| `addFilter($filter)` | [prilagoditev vnosa |validation#Spreminjanje vnosa] - - -Izpustitev vrednosti -==================== - -Če nas vrednost, ki jo je vnesel uporabnik, ne zanima, jo lahko s pomočjo `setOmitted()` izpustimo iz rezultata metode `$form->getValues()` ali iz podatkov, predanih handlerjem. To je koristno za različna gesla za preverjanje, antispam elemente itd. - -```php -$form->addPassword('passwordVerify', 'Geslo za preverjanje:') - ->setRequired('Prosimo, vnesite geslo še enkrat za preverjanje') - ->addRule($form::Equal, 'Gesli se ne ujemata', $form['password']) - ->setOmitted(); -``` - - -Deaktivacija elementov -====================== - -Elemente lahko deaktivirate s pomočjo `setDisabled()`. Takšnega elementa uporabnik ne more urejati. - -```php -$form->addText('username', 'Uporabniško ime:') - ->setDisabled(); -``` - -Onemogočeni elementi brskalnik sploh ne pošilja na strežnik, torej jih niti ne najdete v podatkih, vrnjenih s funkcijo `$form->getValues()`. Če pa nastavite `setOmitted(false)`, Nette v te podatke vključi njihovo privzeto vrednost. - -Pri klicu `setDisabled()` se iz varnostnih razlogov **izbriše vrednost elementa**. Če nastavljate privzeto vrednost, je to treba storiti šele po njegovi deaktivaciji: - -```php -$form->addText('username', 'Uporabniško ime:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Alternativa onemogočenim elementom so elementi s HTML atributom `readonly`, ki jih brskalnik pošilja na strežnik. Čeprav je element samo za branje, je **pomembno se zavedati**, da lahko njegovo vrednost še vedno spremeni ali ponaredi napadalec. - - -Lastni elementi -=============== - -Poleg široke palete vgrajenih elementov obrazca lahko v obrazec dodajate lastne elemente na ta način: - -```php -$form->addComponent(new DateInput('Datum:'), 'date'); -// alternativna sintaksa: $form['date'] = new DateInput('Datum:'); -``` - -.[note] -Obrazec je potomec razreda [Container |component-model:#Container] in posamezni elementi so potomci [Component |component-model:#Component]. - -Obstaja način, kako definirati nove metode obrazca, ki služijo za dodajanje lastnih elementov (npr. `$form->addZip()`). Gre za t.i. extension methods. Slabost je, da zanje ne bo delovalo predlaganje v urejevalnikih. - -```php -use Nette\Forms\Container; - -// dodamo metodo 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, 'Vsaj 5 številk', '[0-9]{5}'); -}); - -// uporaba -$form->addZip('zip', 'Poštna številka:'); -``` - - -Nizkonivojski elementi -====================== - -Lahko uporabljamo tudi elemente, ki jih zapišemo samo v predlogi in jih ne dodamo v obrazec z nobeno od metod `$form->addXyz()`. Ko na primer izpisujemo zapise iz podatkovne baze in vnaprej ne vemo, koliko jih bo in kakšne ID-je bodo imeli, in želimo pri vsaki vrstici prikazati potrditveno polje ali izbirni gumb, ga zadostuje kodirati v predlogi: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -In po oddaji vrednost ugotovimo: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -kjer prvi parameter je tip elementa (`DataFile` za `type=file`, `DataLine` za enovrstične vnose kot `text`, `password`, `email` ipd. in `DataText` za vse ostale) in drugi parameter `sel[]` ustreza HTML atributu name. Tip elementa lahko kombiniramo z vrednostjo `DataKeys`, ki ohrani ključe elementov. To je koristno zlasti za `select`, `radioList` in `checkboxList`. - -Bistveno je, da `getHttpData()` vrne sanirano vrednost, v tem primeru bo to vedno polje veljavnih UTF-8 nizov, ne glede na to, kaj bi poskušal napadalec podtakniti strežniku. Gre za analogijo neposrednega dela z `$_POST` ali `$_GET`, vendar s to bistveno razliko, da vedno vrne čiste podatke, tako kot ste navajeni pri standardnih elementih Nette obrazcev. diff --git a/forms/sl/in-presenter.texy b/forms/sl/in-presenter.texy deleted file mode 100644 index e09ea5be98..0000000000 --- a/forms/sl/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Obrazci v presenterjih -********************** - -.[perex] -Nette Forms bistveno olajšajo ustvarjanje in obdelavo spletnih obrazcev. V tem poglavju se boste seznanili z uporabo obrazcev znotraj presenterjev. - -Če vas zanima, kako jih uporabljati popolnoma samostojno brez preostalega ogrodja, je za vas namenjen vodič za [samostojno uporabo|standalone]. - - -Prvi obrazec -============ - -Poskusimo napisati preprost registracijski obrazec. Njegova koda bo naslednja: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Ime:'); -$form->addPassword('password', 'Geslo:'); -$form->addSubmit('send', 'Registriraj'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -in v brskalniku se bo prikazal takole: - -[* form-cs.webp *] - -Obrazec v presenterju je objekt razreda `Nette\Application\UI\Form`, njegov predhodnik `Nette\Forms\Form` je namenjen samostojni uporabi. Dodali smo mu t.i. elemente ime, geslo in gumb za oddajo. In na koncu vrstica z `$form->onSuccess` pove, da se mora po oddaji in uspešni validaciji poklicati metoda `$this->formSucceeded()`. - -Z vidika presenterja je obrazec običajna komponenta. Zato se z njim kot s komponento ravna in ga vključimo v presenter s pomočjo [tovarne metode |application:components#Tovarniške metode]. Izgledalo bo takole: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Ime:'); - $form->addPassword('password', 'Geslo:'); - $form->addSubmit('send', 'Registriraj'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // tukaj obdelamo podatke, poslane z obrazcem - // $data->name vsebuje ime - // $data->password vsebuje geslo - $this->flashMessage('Uspešno ste bili registrirani.'); - $this->redirect('Home:'); - } -} -``` - -In v predlogi obrazec izrišemo z oznako `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Registracija</h1> - -{control registrationForm} -``` - -In to je pravzaprav vse :-) Imamo delujoč in popolnoma [zavarovan |#Zaščita pred ranljivostmi] obrazec. - -In zdaj si verjetno mislite, da je bilo prehitro, razmišljate, kako je mogoče, da se pokliče metoda `formSucceeded()` in kaj so parametri, ki jih prejme. Seveda, imate prav, to si zasluži pojasnilo. - -Nette namreč prihaja s svežim mehanizmom, ki mu pravimo [Hollywood style |application:components#Hollywood style]. Namesto da bi se kot razvijalec morali nenehno spraševati, ali se je nekaj zgodilo („ali je bil obrazec poslan?“, „ali je bil poslan veljavno?“ in „ali ni prišlo do njegovega ponarejanja?“), poveste ogrodju „ko bo obrazec veljavno izpolnjen, pokliči to metodo“ in prepustite nadaljnje delo njemu. Če programirate v JavaScriptu, ta slog programiranja dobro poznate. Pišete funkcije, ki se kličejo, ko nastopi določen [dogodek |nette:glossary#Dogodki eventi]. In jezik jim preda ustrezne argumente. - -Prav tako je zgrajena tudi zgoraj navedena koda presenterja. Polje `$form->onSuccess` predstavlja seznam PHP povratnih klicev (callbackov), ki jih Nette pokliče v trenutku, ko je obrazec poslan in pravilno izpolnjen (tj. je veljaven). V okviru [življenjskega cikla presenterja |application:presenters#Življenjski cikel presenterja] gre za t.i. signal, kličejo se torej po `action*` metodi in pred `render*` metodo. In vsakemu povratnemu klicu preda kot prvi parameter sam obrazec in kot drugega poslane podatke v obliki objekta [ArrayHash |utils:arrays#ArrayHash]. Prvi parameter lahko izpustite, če objekta obrazca ne potrebujete. In drugi parameter zna biti bolj prebrisan, ampak o tem [kasneje |#Mapiranje na razrede]. - -Objekt `$data` vsebuje ključe `name` in `password` s podatki, ki jih je izpolnil uporabnik. Običajno podatke takoj pošljemo k nadaljnji obdelavi, kar je lahko na primer vstavljanje v podatkovno bazo. Med obdelavo pa se lahko pojavi napaka, na primer uporabniško ime je že zasedeno. V takem primeru napako predamo nazaj v obrazec s pomočjo `addError()` in pustimo, da se ponovno izriše, tudi s sporočilom o napaki. - -```php -$form->addError('Oprostite, uporabniško ime že nekdo uporablja.'); -``` - -Poleg `onSuccess` obstaja še `onSubmit`: povratni klici se kličejo vedno po oddaji obrazca, tudi takrat, ko ni pravilno izpolnjen. In dalje `onError`: povratni klici se kličejo le, če oddaja ni veljavna. Pokličejo se celo takrat, če v `onSuccess` ali `onSubmit` znevalidiramo obrazec s pomočjo `addError()`. - -Po obdelavi obrazca preusmerimo na naslednjo stran. S tem se prepreči neželeno ponovno pošiljanje obrazca z gumbom *osveži*, *nazaj* ali premikanjem v zgodovini brskalnika. - -Poskusite dodati tudi druge [elemente obrazca|controls]. - - -Dostop do elementov -=================== - -Obrazec je komponenta presenterja, v našem primeru poimenovana `registrationForm` (po imenu tovarne metode `createComponentRegistrationForm`), tako da kjerkoli v presenterju do obrazca pridete s pomočjo: - -```php -$form = $this->getComponent('registrationForm'); -// alternativna sintaksa: $form = $this['registrationForm']; -``` - -Komponente so tudi posamezni elementi obrazca, zato do njih pridete na enak način: - -```php -$input = $form->getComponent('name'); // ali $input = $form['name']; -$button = $form->getComponent('send'); // ali $button = $form['send']; -``` - -Elementi se odstranijo s pomočjo unset: - -```php -unset($form['name']); -``` - - -Validacijska pravila -==================== - -Padla je beseda *veljaven,* ampak obrazec zaenkrat nima nobenih validacijskih pravil. Popravimo to. - -Ime bo obvezno, zato ga označimo z metodo `setRequired()`, katere argument je besedilo sporočila o napaki, ki se prikaže, če uporabnik imena ne izpolni. Če argumenta ne navedemo, se uporabi privzeto sporočilo o napaki. - -```php -$form->addText('name', 'Ime:') - ->setRequired('Prosimo, vnesite ime'); -``` - -Poskusite poslati obrazec brez izpolnjenega imena in videli boste, da se prikaže sporočilo o napaki in brskalnik ali strežnik ga bo zavračal, dokler polja ne izpolnite. - -Hkrati sistema ne boste prelisičili s tem, da v polje napišete na primer le presledke. Kje pa. Nette leve in desne presledke samodejno odstranjuje. Preizkusite to. To je stvar, ki bi jo morali z vsakim enovrstičnim vnosom vedno narediti, a se nanjo pogosto pozabi. Nette to dela samodejno. (Lahko poskusite prelisičiti obrazec in kot ime poslati večvrstični niz. Tudi tukaj se Nette ne pusti zmesti in prelome vrstic spremeni v presledke.) - -Obrazec se vedno validira na strani strežnika, vendar se generira tudi JavaScript validacija, ki poteka bliskovito in uporabnik se o napaki seznani takoj, brez potrebe po pošiljanju obrazca na strežnik. Za to skrbi skript `netteForms.js`. Vstavite ga v predlogo postavitve: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Če pogledate v izvorno kodo strani z obrazcem, lahko opazite, da Nette obvezne elemente vstavlja v elemente s CSS razredom `required`. Poskusite dodati v predlogo naslednji slogovni list in oznaka „Ime“ bo rdeča. Elegantno tako uporabnikom označimo obvezne elemente: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Druga validacijska pravila dodamo z metodo `addRule()`. Prvi parameter je pravilo, drugi je spet besedilo sporočila o napaki in lahko še sledi argument validacijskega pravila. Kaj se s tem misli? - -Obrazec razširimo z novim neobveznim poljem „starost“, ki mora biti celo število (`addInteger()`) in poleg tega v dovoljenem obsegu (`$form::Range`). In tukaj prav izkoristimo tretji parameter metode `addRule()`, s katerim validatorju predamo zahtevani obseg kot par `[od, do]`: - -```php -$form->addInteger('age', 'Starost:') - ->addRule($form::Range, 'Starost mora biti od 18 do 120', [18, 120]); -``` - -.[tip] -Če uporabnik polja ne izpolni, se validacijska pravila ne bodo preverjala, saj je element neobvezen. - -Tukaj nastane prostor za drobno preoblikovanje (refactoring). V sporočilu o napaki in v tretjem parametru so števila navedena podvojeno, kar ni idealno. Če bi ustvarjali [večjezične obrazce |rendering#Prevajanje] in bi bilo sporočilo, ki vsebuje števila, prevedeno v več jezikov, bi se otežila morebitna sprememba vrednosti. Iz tega razloga je mogoče uporabiti nadomestne znake `%d` in Nette vrednosti dopolni: - -```php - ->addRule($form::Range, 'Starost mora biti od %d do %d let', [18, 120]); -``` - -Vrnimo se k elementu `password`, ki ga prav tako naredimo obveznega in še preverimo minimalno dolžino gesla (`$form::MinLength`), spet z uporabo nadomestnega znaka: - -```php -$form->addPassword('password', 'Geslo:') - ->setRequired('Izberite si geslo') - ->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8); -``` - -Dodamo v obrazec še polje `passwordVerify`, kjer uporabnik vnese geslo še enkrat, za preverjanje. S pomočjo validacijskih pravil preverimo, ali sta obe gesli enaki (`$form::Equal`). In kot parameter damo sklic na prvo geslo s pomočjo [oglatih oklepajev |#Dostop do elementov]: - -```php -$form->addPassword('passwordVerify', 'Geslo za preverjanje:') - ->setRequired('Prosimo, vnesite geslo še enkrat za preverjanje') - ->addRule($form::Equal, 'Gesli se ne ujemata', $form['password']) - ->setOmitted(); -``` - -S pomočjo `setOmitted()` smo označili element, katerega vrednost nas pravzaprav ne zanima in ki obstaja le zaradi validacije. Vrednost se ne preda v `$data`. - -S tem imamo končan popolnoma delujoč obrazec z validacijo v PHP in JavaScriptu. Validacijske sposobnosti Nette so veliko širše, dajo se ustvarjati pogoji, puščati glede na njih prikazovati in skrivati dele strani itd. Vse se boste naučili v poglavju o [validaciji obrazcev|validation]. - - -Privzete vrednosti -================== - -Elementom obrazca običajno nastavljamo privzete vrednosti: - -```php -$form->addEmail('email', 'E-pošta') - ->setDefaultValue($lastUsedEmail); -``` - -Pogosto je koristno nastaviti privzete vrednosti vsem elementom hkrati. Na primer, ko obrazec služi za urejanje zapisov. Preberemo zapis iz podatkovne baze in nastavimo privzete vrednosti: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Kličite `setDefaults()` šele po definiciji elementov. - - -Izrisovanje obrazca -=================== - -Standardno se obrazec izriše kot tabela. Posamezni elementi izpolnjujejo osnovno pravilo dostopnosti - vse oznake so zapisane kot `<label>` in povezane z ustreznim elementom obrazca. Ob kliku na oznako se kazalec samodejno pojavi v polju obrazca. - -Vsakemu elementu lahko nastavljamo poljubne HTML atribute. Na primer dodati placeholder: - -```php -$form->addInteger('age', 'Starost:') - ->setHtmlAttribute('placeholder', 'Prosimo, izpolnite starost'); -``` - -Načinov, kako izrisati obrazec, je res veliko, zato je temu namenjeno [samostojno poglavje o izrisovanju|rendering]. - - -Mapiranje na razrede -==================== - -Vrnimo se k metodi `formSucceeded()`, ki v drugem parametru `$data` prejme poslane podatke kot objekt `ArrayHash`. Ker gre za generični razred, nekaj kot `stdClass`, nam bo pri delu z njim manjkalo določeno udobje, kot je na primer predlaganje lastnosti v urejevalnikih ali statična analiza kode. To bi lahko rešili tako, da bi za vsak obrazec imeli konkreten razred, katerega lastnosti predstavljajo posamezne elemente. Npr.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativno lahko uporabite konstruktor: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Lastnosti podatkovnega razreda so lahko tudi enumi in pride do njihovega samodejnega mapiranja. .{data-version:3.2.4} - -Kako povedati Nette, naj nam podatke vrača kot objekte tega razreda? Lažje, kot si mislite. Zadostuje le navesti razred kot tip parametra `$data` v obdelovalni metodi: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $name je instanca RegistrationFormData - $name = $data->name; - // ... -} -``` - -Kot tip lahko navedete tudi `array` in potem podatke preda kot polje. - -Podobno lahko uporabljate tudi funkcijo `getValues()`, ki ji ime razreda ali objekt za hidracijo predamo kot parameter: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Če obrazci tvorijo večnivojsko strukturo, sestavljeno iz vsebnikov, ustvarite za vsakega samostojen razred: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Mapiranje nato iz tipa lastnosti `$person` prepozna, da mora vsebnik mapirati na razred `PersonFormData`. Če bi lastnost vsebovala polje vsebnikov, navedite tip `array` in razred za mapiranje predajte neposredno vsebniku: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Načrt podatkovnega razreda obrazca si lahko pustite generirati s pomočjo metode `Nette\Forms\Blueprint::dataClass($form)`, ki ga izpiše na stran brskalnika. Kodo nato zadostuje s klikom označiti in kopirati v projekt. .{data-version:3.1.15} - - -Več gumbov -========== - -Če ima obrazec več kot en gumb, moramo praviloma razlikovati, kateri od njih je bil pritisnjen. Lahko si za vsak gumb ustvarimo lastno obdelovalno funkcijo. Nastavimo jo kot handler za [dogodek |nette:glossary#Dogodki eventi] `onClick`: - -```php -$form->addSubmit('save', 'Shrani') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Izbriši') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Ti handlerji se kličejo le v primeru veljavno izpolnjenega obrazca, enako kot v primeru dogodka `onSuccess`. Razlika je v tem, da se kot prvi parameter namesto obrazca lahko preda gumb za oddajo, odvisno od tipa, ki ga navedete: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Ko se obrazec odda z gumbom <kbd>Enter</kbd>, se šteje, kot da bi bil oddan s prvim gumbom. - - -Dogodek onAnchor -================ - -Ko v tovarni metodi (kot je npr. `createComponentRegistrationForm`) sestavljamo obrazec, ta še ne ve, ali je bil poslan, niti s kakšnimi podatki. So pa primeri, ko poslane vrednosti potrebujemo poznati, na primer se glede na njih odvija nadaljnja podoba obrazca, ali jih potrebujemo za odvisna izbirna polja itd. - -Del kode, ki sestavlja obrazec, lahko zato pustite poklicati šele v trenutku, ko je t.i. zasidran, torej je že povezan s presenterjem in pozna svoje poslane podatke. Takšno kodo predamo v polje `$onAnchor`: - -```php -$country = $form->addSelect('country', 'Država:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Mesto:'); - -$form->onAnchor[] = function () use ($country, $city) { - // ta funkcija se pokliče šele, ko bo obrazec vedel, ali je bil poslan in s kakšnimi podatki - // lahko torej uporabljamo metodo getValue() - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Zaščita pred ranljivostmi -========================= - -Nette Framework daje velik poudarek varnosti in zato skrbno pazi na dobro zavarovanje obrazcev. To počne popolnoma transparentno in ne zahteva ročnega nastavljanja ničesar. - -Poleg tega, da obrazce zaščiti pred napadom [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] in [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], opravlja veliko drobnih zavarovanj, na katere vam ni več treba misliti. - -Tako na primer iz vnosov odfiltrira vse kontrolne znake in preveri veljavnost UTF-8 kodiranja, tako da bodo podatki iz obrazca vedno čisti. Pri izbirnih poljih in seznamih izbirnih gumbov preverja, da so bile izbrane postavke dejansko iz ponujenih in da ni prišlo do ponarejanja. Že smo omenili, da pri enovrstičnih besedilnih vnosih odstranjuje znake konca vrstic, ki bi jih tja lahko poslal napadalec. Pri večvrstičnih vnosih pa normalizira znake za konce vrstic. In tako naprej. - -Nette za vas rešuje varnostna tveganja, za katera veliko programerjev niti ne ve, da obstajajo. - -Omenjeni CSRF napad temelji na tem, da napadalec žrtev zvabi na stran, ki neopazno v brskalniku žrtve izvede zahtevo na strežnik, na katerem je žrtev prijavljena, in strežnik domneva, da je zahtevo izvedla žrtev po svoji volji. Zato Nette preprečuje pošiljanje POST obrazca iz druge domene. Če iz kakršnega koli razloga želite zaščito izklopiti in dovoliti pošiljanje obrazca iz druge domene, uporabite: - -```php -$form->allowCrossOrigin(); // POZOR! Izklopi zaščito! -``` - -Ta zaščita uporablja SameSite piškotek, poimenovan `_nss`. Zaščita s pomočjo SameSite piškotka morda ni 100% zanesljiva, zato je priporočljivo vklopiti še zaščito s pomočjo žetona (token): - -```php -$form->addProtection(); -``` - -Priporočamo, da tako zaščitite obrazce v administrativnem delu spletnega mesta, ki spreminjajo občutljive podatke v aplikaciji. Ogrodje se proti napadu CSRF brani z generiranjem in preverjanjem avtorizacijskega žetona, ki se shranjuje v sejo. Zato je treba pred prikazom obrazca imeti odprto sejo. V administrativnem delu spletnega mesta je običajno seja že zagnana zaradi prijave uporabnika. Sicer sejo zaženite z metodo `Nette\Http\Session::start()`. - - -Enak obrazec v več presenterjih -=============================== - -Če potrebujete en obrazec uporabiti v več presenterjih, priporočamo, da si zanj ustvarite tovarno, ki si jo nato predajte v presenter. Primerna lokacija za tak razred je npr. imenik `app/Forms`. - -Tovarniški razred lahko izgleda na primer takole: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Ime:'); - $form->addSubmit('send', 'Prijavi se'); - return $form; - } -} -``` - -Razred prosimo za izdelavo obrazca v tovarni metodi za komponente v presenterju: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // lahko obrazec spremenimo, tukaj na primer spreminjamo napis na gumbu - $form['send']->setCaption('Nadaljuj'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // in dodamo handler - return $form; -} -``` - -Handler za obdelavo obrazca je lahko tudi že dodan iz tovarne: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Ime:'); - $form->addSubmit('send', 'Prijavi se'); - $form->onSuccess[] = function (Form $form, $data): void { - // tukaj izvedemo obdelavo obrazca - }; - return $form; - } -} -``` - -Tako, za nami je hiter uvod v obrazce v Nette. Poskusite še pogledati v imenik [examples|https://github.com/nette/forms/tree/master/examples] v distribuciji, kjer najdete dodatno inspiracijo. diff --git a/forms/sl/rendering.texy b/forms/sl/rendering.texy deleted file mode 100644 index e12a057593..0000000000 --- a/forms/sl/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Izrisovanje obrazcev -******************** - -Videz obrazcev je lahko zelo raznolik. V praksi lahko naletimo na dva ekstrema. Na eni strani stoji potreba po izrisovanju številnih obrazcev v aplikaciji, ki so si vizualno podobni kot jajce jajcu, in cenimo enostavno izrisovanje brez predloge s pomočjo `$form->render()`. Gre običajno za primer administrativnih vmesnikov. - -Na drugi strani pa so raznoliki obrazci, kjer velja: vsak kos je original. Njihovo podobo najbolje opišemo z jezikom HTML v predlogi obrazca. In seveda poleg obeh omenjenih ekstremov naletimo na veliko obrazcev, ki se gibljejo nekje vmes. - - -Izrisovanje s pomočjo Latte -=========================== - -[Sistem predlog Latte|latte:] bistveno olajša izrisovanje obrazcev in njihovih elementov. Najprej si bomo pokazali, kako obrazce izrisovati ročno po posameznih elementih in s tem pridobiti popoln nadzor nad kodo. Kasneje si bomo pokazali, kako je mogoče takšno izrisovanje [avtomatizirati |#Samodejno izrisovanje]. - -Načrt Latte predloge obrazca si lahko pustite generirati s pomočjo metode `Nette\Forms\Blueprint::latte($form)`, ki ga izpiše na stran brskalnika. Kodo nato zadostuje s klikom označiti in kopirati v projekt. .{data-version:3.1.15} - - -`{control}` ------------ - -Najenostavnejši način, kako izrisati obrazec, je napisati v predlogi: - -```latte -{control signInForm} -``` - -Vplivati na podobo tako izrisanega obrazca je mogoče s konfiguracijo [Rendererja |#Renderer] in [posameznih elementov |#HTML atributi]. - - -`n:name` --------- - -Definicijo obrazca v PHP kodi je mogoče izjemno enostavno povezati s HTML kodo. Zadostuje le dopolniti atribute `n:name`. Tako je enostavno! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Podobo končne HTML kode imate popolnoma v svojih rokah. Če atribut `n:name` uporabite pri elementih `<select>`, `<button>` ali `<textarea>`, se njihova notranja vsebina samodejno dopolni. Oznaka `<form n:name>` poleg tega ustvari lokalno spremenljivko `$form` z objektom risanega obrazca in zaključna `</form>` izriše vse neizrisane skrite elemente (enako velja tudi za `{form} ... {/form}`). - -Ne smemo pa pozabiti na izrisovanje morebitnih sporočil o napakah. In to tako tistih, ki so se z metodo `addError()` dodala k posameznim elementom (s pomočjo `{inputError}`), kot tudi tistih, dodanih neposredno k obrazcu (vrača jih `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Bolj zapletene elemente obrazca, kot sta RadioList ali CheckboxList, je mogoče tako izrisovati po posameznih postavkah: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Ne želite pri vsakem elementu razmišljati, kateri HTML element zanj uporabiti v predlogi, ali `<input>`, `<textarea>` itd.? Rešitev je univerzalna oznaka `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Password: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Če obrazec uporablja prevajalnik|, bo besedilo znotraj oznak `{label}` prevedeno. - -Tudi v tem primeru je mogoče bolj zapletene elemente obrazca, kot sta RadioList ali CheckboxList, izrisovati po posameznih postavkah: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Za izrisovanje samega `<input>` v elementu Checkbox uporabite `{input myCheckbox:}`. HTML atribute v tem primeru vedno ločujte z vejico `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Izpiše sporočilo o napaki k elementu obrazca, če ga ima. Sporočilo običajno zavijemo v HTML element zaradi stiliranja. Preprečiti izrisovanje praznega elementa, če sporočila ni, je mogoče elegantno s pomočjo `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -Prisotnost napake lahko ugotovimo z metodo `hasErrors()` in glede na to nastavimo razred nadrejenemu elementu: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -Oznake `{form signInForm}...{/form}` so alternativa k `<form n:name="signInForm">...</form>`. - - -Samodejno izrisovanje ---------------------- - -Zahvaljujoč oznakam `{input}` in `{label}` lahko enostavno ustvarimo splošno predlogo za kateri koli obrazec. Postopoma bo iterirala in izrisovala vse njegove elemente, razen skritih elementov, ki se izrišejo samodejno ob zaključku obrazca z oznako `</form>`. Ime izrisovanega obrazca bo pričakovala v spremenljivki `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -Uporabljene samozaključujoče parne oznake `{label .../}` prikazujejo oznake, ki izhajajo iz definicije obrazca v PHP kodi. - -To splošno predlogo shranite na primer v datoteko `basic-form.latte` in za izrisovanje obrazca jo zadostuje vključiti ter predati ime (ali instanco) obrazca v parameter `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Če bi pri izrisovanju enega določenega obrazca želeli poseči v njegovo podobo in na primer en element izrisati drugače, potem je najenostavnejša pot, da si v predlogi predpripravite bloke, ki jih bo mogoče kasneje prepisati. Bloki imajo lahko tudi [dinamična imena |latte:template-inheritance#Dinamična imena blokov], vanje je tako mogoče vstaviti tudi ime izrisovanega elementa. Na primer: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Za element npr. `username` tako nastane blok `input-username`, ki ga je mogoče enostavno prepisati z uporabo oznake [{embed} |latte:template-inheritance#Dedovanje enot]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Alternativno je mogoče celotno vsebino predloge `basic-form.latte` [definirati |latte:template-inheritance#Definicije] kot blok, vključno s parametrom `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Zahvaljujoč temu bo njegov klic nekoliko enostavnejši: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -Blok pri tem zadostuje uvoziti na enem samem mestu in to na začetku predloge postavitve: - -```latte -{import basic-form.latte} -``` - - -Posebni primeri ---------------- - -Če potrebujete izrisati le notranji del obrazca brez HTML oznak `<form>`, na primer pri pošiljanju odrezkov (snippetov), jih skrijte s pomočjo atributa `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Username: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -Z izrisovanjem elementov znotraj vsebnika obrazca pomaga oznaka `{formContainer}`. - -```latte -<p>Katere novice želite prejemati:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Izrisovanje brez Latte -====================== - -Najenostavnejši način, kako izrisati obrazec, je poklicati: - -```php -$form->render(); -``` - -Vplivati na podobo tako izrisanega obrazca je mogoče s konfiguracijo [Rendererja |#Renderer] in [posameznih elementov |#HTML atributi]. - - -Ročno izrisovanje ------------------ - -Vsak element obrazca ima metode, ki generirajo HTML kodo polja obrazca in oznake. Lahko jo vračajo bodisi kot niz ali objekt [Nette\Utils\Html|utils:html-elements]: - -- `getControl(): Html|string` vrne HTML kodo elementa -- `getLabel($caption = null): Html|string|null` vrne HTML kodo oznake, če obstaja - -Obrazec je tako mogoče izrisovati po posameznih elementih: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -Medtem ko pri nekaterih elementih `getControl()` vrne en sam HTML element (npr. `<input>`, `<select>` ipd.), pri drugih cel kos HTML kode (CheckboxList, RadioList). V takem primeru lahko uporabite metode, ki generirajo posamezne vnose in oznake, za vsako postavko posebej: - -- `getControlPart($key = null): ?Html` vrne HTML kodo ene postavke -- `getLabelPart($key = null): ?Html` vrne HTML kodo oznake ene postavke - -.[note] -Te metode imajo iz zgodovinskih razlogov predpono `get`, vendar bi bil boljši `generate`, ker pri vsakem klicu ustvari in vrne nov element `Html`. - - -Renderer -======== - -Gre za objekt, ki zagotavlja izrisovanje obrazca. Tega je mogoče nastaviti z metodo `$form->setRenderer`. Njemu se preda nadzor ob klicu metode `$form->render()`. - -Če ne nastavimo lastnega rendererja, bo uporabljen privzeti izrisovalnik [api:Nette\Forms\Rendering\DefaultFormRenderer]. Ta elemente obrazca izriše v obliki HTML tabele. Izhod izgleda takole: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Ime:</label></th> - - <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">Starost:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Spol:</label></th> - ... -``` - -Ali uporabiti ali ne uporabiti za ogrodje obrazca tabelo je sporno in vrsta spletnih oblikovalcev preferira drugačen markup. Na primer definicijski seznam. Prekonfiguriramo zato `DefaultFormRenderer` tako, da obrazec izriše v obliki seznama. Konfiguracija se izvaja z urejanjem polja [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Prvi indeks vedno predstavlja območje in drugi njegov atribut. Posamezna območja ponazarja slika: - -[* defaultformrenderer.webp *] - -Standardno je skupina elementov `controls` ovita s tabelo `<table>`, vsak `pair` predstavlja vrstico tabele `<tr>` in par `label` ter `control` sta celici `<th>` in `<td>`. Zdaj ovojne elemente spremenimo. Območje `controls` vstavimo v vsebnik `<dl>`, območje `pair` pustimo brez vsebnika, `label` vstavimo v `<dt>` in na koncu `control` ovijemo z oznakami `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Rezultat je ta HTML koda: - -```latte -<dl> - <dt><label class="required" for="frm-name">Ime:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Starost:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Spol:</label></dt> - ... -</dl> -``` - -V polju wrappers je mogoče vplivati na celo vrsto drugih atributov: - -- dodajati CSS razrede posameznim tipom elementov obrazca -- razlikovati CSS razred lihih in sodih vrstic -- vizualno ločiti obvezne in neobvezne postavke -- določati, ali se sporočila o napakah prikažejo neposredno pri elementih ali nad obrazcem - - -Možnosti (Options) ------------------- - -Obnašanje Rendererja je mogoče nadzorovati tudi z nastavljanjem *options* na posameznih elementih obrazca. Tako je mogoče nastaviti napis, ki se izpiše poleg vnosnega polja: - -```php -$form->addText('phone', 'Številka:') - ->setOption('description', 'Ta številka bo ostala skrita'); -``` - -Če vanj želimo umestiti HTML vsebino, uporabimo razred [Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Številka:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Pogoji shranjevanja Vaše številke</a>') - ); -``` - -.[tip] -Html element je mogoče uporabiti tudi namesto oznake: `$form->addCheckbox('conditions', $label)`. - - -Združevanje elementov ---------------------- - -Renderer omogoča združevanje elementov v vizualne skupine (fieldsete): - -```php -$form->addGroup('Osebni podatki'); -``` - -Po ustvarjanju nove skupine ta postane aktivna in vsak na novo dodan element je hkrati dodan tudi vanjo. Tako je mogoče obrazec graditi na ta način: - -```php -$form = new Form; -$form->addGroup('Osebni podatki'); -$form->addText('name', 'Vaše ime:'); -$form->addInteger('age', 'Vaša starost:'); -$form->addEmail('email', 'E-pošta:'); - -$form->addGroup('Naslov za dostavo'); -$form->addCheckbox('send', 'Pošlji na naslov'); -$form->addText('street', 'Ulica:'); -$form->addText('city', 'Mesto:'); -$form->addSelect('country', 'Država:', $countries); -``` - -Renderer najprej izrisuje skupine in šele nato elemente, ki ne pripadajo nobeni skupini. - - -Podpora za Bootstrap --------------------- - -[V primerih |https://github.com/nette/forms/tree/master/examples] najdete primere, kako konfigurirati Renderer za [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] in [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -HTML atributi -============= - -Za nastavitev poljubnih HTML atributov elementov obrazca uporabimo metodo `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Število:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Razvrsti po:', ['ceni', 'nazivu']) - ->setHtmlAttribute('onchange', 'submit()'); // ob spremembi pošlji - - -// Za nastavitev atributov samega <form> -$form->setHtmlAttribute('id', 'myForm'); -``` - -Specifikacija tipa elementa: - -```php -$form->addText('tel', 'Vaš telefon:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'napišite telefon'); -``` - -.[warning] -Nastavitev tipa in drugih atributov služi le za vizualne namene. Preverjanje pravilnosti vnosov mora potekati na strežniku, kar zagotovite z izbiro ustreznega [elementa obrazca|controls] in navedbo [validacijskih pravil|validation]. - -Posameznim postavkam v seznamih izbirnih gumbov ali potrditvenih polj lahko nastavimo HTML atribut z različnimi vrednostmi za vsako od njih. Opazite dvopičje za `style:`, ki zagotovi izbiro vrednosti glede na ključ: - -```php -$colors = ['r' => 'rdeča', 'g' => 'zelena', 'b' => 'modra']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Barve:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Izpiše: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">rdeča</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">zelena</label> -<label><input type="checkbox" name="colors[]" value="b">modra</label> -``` - -Za nastavitev logičnih atributov, kot je `readonly`, lahko uporabimo zapis z vprašajem: - -```php -$form->addCheckboxList('colors', 'Barve:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // za več ključev uporabite polje, npr. ['r', 'g'] -``` - -Izpiše: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">rdeča</label> -<label><input type="checkbox" name="colors[]" value="g">zelena</label> -<label><input type="checkbox" name="colors[]" value="b">modra</label> -``` - -V primeru izbirnih polj (selectbox) metoda `setHtmlAttribute()` nastavlja atribute elementa `<select>`. Če želimo nastaviti atribute posameznim `<option>`, uporabimo metodo `setOptionAttribute()`. Delujejo tudi zapisi z dvopičjem in vprašajem, navedeni zgoraj: - -```php -$form->addSelect('colors', 'Barve:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Izpiše: - -```latte -<select name="colors"> - <option value="r" style="background:red">rdeča</option> - <option value="g" style="background:green">zelena</option> - <option value="b">modra</option> -</select> -``` - - -Prototipi ---------- - -Alternativni način nastavljanja HTML atributov temelji na urejanju predloge, iz katere se HTML element generira. Predloga je objekt `Html` in jo vrača metoda `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Število:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -Na ta način je mogoče modificirati tudi predlogo oznake, ki jo vrača `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -Pri elementih Checkbox, CheckboxList in RadioList lahko vplivate na predlogo elementa, ki celoten element ovija. Vrača jo `getContainerPrototype()`. V privzetem stanju gre za „prazen“ element, tako da se nič ne izrisuje, a s tem, da mu nastavimo ime, se bo izrisoval: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -V primeru CheckboxList in RadioList lahko vplivate tudi na predlogo ločila posameznih postavk, ki ga vrača metoda `getSeparatorPrototype()`. V privzetem stanju je to element `<br>`. Če ga spremenite v parni element, bo posamezne postavke ovijal namesto ločeval. In dalje lahko vplivate na predlogo HTML elementa oznake pri posameznih postavkah, ki ga vrača `getItemLabelPrototype()`. - - -Prevajanje -========== - -Če programirate večjezično aplikacijo, boste verjetno potrebovali obrazec izrisati v različnih jezikovnih mutacijah. Nette Framework za ta namen definira vmesnik za prevajanje [api:Nette\Localization\Translator]. V Nette ni nobene privzete implementacije, lahko si izberete glede na svoje potrebe iz več pripravljenih rešitev, ki jih najdete na [Componette |https://componette.org/search/localization]. V njihovi dokumentaciji boste izvedeli, kako prevajalnik konfigurirati. - -Obrazci podpirajo izpisovanje besedil preko prevajalnika. Predamo jim ga s pomočjo metode `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -Od te točke naprej se ne le vse oznake, ampak tudi vsa sporočila o napakah ali postavke izbirnih polj prevedejo v drug jezik. - -Pri posameznih elementih obrazca je pri tem mogoče nastaviti drug prevajalnik ali prevajanje popolnoma izklopiti z vrednostjo `null`: - -```php -$form->addSelect('carModel', 'Model:', $cars) - ->setTranslator(null); -``` - -Pri [validacijskih pravilih|validation] se prevajalniku predajajo tudi specifični parametri, na primer pri pravilu: - -```php -$form->addPassword('password', 'Geslo:') - ->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8); -``` - -se kliče prevajalnik s temi parametri: - -```php -$translator->translate('Geslo mora imeti vsaj %d znakov', 8); -``` - -in torej lahko izbere pravilno obliko množine pri besedi `znakov` glede na število. - - -Dogodek onRender -================ - -Tik preden se obrazec izriše, lahko pustimo poklicati našo kodo. Ta lahko na primer dopolni elementom obrazca HTML razrede za pravilno prikazovanje. Kodo dodamo v polje `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/sl/standalone.texy b/forms/sl/standalone.texy deleted file mode 100644 index a242f18279..0000000000 --- a/forms/sl/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Obrazci, uporabljeni samostojno -******************************* - -.[perex] -Nette Forms bistveno olajšajo ustvarjanje in obdelavo spletnih obrazcev. Uporabljate jih lahko v svojih aplikacijah popolnoma samostojno brez preostalega ogrodja, kar bomo pokazali v tem poglavju. - -Če pa uporabljate Nette Application in presenterje, je za vas namenjen vodnik za [uporabo v presenterjih|in-presenter]. - - -Prvi obrazec -============ - -Poskusimo napisati preprost obrazec za registracijo. Njegova koda bo naslednja ("celotna koda":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Ime:'); -$form->addPassword('password', 'Geslo:'); -$form->addSubmit('send', 'Registriraj'); -``` - -Zelo enostavno ga izrišemo: - -```php -$form->render(); -``` - -in v brskalniku se prikaže takole: - -[* form-cs.webp *] - -Obrazec je objekt razreda `Nette\Forms\Form` (razred `Nette\Application\UI\Form` se uporablja v presenterjih). Dodali smo mu t.i. elemente ime, geslo in gumb za pošiljanje. - -In zdaj obrazec oživimo. Z vprašanjem `$form->isSuccess()` ugotovimo, ali je bil obrazec poslan in ali je bil veljavno izpolnjen. Če je, podatke izpišemo. Za definicijo obrazca torej dodamo: - -```php -if ($form->isSuccess()) { - echo 'Obrazec je bil pravilno izpolnjen in poslan'; - $data = $form->getValues(); - // $data->name vsebuje ime - // $data->password vsebuje geslo - var_dump($data); -} -``` - -Metoda `getValues()` vrača poslane podatke v obliki objekta [ArrayHash |utils:arrays#ArrayHash]. Kako to spremeniti, si bomo ogledali [kasneje |#Preslikava v razrede]. Objekt `$data` vsebuje ključa `name` in `password` s podatki, ki jih je izpolnil uporabnik. - -Običajno podatke takoj pošljemo v nadaljnjo obdelavo, kar je lahko na primer vstavljanje v bazo podatkov. Med obdelavo pa se lahko pojavi napaka, na primer uporabniško ime je že zasedeno. V takem primeru napako vrnemo nazaj v obrazec z `addError()` in ga pustimo ponovno izrisati, tudi s sporočilom o napaki. - -```php -$form->addError('Oprostite, to uporabniško ime že nekdo uporablja.'); -``` - -Po obdelavi obrazca preusmerimo na naslednjo stran. S tem preprečimo neželeno ponovno pošiljanje obrazca z gumbom *obnovi*, *nazaj* ali premikanjem v zgodovini brskalnika. - -Obrazec se standardno pošilja z metodo POST in to na isto stran. Oboje se da spremeniti: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -In to je pravzaprav vse :-) Imamo delujoč in popolnoma [zaščiten |#Zaščita pred ranljivostmi] obrazec. - -Poskusite dodati tudi druge [elemente obrazca|controls]. - - -Dostop do elementov -=================== - -Obrazec in njegove posamezne elemente imenujemo komponente. Tvorijo drevo komponent, kjer je koren prav obrazec. Do posameznih elementov obrazca dostopamo na ta način: - -```php -$input = $form->getComponent('name'); -// alternativna sintaksa: $input = $form['name']; - -$button = $form->getComponent('send'); -// alternativna sintaksa: $button = $form['send']; -``` - -Elementi se odstranijo z unset: - -```php -unset($form['name']); -``` - - -Validacijska pravila -==================== - -Omenili smo besedo *veljaven,* vendar obrazec zaenkrat nima nobenih validacijskih pravil. Popravimo to. - -Ime bo obvezno, zato ga označimo z metodo `setRequired()`, katere argument je besedilo sporočila o napaki, ki se prikaže, če uporabnik imena ne izpolni. Če argumenta ne navedemo, se uporabi privzeto sporočilo o napaki. - -```php -$form->addText('name', 'Ime:') - ->setRequired('Prosimo, vnesite ime'); -``` - -Poskusite poslati obrazec brez izpolnjenega imena in videli boste, da se prikaže sporočilo o napaki in brskalnik ali strežnik ga bo zavračal, dokler polja ne izpolnite. - -Hkrati sistema ne boste prelisičili s tem, da v polje vpišete samo presledke. Kje pa. Nette samodejno odstranjuje leve in desne presledke. Preizkusite. To je stvar, ki bi jo morali vedno narediti z vsakim enovrstičnim vnosom, vendar se nanjo pogosto pozablja. Nette to naredi samodejno. (Lahko poskusite prelisičiti obrazec in kot ime poslati večvrstični niz. Tudi tu se Nette ne pusti zmesti in prelome vrstic spremeni v presledke.) - -Obrazec se vedno validira na strani strežnika, vendar se generira tudi JavaScript validacija, ki poteka bliskovito in uporabnik se o napaki takoj obvesti, brez potrebe po pošiljanju obrazca na strežnik. Za to skrbi skript `netteForms.js`. Vstavite ga na stran: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Če pogledate izvorno kodo strani z obrazcem, lahko opazite, da Nette obvezne elemente vstavlja v elemente s CSS razredom `required`. Poskusite dodati v predlogo naslednji slog in oznaka "Ime" bo rdeča. Tako elegantno uporabnikom označimo obvezne elemente: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Druga validacijska pravila dodamo z metodo `addRule()`. Prvi parameter je pravilo, drugi je spet besedilo sporočila o napaki in lahko še sledi argument validacijskega pravila. Kaj to pomeni? - -Obrazec razširimo z novim neobveznim poljem "starost", ki mora biti celo število (`addInteger()`) in poleg tega v dovoljenem obsegu (`$form::Range`). In tu prav izkoristimo tretji parameter metode `addRule()`, s katerim validatorju predamo zahtevani obseg kot par `[od, do]`: - -```php -$form->addInteger('age', 'Starost:') - ->addRule($form::Range, 'Starost mora biti od 18 do 120', [18, 120]); -``` - -.[tip] -Če uporabnik polja ne izpolni, se validacijska pravila ne bodo preverjala, saj je element neobvezen. - -Tu nastane prostor za drobno preoblikovanje (refactoring). V sporočilu o napaki in v tretjem parametru so števila navedena dvakrat, kar ni idealno. Če bi ustvarjali [večjezične obrazce |rendering#Prevajanje] in bi bilo sporočilo, ki vsebuje števila, prevedeno v več jezikov, bi se morebitna sprememba vrednosti otežila. Zato je mogoče uporabiti nadomestne znake `%d` in Nette vrednosti dopolni: - -```php - ->addRule($form::Range, 'Starost mora biti od %d do %d let', [18, 120]); -``` - -Vrnimo se k elementu `password`, ki ga prav tako naredimo obveznega in še preverimo minimalno dolžino gesla (`$form::MinLength`), spet z uporabo nadomestnega znaka: - -```php -$form->addPassword('password', 'Geslo:') - ->setRequired('Izberite si geslo') - ->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8); -``` - -Dodamo v obrazec še polje `passwordVerify`, kjer uporabnik vnese geslo še enkrat, za kontrolo. S pomočjo validacijskih pravil preverimo, ali sta obe gesli enaki (`$form::Equal`). In kot parameter damo sklic na prvo geslo z uporabo [oglatimi oklepaji |#Dostop do elementov]: - -```php -$form->addPassword('passwordVerify', 'Geslo za kontrolo:') - ->setRequired('Prosimo, vnesite geslo še enkrat za kontrolo') - ->addRule($form::Equal, 'Gesli se ne ujemata', $form['password']) - ->setOmitted(); -``` - -Z `setOmitted()` smo označili element, katerega vrednost nas pravzaprav ne zanima in ki obstaja le zaradi validacije. Vrednost se ne preda v `$data`. - -S tem imamo končan popolnoma delujoč obrazec z validacijo v PHP in JavaScriptu. Validacijske zmožnosti Nette so veliko širše, dajo se ustvarjati pogoji, puščati po njih prikazovati in skrivati dele strani itd. Vse boste izvedeli v poglavju o [validaciji obrazcev|validation]. - - -Privzete vrednosti -================== - -Elementom obrazca običajno nastavimo privzete vrednosti: - -```php -$form->addEmail('email', 'E-pošta') - ->setDefaultValue($lastUsedEmail); -``` - -Pogosto je koristno nastaviti privzete vrednosti vsem elementom hkrati. Na primer, ko obrazec služi za urejanje zapisov. Preberemo zapis iz baze podatkov in nastavimo privzete vrednosti: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Pokličite `setDefaults()` šele po definiciji elementov. - - -Izris obrazca -============= - -Standardno se obrazec izriše kot tabela. Posamezni elementi izpolnjujejo osnovno pravilo dostopnosti - vse oznake so zapisane kot `<label>` in povezane z ustreznim elementom obrazca. Pri kliku na oznako se kazalec samodejno pojavi v polju obrazca. - -Vsakemu elementu lahko nastavimo poljubne HTML atribute. Na primer, dodamo placeholder: - -```php -$form->addInteger('age', 'Starost:') - ->setHtmlAttribute('placeholder', 'Prosimo, izpolnite starost'); -``` - -Načinov, kako izrisati obrazec, je res veliko, zato je temu namenjeno [ločeno poglavje o izrisu|rendering]. - - -Preslikava v razrede -==================== - -Vrnimo se k obdelavi podatkov obrazca. Metoda `getValues()` nam je vračala poslane podatke kot objekt `ArrayHash`. Ker gre za generični razred, nekaj kot `stdClass`, nam bo pri delu z njim manjkalo določeno udobje, kot je na primer predlaganje lastnosti v urejevalnikih ali statična analiza kode. To bi lahko rešili tako, da bi za vsak obrazec imeli konkreten razred, katerega lastnosti predstavljajo posamezne elemente. Npr.: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Alternativno lahko uporabite konstruktor: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public int $age, - public string $password, - ) { - } -} -``` - -Lastnosti podatkovnega razreda so lahko tudi enumi in pride do njihove samodejne preslikave. .{data-version:3.2.4} - -Kako Nette sporočiti, naj nam podatke vrača kot objekte tega razreda? Lažje, kot si mislite. Dovolj je le ime razreda ali objekt za hidracijo navesti kot parameter: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Kot parameter lahko navedete tudi `'array'` in potem podatke vrne kot polje. - -Če obrazci tvorijo večnivojsko strukturo, sestavljeno iz vsebnikov, ustvarite za vsakega ločen razred: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Preslikava nato iz tipa lastnosti `$person` prepozna, da mora vsebnik preslikati v razred `PersonFormData`. Če bi lastnost vsebovala polje vsebnikov, navedite tip `array` in razred za preslikavo predajte neposredno vsebniku: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Načrt podatkovnega razreda obrazca si lahko pustite generirati s pomočjo metode `Nette\Forms\Blueprint::dataClass($form)`, ki ga izpiše na stran brskalnika. Kodo nato samo kliknite, označite in kopirajte v projekt. .{data-version:3.1.15} - - -Več gumbov -========== - -Če ima obrazec več kot en gumb, moramo praviloma razlikovati, kateri od njih je bil pritisnjen. To informacijo nam vrne metoda `isSubmittedBy()` gumba: - -```php -$form->addSubmit('save', 'Shrani'); -$form->addSubmit('delete', 'Izbriši'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Vprašanja `$form->isSuccess()` ne izpustite, s tem preverite veljavnost podatkov. - -Ko se obrazec pošlje z gumbom <kbd>Enter</kbd>, se šteje, kot da je bil poslan s prvim gumbom. - - -Zaščita pred ranljivostmi -========================= - -Nette Framework daje velik poudarek varnosti in zato skrbno pazi na dobro zaščito obrazcev. - -Poleg tega, da obrazce zaščiti pred napadom [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] in [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], izvaja veliko drobnih zaščit, na katere vam ni treba več misliti. - -Tako na primer iz vhodov filtrira vse kontrolne znake in preveri veljavnost UTF-8 kodiranja, tako da bodo podatki iz obrazca vedno čisti. Pri izbirnih poljih in seznamih radijskih gumbov preverja, ali so bili izbrani elementi resnično iz ponujenih in ni prišlo do ponarejanja. Že smo omenili, da pri enovrstičnih besedilnih vnosih odstranjuje znake konca vrstice, ki jih je tja lahko poslal napadalec. Pri večvrstičnih vnosih pa normalizira znake za konce vrstic. In tako naprej. - -Nette za vas rešuje varnostna tveganja, za katera veliko programerjev sploh ne ve, da obstajajo. - -Omenjeni napad CSRF temelji na tem, da napadalec žrtev zvabi na stran, ki neopazno v brskalniku žrtve izvede zahtevo na strežnik, na katerem je žrtev prijavljena, in strežnik domneva, da je zahtevo izvedla žrtev po svoji volji. Zato Nette preprečuje pošiljanje obrazca POST iz druge domene. Če iz kakršnega koli razloga želite zaščito izklopiti in dovoliti pošiljanje obrazca iz druge domene, uporabite: - -```php -$form->allowCrossOrigin(); // POZOR! Izklopi zaščito! -``` - -Ta zaščita uporablja SameSite piškotek z imenom `_nss`. Zato ustvarite objekt obrazca še pred pošiljanjem prvega izpisa, da bo mogoče piškotek poslati. - -Zaščita s pomočjo SameSite piškotka morda ni 100% zanesljiva, zato je priporočljivo vklopiti še zaščito s pomočjo žetona: - -```php -$form->addProtection(); -``` - -Priporočamo, da tako zaščitite obrazce v administrativnem delu spletnega mesta, ki spreminjajo občutljive podatke v aplikaciji. Ogrodje se proti napadu CSRF brani z generiranjem in preverjanjem avtorizacijskega žetona, ki se shranjuje v sejo. Zato je treba pred prikazom obrazca imeti odprto sejo. V administrativnem delu spletnega mesta je običajno seja že zagnana zaradi prijave uporabnika. Sicer sejo zaženite z metodo `Nette\Http\Session::start()`. - -Tako, za nami je hiter uvod v obrazce v Nette. Poskusite si še pogledati v imenik [examples|https://github.com/nette/forms/tree/master/examples] v distribuciji, kjer boste našli dodatno inspiracijo. diff --git a/forms/sl/validation.texy b/forms/sl/validation.texy deleted file mode 100644 index da4379ff7c..0000000000 --- a/forms/sl/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Validacija obrazcev -******************* - - -Obvezni elementi -================ - -Obvezne elemente označimo z metodo `setRequired()`, katere argument je besedilo [#sporočila o napakah], ki se prikaže, če uporabnik elementa ne izpolni. Če argumenta ne navedemo, se uporabi privzeto sporočilo o napaki. - -```php -$form->addText('name', 'Ime:') - ->setRequired('Prosimo, vnesite ime'); -``` - - -Pravila -======= - -Validacijska pravila dodajamo elementom z metodo `addRule()`. Prvi parameter je pravilo, drugi je besedilo [#sporočila o napakah] in tretji je argument validacijskega pravila. - -```php -$form->addPassword('password', 'Geslo:') - ->addRule($form::MinLength, 'Geslo mora imeti vsaj %d znakov', 8); -``` - -**Validacijska pravila se preverjajo samo v primeru, da je uporabnik element izpolnil.** - -Nette prihaja s celo vrsto preddefiniranih pravil, katerih imena so konstante razreda `Nette\Forms\Form`. Pri vseh elementih lahko uporabimo ta pravila: - -| konstanta | opis | tip argumenta -|------- -| `Required` | obvezen element, alias za `setRequired()` | - -| `Filled` | obvezen element, alias za `setRequired()` | - -| `Blank` | element ne sme biti izpolnjen | - -| `Equal` | vrednost je enaka parametru | `mixed` -| `NotEqual` | vrednost ni enaka parametru | `mixed` -| `IsIn` | vrednost je enaka nekateremu elementu v polju | `array` -| `IsNotIn` | vrednost ni enaka nobenemu elementu v polju | `array` -| `Valid` | je element pravilno izpolnjen? (za [pogoje |#Pogoji]) | - - - -Besedilni vnosi ---------------- - -Pri elementih `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` lahko uporabimo tudi nekatera naslednja pravila: - -| `MinLength` | minimalna dolžina besedila | `int` -| `MaxLength` | maksimalna dolžina besedila | `int` -| `Length` | dolžina v obsegu ali natančna dolžina | par `[int, int]` ali `int` -| `Email` | veljaven e-poštni naslov | - -| `URL` | absolutni URL | - -| `Pattern` | ustreza regularnemu izrazu | `string` -| `PatternInsensitive` | kot `Pattern`, vendar neodvisno od velikosti črk | `string` -| `Integer` | celoštevilska vrednost | - -| `Numeric` | alias za `Integer` | - -| `Float` | število | - -| `Min` | minimalna vrednost numeričnega elementa | `int\|float` -| `Max` | maksimalna vrednost numeričnega elementa | `int\|float` -| `Range` | vrednost v obsegu | par `[int\|float, int\|float]` - -Validacijska pravila `Integer`, `Numeric` in `Float` takoj pretvorijo vrednost v integer oz. float. In nadalje pravilo `URL` sprejme tudi naslov brez sheme (npr. `nette.org`) in shemo dopolni (`https://nette.org`). Izraz v `Pattern` in `PatternIcase` mora veljati za celotno vrednost, tj. kot da bi bil obdan z znakoma `^` in `$`. - - -Število elementov ------------------ - -Pri elementih `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` lahko uporabimo tudi naslednja pravila za omejitev števila izbranih elementov oz. naloženih datotek: - -| `MinLength` | minimalno število | `int` -| `MaxLength` | maksimalno število | `int` -| `Length` | število v obsegu ali natančno število | par `[int, int]` ali `int` - - -Nalaganje datotek ------------------ - -Pri elementih `addUpload()`, `addMultiUpload()` lahko uporabimo tudi naslednja pravila: - -| `MaxFileSize` | maksimalna velikost datoteke v bajtih | `int` -| `MimeType` | MIME tip, dovoljeni nadomestni znaki (`'video/*'`) | `string\|string[]` -| `Image` | slika JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | ime datoteke ustreza regularnemu izrazu | `string` -| `PatternInsensitive` | kot `Pattern`, vendar neodvisno od velikosti črk | `string` - -`MimeType` in `Image` zahtevata PHP razširitev `fileinfo`. Da je datoteka ali slika zahtevanega tipa, zaznajo na podlagi njene signature in **ne preverjajo integritete celotne datoteke.** Ali slika ni poškodovana, lahko ugotovite na primer s poskusom njenega [nalaganjem |http:request#toImage]. - - -Sporočila o napakah -=================== - -Vsa preddefinirana pravila z izjemo `Pattern` in `PatternInsensitive` imajo privzeto sporočilo o napaki, zato ga lahko izpustite. Vendar z navedbo in oblikovanjem vseh sporočil po meri naredite obrazec uporabniku prijaznejši. - -Spremeniti privzeta sporočila lahko v [konfiguraciji|forms:configuration], s prilagoditvijo besedil v polju `Nette\Forms\Validator::$messages` ali z uporabo [prevajalniku |rendering#Prevajanje]. - -V besedilu sporočil o napakah lahko uporabljate te nadomestne nize: - -| `%d` | postopoma nadomesti z argumenti pravila -| `%n$d` | nadomesti z n-tim argumentom pravila -| `%label` | nadomesti z oznako elementa (brez dvopičja) -| `%name` | nadomesti z imenom elementa (npr. `name`) -| `%value` | nadomesti z vrednostjo, ki jo je vnesel uporabnik - -```php -$form->addText('name', 'Ime:') - ->setRequired('Izpolnite prosim %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'najmanj %d in največ %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'največ %2$d in najmanj %1$d', [5, 10]); -``` - - -Pogoji -====== - -Poleg pravil lahko dodajamo tudi pogoje. Ti se zapisujejo podobno kot pravila, le da namesto `addRule()` uporabimo metodo `addCondition()` in seveda ne navajamo nobenega sporočila o napaki (pogoj se samo sprašuje): - -```php -$form->addPassword('password', 'Geslo:') - // če geslo ni daljše od 8 znakov - ->addCondition($form::MaxLength, 8) - // potem mora vsebovati števko - ->addRule($form::Pattern, 'Mora vsebovati števko', '.*[0-9].*'); -``` - -Pogoj je mogoče vezati tudi na drug element kot trenutni s pomočjo `addConditionOn()`. Kot prvi parameter navedemo sklic na element. V tem primeru bo e-pošta obvezna le takrat, ko se označi potrditveno polje (njegova vrednost bo true): - -```php -$form->addCheckbox('newsletters', 'pošiljajte mi novice'); - -$form->addEmail('email', 'E-pošta:') - // če je potrditveno polje označeno - ->addConditionOn($form['newsletters'], $form::Equal, true) - // potem zahtevaj e-pošto - ->setRequired('Vnesite e-poštni naslov'); -``` - -Iz pogojev je mogoče ustvarjati kompleksne strukture s pomočjo `elseCondition()` in `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // če je izpolnjen prvi pogoj - ->addConditionOn(/* ... */) // in drugi pogoj na drugem elementu - ->addRule(/* ... */) // zahtevaj to pravilo - ->elseCondition() // če drugi pogoj ni izpolnjen - ->addRule(/* ... */) // zahtevaj ta pravila - ->addRule(/* ... */) - ->endCondition() // vračamo se k prvemu pogoju - ->addRule(/* ... */); -``` - -V Nette je mogoče zelo enostavno reagirati na izpolnitev ali neizpolnitev pogoja tudi na strani JavaScripta s pomočjo metode `toggle()`, glej [#Dinamični JavaScript]. - - -Sklic na drug element -===================== - -Kot argument pravila ali pogoja lahko predamo tudi drug element obrazca. Pravilo potem uporabi vrednost, ki jo je kasneje vnesel uporabnik v brskalniku. Tako lahko npr. dinamično validiramo, da element `password` vsebuje enak niz kot element `password_confirm`: - -```php -$form->addPassword('password', 'Geslo'); -$form->addPassword('password_confirm', 'Potrdite geslo') - ->addRule($form::Equal, 'Vneseni gesli se ne ujemata', $form['password']); -``` - - -Pravila in pogoji po meri -========================= - -Včasih se znajdemo v situaciji, ko nam vgrajena validacijska pravila v Nette ne zadostujejo in moramo podatke od uporabnika validirati po svoje. V Nette je to zelo enostavno! - -Metodam `addRule()` ali `addCondition()` lahko kot prvi parameter predamo poljuben povratni klic. Ta sprejme kot prvi parameter sam element in vrača boolean vrednost, ki določa, ali je validacija potekala v redu. Pri dodajanju pravila s pomočjo `addRule()` je mogoče vnesti tudi druge argumente, ti so nato predani kot drugi parameter. - -Lasten nabor validatorjev tako lahko ustvarimo kot razred s statičnimi metodami: - -```php -class MyValidators -{ - // testira, ali je vrednost deljiva z argumentom - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // drugi validatorji - } -} -``` - -Uporaba je nato zelo enostavna: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Vrednost mora biti večkratnik števila %d', - 8, - ); -``` - -Lastna validacijska pravila lahko dodajamo tudi v JavaScript. Pogoj je, da je pravilo statična metoda. Njeno ime za JavaScript validator nastane s spojitvijo imena razreda brez povratnih poševnic `\`, podčrtaja `_` in imena metode. Npr. `App\MyValidators::validateDivisibility` zapišemo kot `AppMyValidators_validateDivisibility` in dodamo v objekt `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Dogodek onValidate -================== - -Po pošiljanju obrazca se izvede validacija, kjer se preverijo posamezna pravila, dodana s pomočjo `addRule()`, in nato se sproži [dogodek |nette:glossary#Dogodki eventi] `onValidate`. Njegov obravnavalnik lahko uporabimo za dodatno validacijo, tipično preverjanje pravilne kombinacije vrednosti v več elementih obrazca. - -Če se odkrije napaka, jo predamo v obrazec z metodo `addError()`. To lahko pokličemo bodisi na konkretnem elementu ali neposredno na obrazcu. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Ta kombinacija ni mogoča.'); - } -} -``` - - -Napake pri obdelavi -=================== - -V mnogih primerih se o napaki zavemo šele v trenutku, ko obdelujemo veljaven obrazec, na primer zapisujemo novo postavko v bazo podatkov in naletimo na podvojitev ključev. V takem primeru napako spet predamo v obrazec z metodo `addError()`. To lahko pokličemo bodisi na konkretnem elementu ali neposredno na obrazcu: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Neveljavno geslo.'); - } -} -``` - -Če je mogoče, priporočamo, da napako priključite neposredno elementu obrazca, ker se bo prikazala poleg njega pri uporabi privzetega rendererja. - -```php -$form['date']->addError('Oprostite, ampak ta datum je že zaseden.'); -``` - -Lahko `addError()` kličete večkrat in tako predaste obrazcu ali elementu več sporočil o napakah. Dobite jih s pomočjo `getErrors()`. - -Pozor, `$form->getErrors()` vrača povzetek vseh sporočil o napakah, tudi tistih, ki so bila predana neposredno posameznim elementom, ne le neposredno obrazcu. Sporočila o napakah, predana samo obrazcu, dobite preko `$form->getOwnErrors()`. - - -Spreminjanje vnosa -================== - -S pomočjo metode `addFilter()` lahko spremenimo vrednost, ki jo je vnesel uporabnik. V tem primeru bomo tolerirali in odstranjevali presledke v poštni številki: - -```php -$form->addText('zip', 'Poštna št.:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // odstranimo presledke iz poštne številke - }) - ->addRule($form::Pattern, 'Poštna št. ni v obliki petih števk', '\d{5}'); -``` - -Filter se vključi med validacijska pravila in pogoje, zato je vrstni red metod pomemben, tj. filter in pravilo se kličeta v takem vrstnem redu, kot je vrstni red metod `addFilter()` in `addRule()`. - - -Validacija JavaScript -===================== - -Jezik za oblikovanje pogojev in pravil je zelo močan. Vse konstrukcije pri tem delujejo tako na strani strežnika kot tudi na strani JavaScripta. Prenašajo se v HTML atributih `data-nette-rules` kot JSON. Samo validacijo nato izvaja skript, ki prestreže dogodek obrazca `submit`, pregleda posamezne elemente in izvede ustrezno validacijo. - -Ta skript je `netteForms.js` in je na voljo iz več možnih virov: - -Skript lahko vstavite neposredno v HTML stran iz CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Ali ga kopirate lokalno v javno mapo projekta (npr. iz `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Ali namestite preko [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -In nato naložite in zaženete: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Alternativno ga lahko naložite neposredno iz mape `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -Dinamični JavaScript -==================== - -Želite prikazati polja za vnos naslova samo, če uporabnik izbere pošiljanje blaga po pošti? Ni problema. Ključ je par metod `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Ta koda pravi, da ko je pogoj izpolnjen, torej ko je potrditveno polje označeno, bo viden HTML element `#address-container`. In obratno. Elemente obrazca z naslovom prejemnika tako postavimo v vsebnik s tem ID-jem in ob kliku na potrditveno polje se skrijejo ali prikažejo. To zagotavlja skript `netteForms.js`. - -Kot argument metode `toggle()` je mogoče predati poljuben selektor. Iz zgodovinskih razlogov se alfanumerični niz brez drugih posebnih znakov razume kot ID elementa, torej enako, kot če bi mu predhajal znak `#`. Drugi neobvezni parameter omogoča obrniti vedenje, tj. če bi uporabili `toggle('#address-container', false)`, bi se element nasprotno prikazal samo takrat, če potrditveno polje ne bi bilo označeno. - -Privzeta implementacija v JavaScriptu spreminja elementom lastnost `hidden`. Vedenje pa lahko enostavno spremenimo, na primer dodamo animacijo. Dovolj je, da v JavaScriptu prepišemo metodo `Nette.toggle` z lastno rešitvijo: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // skrijemo ali prikažemo 'el' glede na vrednost 'visible' - }); -}; -``` - - -Izklop validacije -================= - -Včasih se lahko zgodi, da je treba validacijo izklopiti. Če pritisk na gumb za pošiljanje ne sme izvajati validacije (primerno za gumbe *Prekliči* ali *Predogled*), jo izklopimo z metodo `$submit->setValidationScope([])`. Če naj izvaja le delno validacijo, lahko določimo, katera polja ali vsebnikov obrazca se naj validirajo. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Validira celoten obrazec -$form->addSubmit('send2') - ->setValidationScope([]); // Sploh ne validira -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Validira samo element name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Validira samo element age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Validira vsebnik details -``` - -`setValidationScope` ne vpliva na [#dogodek onValidate] pri obrazcu, ki bo vedno poklican. Dogodek `onValidate` pri vsebniku bo sprožen samo, če je ta vsebnik označen za delno validacijo. diff --git a/forms/uk/@home.texy b/forms/uk/@home.texy deleted file mode 100644 index 39bfd4af56..0000000000 --- a/forms/uk/@home.texy +++ /dev/null @@ -1,32 +0,0 @@ -Nette Forms -*********** - -<div class=perex> - -Nette Forms здійснили революцію у створенні веб-форм. Раптом стало достатньо написати кілька зрозумілих рядків коду, і ви мали готову форму, включаючи відображення, валідацію на стороні JavaScript та сервера, а також відмінно захищену. Ми покажемо, як - -- створювати зручні форми -- валідувати надіслані дані -- відображати елементи точно за потребою - -</div> - - -Використовуючи Nette Forms, ви уникнете цілої низки рутинних завдань, таких як написання валідації (до того ж подвійної, на стороні сервера та клієнта), мінімізуєте ймовірність виникнення помилок та дірок у безпеці. - -Форми можна використовувати або як частину Nette Application (тобто в презентерах), або повністю самостійно. Оскільки в обох випадках використання трохи відрізняється, ми підготували для вас два посібники: - -<div class="wiki-buttons"> -<div> "Форми в презентерах .[wiki-button]":in-presenter </div> -<div> "Форми самостійно .[wiki-button]":standalone </div> -</div> - - -Встановлення ------------- - -Бібліотеку можна завантажити та встановити за допомогою інструменту [Composer|best-practices:composer]: - -```shell -composer require nette/forms -``` diff --git a/forms/uk/@left-menu.texy b/forms/uk/@left-menu.texy deleted file mode 100644 index b1497f49e6..0000000000 --- a/forms/uk/@left-menu.texy +++ /dev/null @@ -1,14 +0,0 @@ -Nette Forms -*********** -- [Вступ |@home] -- [Форми в презентерах|in-presenter] -- [Форми самостійно|standalone] -- [Елементи форм |controls] -- [Валідація |validation] -- [Відображення |rendering] -- [Конфігурація |configuration] - - -Додаткове читання -***************** -- [Посібники та практики |best-practices:] diff --git a/forms/uk/@meta.texy b/forms/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/forms/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/forms/uk/configuration.texy b/forms/uk/configuration.texy deleted file mode 100644 index c2e8ab9954..0000000000 --- a/forms/uk/configuration.texy +++ /dev/null @@ -1,61 +0,0 @@ -Конфігурація форм -***************** - -.[perex] -У конфігурації можна змінити стандартні [повідомлення про помилки форм |validation]. - -```neon -forms: - messages: - 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.' - 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.' -``` - -Ось український переклад: - -```neon -forms: - messages: - Equal: 'Введіть %s.' - NotEqual: 'Це значення не повинно бути %s.' - Filled: 'Це поле є обов’язковим.' - Blank: 'Це поле повинно бути порожнім.' - MinLength: 'Будь ласка, введіть щонайменше %d символів.' - MaxLength: 'Будь ласка, введіть не більше %d символів.' - Length: 'Будь ласка, введіть значення довжиною від %d до %d символів.' - Email: 'Введіть дійсну адресу електронної пошти.' - URL: 'Будь ласка, введіть дійсну URL-адресу.' - Integer: 'Введіть дійсне ціле число.' - Float: 'Введіть дійсне число.' - Min: 'Будь ласка, введіть значення, більше або рівне %d.' - Max: 'Будь ласка, введіть значення, менше або рівне %d.' - Range: 'Введіть значення між %d та %d.' - MaxFileSize: 'Розмір завантаженого файлу може бути не більше %d байт.' - MaxPostSize: 'Завантажені дані перевищують ліміт %d байт.' - MimeType: 'Завантажений файл не у очікуваному форматі.' - Image: 'Завантажений файл має бути зображенням у форматі JPEG, GIF, PNG, WebP або AVIF.' - Nette\Forms\Controls\SelectBox::Valid: 'Будь ласка, виберіть дійсний варіант.' - Nette\Forms\Controls\UploadControl::Valid: 'Під час завантаження файлу сталася помилка.' - Nette\Forms\Controls\CsrfProtection::Protection: 'Ваша сесія закінчилася. Поверніться на головну сторінку та спробуйте ще раз.' -``` - -Якщо ви не використовуєте весь фреймворк, а отже, і конфігураційні файли, ви можете змінити стандартні повідомлення про помилки безпосередньо в масиві `Nette\Forms\Validator::$messages`. diff --git a/forms/uk/controls.texy b/forms/uk/controls.texy deleted file mode 100644 index afa847c3b1..0000000000 --- a/forms/uk/controls.texy +++ /dev/null @@ -1,559 +0,0 @@ -Елементи форми -************** - -.[perex] -Огляд стандартних елементів форми. - - -addText(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -================================================================================================== - -Додає однорядкове текстове поле (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Якщо користувач не заповнює поле, повертається порожній рядок `''`, або за допомогою `setNullable()` можна вказати, щоб повертався `null`. - -```php -$form->addText('name', 'Ім\'я:') - ->setRequired() - ->setNullable(); -``` - -Автоматично перевіряє UTF-8, обрізає пробіли зліва та справа та видаляє переноси рядків, які може надіслати зловмисник. - -Максимальну довжину можна обмежити за допомогою `setMaxLength()`. Змінити введене користувачем значення дозволяє [addFilter() |validation#Зміна вводу]. - -За допомогою `setHtmlType()` можна змінити візуальний характер текстового поля на типи, такі як `search`, `tel` або `url`, див. [специфікацію |https://developer.mozilla.org/en-US/docs/Learn/Forms/HTML5_input_types]. Пам'ятайте, що зміна типу є лише візуальною і не замінює функцію валідації. Для типу `url` доцільно додати специфічне правило валідації [URL |validation#Текстові поля]. - -.[note] -Для інших типів введення, таких як `number`, `range`, `email`, `date`, `datetime-local`, `time` та `color`, використовуйте спеціалізовані методи, такі як [#addInteger], [#addFloat], [#addEmail] [#addDate], [#addTime], [#addDateTime] та [#addColor], які забезпечують серверну валідацію. Типи `month` та `week` поки що не повністю підтримуються всіма браузерами. - -Елементу можна встановити так зване empty-value, щось на зразок значення за замовчуванням, але якщо користувач його не змінить, елемент поверне порожній рядок або `null`. - -```php -$form->addText('phone', 'Телефон:') - ->setHtmlType('tel') - ->setEmptyValue('+420'); -``` - - -addTextArea(string|int $name, $label=null): TextArea .[method] -============================================================== - -Додає поле для введення багаторядкового тексту (клас [TextArea |api:Nette\Forms\Controls\TextArea]). Якщо користувач не заповнює поле, повертається порожній рядок `''`, або за допомогою `setNullable()` можна вказати, щоб повертався `null`. - -```php -$form->addTextArea('note', 'Примітка:') - ->addRule($form::MaxLength, 'Примітка занадто довга', 10000); -``` - -Автоматично перевіряє UTF-8 і нормалізує роздільники рядків до `\n`. На відміну від однорядкового поля введення, обрізання пробілів не відбувається. - -Максимальну довжину можна обмежити за допомогою `setMaxLength()`. Змінити введене користувачем значення дозволяє [addFilter() |validation#Зміна вводу]. Можна встановити так зване empty-value за допомогою `setEmptyValue()`. - - -addInteger(string|int $name, $label=null): TextInput .[method] -============================================================== - -Додає поле для введення цілого числа (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Повертає або ціле число (integer), або `null`, якщо користувач нічого не ввів. - -```php -$form->addInteger('year', 'Рік:') - ->addRule($form::Range, 'Рік має бути в діапазоні від %d до %d.', [1900, 2023]); -``` - -Елемент відображається як `<input type="number">`. За допомогою методу `setHtmlType()` можна змінити тип на `range` для відображення у вигляді повзунка, або на `text`, якщо ви віддаєте перевагу стандартному текстовому полю без спеціальної поведінки типу `number`. - - -addFloat(string|int $name, $label=null): TextInput .[method]{data-version:3.1.12} -================================================================================= - -Додає поле для введення десяткового числа (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Повертає або float, або `null`, якщо користувач нічого не ввів. - -```php -$form->addFloat('level', 'Рівень:') - ->setDefaultValue(0) - ->addRule($form::Range, 'Рівень має бути в діапазоні від %d до %d.', [0, 100]); -``` - -Елемент відображається як `<input type="number">`. За допомогою методу `setHtmlType()` можна змінити тип на `range` для відображення у вигляді повзунка, або на `text`, якщо ви віддаєте перевагу стандартному текстовому полю без спеціальної поведінки типу `number`. - -Nette та браузер Chrome приймають як роздільник десяткових знаків як кому, так і крапку. Щоб ця функціональність була доступна і у Firefox, рекомендується встановити атрибут `lang` або для даного елемента, або для всієї сторінки, наприклад `<html lang="uk">`. - - -addEmail(string|int $name, $label=null, int $maxLength=255): TextInput .[method] -================================================================================ - -Додає поле для введення адреси електронної пошти (клас [TextInput |api:Nette\Forms\Controls\TextInput]). Якщо користувач не заповнює поле, повертається порожній рядок `''`, або за допомогою `setNullable()` можна вказати, щоб повертався `null`. - -```php -$form->addEmail('email', 'E-mail:'); -``` - -Перевіряє, чи є значення дійсною адресою електронної пошти. Не перевіряється, чи дійсно існує домен, перевіряється лише синтаксис. Автоматично перевіряє UTF-8, обрізає пробіли зліва та справа. - -Максимальну довжину можна обмежити за допомогою `setMaxLength()`. Змінити введене користувачем значення дозволяє [addFilter() |validation#Зміна вводу]. Можна встановити так зване empty-value за допомогою `setEmptyValue()`. - - -addPassword(string|int $name, $label=null, ?int $cols=null, ?int $maxLength=null): TextInput .[method] -====================================================================================================== - -Додає поле для введення пароля (клас [TextInput |api:Nette\Forms\Controls\TextInput]). - -```php -$form->addPassword('password', 'Пароль:') - ->setRequired() - ->addRule($form::MinLength, 'Пароль повинен містити щонайменше %d символів', 8) - ->addRule($form::Pattern, 'Повинен містити цифру', '.*[0-9].*'); -``` - -При повторному відображенні форми поле буде порожнім. Автоматично перевіряє UTF-8, обрізає пробіли зліва та справа та видаляє переноси рядків, які може надіслати зловмисник. - - -addCheckbox(string|int $name, $caption=null): Checkbox .[method] -================================================================ - -Додає прапорець (клас [Checkbox |api:Nette\Forms\Controls\Checkbox]). Повертає значення `true` або `false`, залежно від того, чи встановлено прапорець. - -```php -$form->addCheckbox('agree', 'Згоден з умовами') - ->setRequired('Необхідно погодитися з умовами'); -``` - - -addCheckboxList(string|int $name, $label=null, ?array $items=null): CheckboxList .[method] -========================================================================================== - -Додає прапорці для вибору кількох елементів (клас [CheckboxList |api:Nette\Forms\Controls\CheckboxList]). Повертає масив ключів вибраних елементів. Метод `getSelectedItems()` повертає значення замість ключів. - -```php -$form->addCheckboxList('colors', 'Кольори:', [ - 'r' => 'червоний', - 'g' => 'зелений', - 'b' => 'синій', -]); -``` - -Масив пропонованих елементів передаємо як третій параметр або методом `setItems()`. - -За допомогою `setDisabled(['r', 'g'])` можна деактивувати окремі елементи. - -Елемент автоматично перевіряє, що не відбулося підробки і що вибрані елементи дійсно є одними з пропонованих і не були деактивовані. Методом `getRawValue()` можна отримати надіслані елементи без цієї важливої перевірки. - -При встановленні вибраних елементів за замовчуванням також перевіряє, що вони є одними з пропонованих, інакше викидає виняток. Цю перевірку можна вимкнути за допомогою `checkDefaultValue(false)`. - -Якщо ви надсилаєте форму методом `GET`, ви можете вибрати компактніший спосіб передачі даних, який економить розмір рядка запиту. Він активується встановленням HTML-атрибута форми: - -```php -$form->setHtmlAttribute('data-nette-compact'); -``` - - -addRadioList(string|int $name, $label=null, ?array $items=null): RadioList .[method] -==================================================================================== - -Додає перемикачі (клас [RadioList |api:Nette\Forms\Controls\RadioList]). Повертає ключ вибраного елемента або `null`, якщо користувач нічого не вибрав. Метод `getSelectedItem()` повертає значення замість ключа. - -```php -$sex = [ - 'm' => 'чоловік', - 'f' => 'жінка', -]; -$form->addRadioList('gender', 'Стать:', $sex); -``` - -Масив пропонованих елементів передаємо як третій параметр або методом `setItems()`. - -За допомогою `setDisabled(['m', 'f'])` можна деактивувати окремі елементи. - -Елемент автоматично перевіряє, що не відбулося підробки і що вибраний елемент дійсно є одним із пропонованих і не був деактивований. Методом `getRawValue()` можна отримати надісланий елемент без цієї важливої перевірки. - -При встановленні вибраного елемента за замовчуванням також перевіряє, що він є одним із пропонованих, інакше викидає виняток. Цю перевірку можна вимкнути за допомогою `checkDefaultValue(false)`. - - -addSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): SelectBox .[method] -================================================================================================== - -Додає select box (клас [SelectBox |api:Nette\Forms\Controls\SelectBox]). Повертає ключ вибраного елемента або `null`, якщо користувач нічого не вибрав. Метод `getSelectedItem()` повертає значення замість ключа. - -```php -$countries = [ - 'CZ' => 'Чеська Республіка', - 'SK' => 'Словаччина', - 'GB' => 'Велика Британія', -]; - -$form->addSelect('country', 'Країна:', $countries) - ->setDefaultValue('SK'); -``` - -Масив пропонованих елементів передаємо як третій параметр або методом `setItems()`. Елементи також можуть бути двовимірним масивом: - -```php -$countries = [ - 'Європа' => [ - 'CZ' => 'Чеська Республіка', - 'SK' => 'Словаччина', - 'GB' => 'Велика Британія', - ], - 'CA' => 'Канада', - 'US' => 'США', - '?' => 'інша', -]; -``` - -У select box-ах часто перший елемент має особливе значення, служить як заклик до дії. Для додавання такого елемента служить метод `setPrompt()`. - -```php -$form->addSelect('country', 'Країна:', $countries) - ->setPrompt('Виберіть країну'); -``` - -За допомогою `setDisabled(['CZ', 'SK'])` можна деактивувати окремі елементи. - -Елемент автоматично перевіряє, що не відбулося підробки і що вибраний елемент дійсно є одним із пропонованих і не був деактивований. Методом `getRawValue()` можна отримати надісланий елемент без цієї важливої перевірки. - -При встановленні вибраного елемента за замовчуванням також перевіряє, що він є одним із пропонованих, інакше викидає виняток. Цю перевірку можна вимкнути за допомогою `checkDefaultValue(false)`. - - -addMultiSelect(string|int $name, $label=null, ?array $items=null, ?int $size=null): MultiSelectBox .[method] -============================================================================================================ - -Додає select box для вибору кількох елементів (клас [MultiSelectBox |api:Nette\Forms\Controls\MultiSelectBox]). Повертає масив ключів вибраних елементів. Метод `getSelectedItems()` повертає значення замість ключів. - -```php -$form->addMultiSelect('countries', 'Країна:', $countries); -``` - -Масив пропонованих елементів передаємо як третій параметр або методом `setItems()`. Елементи також можуть бути двовимірним масивом. - -За допомогою `setDisabled(['CZ', 'SK'])` можна деактивувати окремі елементи. - -Елемент автоматично перевіряє, що не відбулося підробки і що вибрані елементи дійсно є одними з пропонованих і не були деактивовані. Методом `getRawValue()` можна отримати надіслані елементи без цієї важливої перевірки. - -При встановленні вибраних елементів за замовчуванням також перевіряє, що вони є одними з пропонованих, інакше викидає виняток. Цю перевірку можна вимкнути за допомогою `checkDefaultValue(false)`. - - -addUpload(string|int $name, $label=null): UploadControl .[method] -================================================================= - -Додає поле для завантаження файлу (клас [UploadControl |api:Nette\Forms\Controls\UploadControl]). Повертає об'єкт [FileUpload |http:request#FileUpload] навіть у випадку, якщо користувач не надіслав жодного файлу, що можна перевірити методом `FileUpload::hasFile()`. - -```php -$form->addUpload('avatar', 'Аватар:') - ->addRule($form::Image, 'Аватар має бути у форматі JPEG, PNG, GIF, WebP або AVIF.') - ->addRule($form::MaxFileSize, 'Максимальний розмір 1 МБ.', 1024 * 1024); -``` - -Якщо файл не вдалося коректно завантажити, форма не надсилається успішно і відображається помилка. Тобто при успішному надсиланні не потрібно перевіряти метод `FileUpload::isOk()`. - -Ніколи не довіряйте оригінальній назві файлу, повернутій методом `FileUpload::getName()`, клієнт міг надіслати шкідливу назву файлу з наміром пошкодити або зламати ваш застосунок. - -Правила `MimeType` та `Image` визначають потрібний тип на основі сигнатури файлу і не перевіряють його цілісність. Чи не пошкоджене зображення, можна з'ясувати, наприклад, спробувавши його [завантажити |http:request#toImage]. - - -addMultiUpload(string|int $name, $label=null): UploadControl .[method] -====================================================================== - -Додає поле для одночасного завантаження кількох файлів (клас [UploadControl |api:Nette\Forms\Controls\UploadControl]). Повертає масив об'єктів [FileUpload |http:request#FileUpload]. Метод `FileUpload::hasFile()` для кожного з них повертатиме `true`. - -```php -$form->addMultiUpload('files', 'Файли:') - ->addRule($form::MaxLength, 'Максимально можна завантажити %d файлів', 10); -``` - -Якщо якийсь із файлів не вдалося коректно завантажити, форма не надсилається успішно і відображається помилка. Тобто при успішному надсиланні не потрібно перевіряти метод `FileUpload::isOk()`. - -Ніколи не довіряйте оригінальним назвам файлів, повернутим методом `FileUpload::getName()`, клієнт міг надіслати шкідливу назву файлу з наміром пошкодити або зламати ваш застосунок. - -Правила `MimeType` та `Image` визначають потрібний тип на основі сигнатури файлу і не перевіряють його цілісність. Чи не пошкоджене зображення, можна з'ясувати, наприклад, спробувавши його [завантажити |http:request#toImage]. - - -addDate(string|int $name, $label=null): DateTimeControl .[method]{data-version:3.1.14} -====================================================================================== - -Додає поле, яке дозволяє користувачеві легко ввести дату, що складається з року, місяця та дня (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Як значення за замовчуванням приймає або об'єкти, що реалізують інтерфейс `DateTimeInterface`, рядок з часом, або число, що представляє UNIX timestamp. Те саме стосується аргументів правил `Min`, `Max` або `Range`, які визначають мінімальну та максимальну допустиму дату. - -```php -$form->addDate('date', 'Дата:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Дата повинна бути щонайменше місячної давності.', new DateTime('-1 month')); -``` - -Стандартно повертає об'єкт `DateTimeImmutable`, методом `setFormat()` ви можете вказати [текстовий формат |https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] або timestamp: - -```php -$form->addDate('date', 'Дата:') - ->setFormat('Y-m-d'); -``` - - -addTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=============================================================================================================== - -Додає поле, яке дозволяє користувачеві легко ввести час, що складається з годин, хвилин та, за бажанням, секунд (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Як значення за замовчуванням приймає або об'єкти, що реалізують інтерфейс `DateTimeInterface`, рядок з часом, або число, що представляє UNIX timestamp. З цих вхідних даних використовується лише інформація про час, дата ігнорується. Те саме стосується аргументів правил `Min`, `Max` або `Range`, які визначають мінімальний та максимальний допустимий час. Якщо встановлене мінімальне значення більше за максимальне, створюється часовий діапазон, що переходить через північ. - -```php -$form->addTime('time', 'Час:', withSeconds: true) - ->addRule($form::Range, 'Час має бути в діапазоні від %d до %d.', ['12:30', '13:30']); -``` - -Стандартно повертає об'єкт `DateTimeImmutable` (з датою 1 січня 1 року), методом `setFormat()` ви можете вказати [текстовий формат |https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters]: - -```php -$form->addTime('time', 'Час:') - ->setFormat('H:i'); -``` - - -addDateTime(string|int $name, $label=null, bool $withSeconds=false): DateTimeControl .[method]{data-version:3.1.14} -=================================================================================================================== - -Додає поле, яке дозволяє користувачеві легко ввести дату та час, що складаються з року, місяця, дня, годин, хвилин та, за бажанням, секунд (клас [DateTimeControl |api:Nette\Forms\Controls\DateTimeControl]). - -Як значення за замовчуванням приймає або об'єкти, що реалізують інтерфейс `DateTimeInterface`, рядок з часом, або число, що представляє UNIX timestamp. Те саме стосується аргументів правил `Min`, `Max` або `Range`, які визначають мінімальну та максимальну допустиму дату. - -```php -$form->addDateTime('datetime', 'Дата і час:') - ->setDefaultValue(new DateTime) - ->addRule($form::Min, 'Дата повинна бути щонайменше місячної давності.', new DateTime('-1 month')); -``` - -Стандартно повертає об'єкт `DateTimeImmutable`, методом `setFormat()` ви можете вказати [текстовий формат |https://www.php.net/manual/en/datetime.format.php#refsect1-datetime.format-parameters] або timestamp: - -```php -$form->addDateTime('datetime') - ->setFormat(DateTimeControl::FormatTimestamp); -``` - - -addColor(string|int $name, $label=null): ColorPicker .[method]{data-version:3.1.14} -=================================================================================== - -Додає поле для вибору кольору (клас [ColorPicker |api:Nette\Forms\Controls\ColorPicker]). Колір - це рядок у форматі `#rrggbb`. Якщо користувач не зробить вибір, повертається чорний колір `#000000`. - -```php -$form->addColor('color', 'Колір:') - ->setDefaultValue('#3C8ED7'); -``` - - -addHidden(string|int $name, ?string $default=null): HiddenField .[method] -========================================================================= - -Додає приховане поле (клас [HiddenField |api:Nette\Forms\Controls\HiddenField]). - -```php -$form->addHidden('userid'); -``` - -За допомогою `setNullable()` можна налаштувати, щоб повертався `null` замість порожнього рядка. Змінити надіслане значення дозволяє [addFilter() |validation#Зміна вводу]. - -Хоча елемент прихований, **важливо усвідомлювати**, що значення все ще може бути змінено або підроблено зловмисником. Завжди ретельно перевіряйте та валідуйте всі отримані значення на стороні сервера, щоб запобігти ризикам безпеки, пов'язаним з маніпуляцією даними. - - -addSubmit(string|int $name, $caption=null): SubmitButton .[method] -================================================================== - -Додає кнопку надсилання (клас [SubmitButton |api:Nette\Forms\Controls\SubmitButton]). - -```php -$form->addSubmit('submit', 'Надіслати'); -``` - -У формі можна мати кілька кнопок надсилання: - -```php -$form->addSubmit('register', 'Зареєструватися'); -$form->addSubmit('cancel', 'Скасувати'); -``` - -Щоб з'ясувати, на яку з них було натиснуто, використовуйте: - -```php -if ($form['register']->isSubmittedBy()) { - // ... -} -``` - -Якщо ви не хочете валідувати всю форму при натисканні кнопки (наприклад, для кнопок *Скасувати* або *Попередній перегляд*), використовуйте [setValidationScope() |validation#Вимкнення валідації]. - - -addButton(string|int $name, $caption): Button .[method] -======================================================= - -Додає кнопку (клас [Button |api:Nette\Forms\Controls\Button]), яка не має функції надсилання. Отже, її можна використовувати для іншої функції, наприклад, виклику функції JavaScript при натисканні. - -```php -$form->addButton('raise', 'Підвищити зарплату') - ->setHtmlAttribute('onclick', 'raiseSalary()'); -``` - - -addImageButton(string|int $name, ?string $src=null, ?string $alt=null): ImageButton .[method] -============================================================================================= - -Додає кнопку надсилання у вигляді зображення (клас [ImageButton |api:Nette\Forms\Controls\ImageButton]). - -```php -$form->addImageButton('submit', '/path/to/image'); -``` - -При використанні кількох кнопок надсилання можна з'ясувати, на яку було натиснуто, за допомогою `$form['submit']->isSubmittedBy()`. - - -addContainer(string|int $name): Container .[method] -=================================================== - -Додає підформу (клас [Container |api:Nette\Forms\Container]), або контейнер, до якого можна додавати інші елементи так само, як ми додаємо їх до форми. Також працюють методи `setDefaults()` або `getValues()`. - -```php -$sub1 = $form->addContainer('first'); -$sub1->addText('name', 'Ваше ім\'я:'); -$sub1->addEmail('email', 'Email:'); - -$sub2 = $form->addContainer('second'); -$sub2->addText('name', 'Ваше ім\'я:'); -$sub2->addEmail('email', 'Email:'); -``` - -Надіслані дані потім повертаються як багатовимірна структура: - -```php -[ - 'first' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], - 'second' => [ - 'name' => /* ... */, - 'email' => /* ... */, - ], -] -``` - - -Огляд налаштувань -================= - -Для всіх елементів ми можемо викликати наступні методи (повний огляд в [документації API |https://api.nette.org/forms/master/Nette/Forms/Controls.html]): - -.[table-form-methods language-php] -| `setDefaultValue($value)` | встановлює значення за замовчуванням -| `getValue()` | отримати поточне значення -| `setOmitted()` | [##пропуск значення] -| `setDisabled()` | [##деактивація елементів] - -Відображення: -.[table-form-methods language-php] -| `setCaption($caption)` | змінює підпис елемента -| `setTranslator($translator)` | встановлює [перекладач |rendering#Переклад] -| `setHtmlAttribute($name, $value)` | встановлює [HTML-атрибут |rendering#HTML атрибути] елемента -| `setHtmlId($id)` | встановлює HTML-атрибут `id` -| `setHtmlType($type)` | встановлює HTML-атрибут `type` -| `setHtmlName($name)` | встановлює HTML-атрибут `name` -| `setOption($key, $value)` | [налаштування для відображення |rendering#Options] - -Валідація: -.[table-form-methods language-php] -| `setRequired()` | [обов'язковий елемент |validation] -| `addRule()` | встановлення [правила валідації |validation#Правила] -| `addCondition()`, `addConditionOn()` | встановлює [умову валідації |validation#Умови] -| `addError($message)` | [передача повідомлення про помилку |validation#Помилки під час обробки] - -Для елементів `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()` можна викликати наступні методи: - -.[table-form-methods language-php] -| `setNullable()` | встановлює, чи поверне getValue() `null` замість порожнього рядка -| `setEmptyValue($value)` | встановлює спеціальне значення, яке вважається порожнім рядком -| `setMaxLength($length)` | встановлює максимальну кількість дозволених символів -| `addFilter($filter)` | [зміна вводу |validation#Зміна вводу] - - -Пропуск значення -================ - -Якщо нас не цікавить значення, введене користувачем, ми можемо пропустити його за допомогою `setOmitted()` з результату методу `$form->getValues()` або з даних, що передаються в обробники. Це корисно для різних паролів для перевірки, антиспам-елементів тощо. - -```php -$form->addPassword('passwordVerify', 'Пароль для перевірки:') - ->setRequired('Будь ласка, введіть пароль ще раз для перевірки') - ->addRule($form::Equal, 'Паролі не співпадають', $form['password']) - ->setOmitted(); -``` - - -Деактивація елементів -===================== - -Елементи можна деактивувати за допомогою `setDisabled()`. Такий елемент користувач не може редагувати. - -```php -$form->addText('username', 'Ім\'я користувача:') - ->setDisabled(); -``` - -Вимкнені елементи браузер взагалі не надсилає на сервер, тому ви їх не знайдете в даних, повернутих функцією `$form->getValues()`. Однак, якщо ви встановите `setOmitted(false)`, Nette включить їхнє значення за замовчуванням у ці дані. - -При виклику `setDisabled()` з міркувань безпеки **значення елемента видаляється**. Якщо ви встановлюєте значення за замовчуванням, це необхідно зробити після його деактивації: - -```php -$form->addText('username', 'Ім\'я користувача:') - ->setDisabled() - ->setDefaultValue($userName); -``` - -Альтернативою вимкненим елементам є елементи з HTML-атрибутом `readonly`, які браузер надсилає на сервер. Хоча елемент призначений лише для читання, **важливо усвідомлювати**, що його значення все ще може бути змінено або підроблено зловмисником. - - -Власні елементи -=============== - -Поряд із широким спектром вбудованих елементів форми, ви можете додавати власні елементи до форми таким чином: - -```php -$form->addComponent(new DateInput('Дата:'), 'date'); -// альтернативний синтаксис: $form['date'] = new DateInput('Дата:'); -``` - -.[note] -Форма є нащадком класу [Container |component-model:#Container], а окремі елементи є нащадками [Component |component-model:#Component]. - -Існує спосіб визначення нових методів форми для додавання власних елементів (наприклад, `$form->addZip()`). Це так звані extension methods. Недоліком є те, що для них не працюватиме автодоповнення в редакторах. - -```php -use Nette\Forms\Container; - -// додамо метод 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, 'Щонайменше 5 цифр', '[0-9]{5}'); -}); - -// використання -$form->addZip('zip', 'Поштовий індекс:'); -``` - - -Низькорівневі елементи -====================== - -Можна також використовувати елементи, які ми записуємо лише в шаблоні і не додаємо до форми жодним із методів `$form->addXyz()`. Наприклад, коли ми виводимо записи з бази даних і заздалегідь не знаємо, скільки їх буде і які у них будуть ID, і хочемо біля кожного рядка відобразити прапорець або перемикач, достатньо закодувати його в шаблоні: - -```latte -{foreach $items as $item} - <p><input type=checkbox name="sel[]" value={$item->id}> {$item->name}</p> -{/foreach} -``` - -А після надсилання дізнаємося значення: - -```php -$data = $form->getHttpData($form::DataText, 'sel[]'); -$data = $form->getHttpData($form::DataText | $form::DataKeys, 'sel[]'); -``` - -де перший параметр - це тип елемента (`DataFile` для `type=file`, `DataLine` для однорядкових полів введення, таких як `text`, `password`, `email` тощо, і `DataText` для всіх інших), а другий параметр `sel[]` відповідає HTML-атрибуту name. Тип елемента можна комбінувати зі значенням `DataKeys`, яке зберігає ключі елементів. Це особливо корисно для `select`, `radioList` та `checkboxList`. - -Важливо те, що `getHttpData()` повертає санітизоване значення, у цьому випадку це завжди буде масив дійсних рядків UTF-8, незалежно від того, що зловмисник спробував би підсунути серверу. Це аналог прямої роботи з `$_POST` або `$_GET`, але з тією суттєвою різницею, що він завжди повертає чисті дані, так, як ви звикли зі стандартними елементами форм Nette. diff --git a/forms/uk/in-presenter.texy b/forms/uk/in-presenter.texy deleted file mode 100644 index 4a23b0bfdb..0000000000 --- a/forms/uk/in-presenter.texy +++ /dev/null @@ -1,431 +0,0 @@ -Форми в презентерах -******************* - -.[perex] -Nette Forms значно полегшують створення та обробку веб-форм. У цьому розділі ви дізнаєтеся, як використовувати форми всередині презентерів. - -Якщо вас цікавить, як використовувати їх повністю окремо без решти фреймворку, для вас призначений посібник для [самостійного використання |standalone]. - - -Перша форма -=========== - -Спробуємо написати просту форму реєстрації. Її код буде таким: - -```php -use Nette\Application\UI\Form; - -$form = new Form; -$form->addText('name', 'Ім\'я:'); -$form->addPassword('password', 'Пароль:'); -$form->addSubmit('send', 'Зареєструватися'); -$form->onSuccess[] = [$this, 'formSucceeded']; -``` - -і в браузері вона відобразиться так: - -[* form-cs.webp *] - -Форма в presenter'і є об'єктом класу `Nette\Application\UI\Form`, її попередник `Nette\Forms\Form` призначений для самостійного використання. Ми додали до неї так звані елементи ім'я, пароль та кнопку відправки. І, нарешті, рядок з `$form->onSuccess` говорить, що після відправки та успішної валідації має бути викликаний метод `$this->formSucceeded()`. - -З точки зору presenter'а форма є звичайним компонентом. Тому з нею поводяться як з компонентом і включають її до presenter'а за допомогою [фабричного методу |application:components#Фабричні методи]. Це виглядатиме так: - -```php .{file:app/Presentation/Home/HomePresenter.php} -use Nette; -use Nette\Application\UI\Form; - -class HomePresenter extends Nette\Application\UI\Presenter -{ - protected function createComponentRegistrationForm(): Form - { - $form = new Form; - $form->addText('name', 'Ім\'я:'); - $form->addPassword('password', 'Пароль:'); - $form->addSubmit('send', 'Зареєструватися'); - $form->onSuccess[] = [$this, 'formSucceeded']; - return $form; - } - - public function formSucceeded(Form $form, $data): void - { - // тут ми обробляємо дані, надіслані формою - // $data->name містить ім'я - // $data->password містить пароль - $this->flashMessage('Ви були успішно зареєстровані.'); - $this->redirect('Home:'); - } -} -``` - -А в шаблоні форму відображаємо за допомогою тегу `{control}`: - -```latte .{file:app/Presentation/Home/default.latte} -<h1>Реєстрація</h1> - -{control registrationForm} -``` - -І це, власне, все :-) Ми маємо функціональну та ідеально [захищену |#Захист від вразливостей] форму. - -А тепер ви, мабуть, думаєте, що це було занадто швидко, і розмірковуєте, як можливо, що викликається метод `formSucceeded()` і які параметри він отримує. Звичайно, ви маєте рацію, це заслуговує на пояснення. - -Nette пропонує свіжий механізм, який ми називаємо [Hollywood style |application:components#Голлівудський стиль]. Замість того, щоб ви як розробник постійно запитували, чи щось сталося («чи була форма відправлена?», «чи була вона відправлена валідно?» і «чи не була вона підроблена?»), ви говорите фреймворку «коли форма буде валідно заповнена, виклич цей метод» і залишаєте подальшу роботу йому. Якщо ви програмуєте на JavaScript, цей стиль програмування вам добре знайомий. Ви пишете функції, які викликаються, коли настає певна [подія |nette:glossary#Події události]. І мова передає їм відповідні аргументи. - -Саме так побудований і вищезгаданий код presenter'а. Масив `$form->onSuccess` представляє список PHP callback'ів, які Nette викличе в момент, коли форма буде відправлена і правильно заповнена (тобто є валідною). У рамках [життєвого циклу presenter'а |application:presenters#Життєвий цикл презентера] це так званий сигнал, тому вони викликаються після методу `action*` і перед методом `render*`. І кожному callback'у передає як перший параметр саму форму, а як другий — надіслані дані у вигляді об'єкта [ArrayHash |utils:arrays#ArrayHash]. Перший параметр можна пропустити, якщо об'єкт форми вам не потрібен. А другий параметр може бути хитрішим, але про це [пізніше |#Мапування на класи]. - -Об'єкт `$data` містить ключі `name` та `password` з даними, які заповнив користувач. Зазвичай дані відразу відправляються на подальшу обробку, що може бути, наприклад, вставкою в базу даних. Однак під час обробки може виникнути помилка, наприклад, ім'я користувача вже зайняте. У такому випадку ми передаємо помилку назад у форму за допомогою `addError()` і дозволяємо їй відобразитися знову, вже з повідомленням про помилку. - -```php -$form->addError('Вибачте, це ім\'я користувача вже використовується.'); -``` - -Крім `onSuccess`, існує ще `onSubmit`: callback'и викликаються завжди після відправлення форми, навіть якщо вона заповнена неправильно. А також `onError`: callback'и викликаються тільки якщо відправлення не є валідним. Вони викликаються навіть тоді, якщо в `onSuccess` або `onSubmit` ми зробимо форму невалідною за допомогою `addError()`. - -Після обробки форми ми перенаправляємо на наступну сторінку. Це запобігає небажаному повторному надсиланню форми кнопкою *оновити*, *назад* або рухом в історії браузера. - -Спробуйте додати й інші [елементи форми |controls]. - - -Доступ до елементів -=================== - -Форма є компонентом presenter'а, у нашому випадку названим `registrationForm` (за назвою фабричного методу `createComponentRegistrationForm`), тому будь-де в presenter'і ви можете отримати доступ до форми за допомогою: - -```php -$form = $this->getComponent('registrationForm'); -// альтернативний синтаксис: $form = $this['registrationForm']; -``` - -Окремі елементи форми також є компонентами, тому ви можете отримати доступ до них таким же чином: - -```php -$input = $form->getComponent('name'); // або $input = $form['name']; -$button = $form->getComponent('send'); // або $button = $form['send']; -``` - -Елементи видаляються за допомогою unset: - -```php -unset($form['name']); -``` - - -Правила валідації -================= - -Тут прозвучало слово *валідний,* але форма поки що не має жодних правил валідації. Давайте це виправимо. - -Ім'я буде обов'язковим, тому позначимо його методом `setRequired()`, аргументом якого є текст повідомлення про помилку, яке відобразиться, якщо користувач не заповнить ім'я. Якщо аргумент не вказано, буде використано стандартне повідомлення про помилку. - -```php -$form->addText('name', 'Ім\'я:') - ->setRequired('Будь ласка, введіть ім\'я'); -``` - -Спробуйте надіслати форму без заповненого імені, і ви побачите, що з'явиться повідомлення про помилку, і браузер або сервер відхилятимуть її доти, доки ви не заповните поле. - -Водночас систему не обдуриш, написавши в полі, наприклад, лише пробіли. Ні. Nette автоматично видаляє пробіли зліва та справа. Спробуйте самі. Це те, що ви завжди повинні робити з кожним однорядковим полем введення, але про це часто забувають. Nette робить це автоматично. (Можете спробувати обдурити форму і надіслати як ім'я багаторядковий рядок. Навіть тут Nette не дасть себе обдурити і замінить переноси рядків на пробіли.) - -Форма завжди валідується на стороні сервера, але також генерується JavaScript-валідація, яка відбувається миттєво, і користувач дізнається про помилку відразу, без необхідності надсилати форму на сервер. За це відповідає скрипт `netteForms.js`. Вставте його в шаблон макета: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Якщо ви подивитеся на вихідний код сторінки з формою, то помітите, що Nette вставляє обов'язкові елементи в елементи з CSS-класом `required`. Спробуйте додати до шаблону наступний стиль, і напис "Ім'я" стане червоним. Таким чином, ми елегантно позначаємо обов'язкові елементи для користувачів: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Інші правила валідації додамо методом `addRule()`. Перший параметр — це правило, другий — знову текст повідомлення про помилку, а потім може йти аргумент правила валідації. Що це означає? - -Розширимо форму новим необов'язковим полем "вік", яке має бути цілим числом (`addInteger()`) і, крім того, в дозволеному діапазоні (`$form::Range`). І тут ми використаємо третій параметр методу `addRule()`, яким передамо валідатору необхідний діапазон у вигляді пари `[від, до]`: - -```php -$form->addInteger('age', 'Вік:') - ->addRule($form::Range, 'Вік має бути від 18 до 120', [18, 120]); -``` - -.[tip] -Якщо користувач не заповнить поле, правила валідації не перевірятимуться, оскільки елемент є необов'язковим. - -Тут виникає простір для невеликого рефакторингу. У повідомленні про помилку та в третьому параметрі числа вказані дубльовано, що не ідеально. Якби ми створювали [багатомовні форми |rendering#Переклад], і повідомлення, що містить числа, було б перекладено кількома мовами, це ускладнило б можливу зміну значень. З цієї причини можна використовувати плейсхолдери `%d`, і Nette доповнить значення: - -```php - ->addRule($form::Range, 'Вік має бути від %d до %d років', [18, 120]); -``` - -Повернемося до елемента `password`, який ми також зробимо обов'язковим і ще перевіримо мінімальну довжину пароля (`$form::MinLength`), знову ж таки, використовуючи плейсхолдер: - -```php -$form->addPassword('password', 'Пароль:') - ->setRequired('Виберіть пароль') - ->addRule($form::MinLength, 'Пароль повинен містити щонайменше %d символів', 8); -``` - -Додамо до форми ще поле `passwordVerify`, де користувач введе пароль ще раз для перевірки. За допомогою правил валідації перевіримо, чи обидва паролі однакові (`$form::Equal`). А як параметр дамо посилання на перший пароль за допомогою [квадратних дужок |#Доступ до елементів]: - -```php -$form->addPassword('passwordVerify', 'Пароль для перевірки:') - ->setRequired('Будь ласка, введіть пароль ще раз для перевірки') - ->addRule($form::Equal, 'Паролі не співпадають', $form['password']) - ->setOmitted(); -``` - -За допомогою `setOmitted()` ми позначили елемент, значення якого насправді не має значення і який існує лише для валідації. Значення не передається до `$data`. - -Таким чином, ми маємо готову, повністю функціональну форму з валідацією в PHP та JavaScript. Можливості валідації Nette набагато ширші, можна створювати умови, за якими відображати та приховувати частини сторінки тощо. Все це ви дізнаєтеся в розділі про [валідацію форм |validation]. - - -Значення за замовчуванням -========================= - -Елементам форми зазвичай встановлюють значення за замовчуванням: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Часто буває зручно встановити значення за замовчуванням для всіх елементів одночасно. Наприклад, коли форма використовується для редагування записів. Ми читаємо запис з бази даних і встановлюємо значення за замовчуванням: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Викликайте `setDefaults()` після визначення елементів. - - -Відображення форми -================== - -Стандартно форма відображається як таблиця. Окремі елементи відповідають основному правилу доступності - всі написи записані як `<label>` і пов'язані з відповідним елементом форми. При натисканні на напис курсор автоматично з'являється в полі форми. - -Кожному елементу ми можемо встановлювати будь-які HTML-атрибути. Наприклад, додати placeholder: - -```php -$form->addInteger('age', 'Вік:') - ->setHtmlAttribute('placeholder', 'Будь ласка, заповніть вік'); -``` - -Способів відображення форми дійсно багато, тому цьому присвячено [окремий розділ про відображення |rendering]. - - -Мапування на класи -================== - -Повернемося до методу `formSucceeded()`, який у другому параметрі `$data` отримує надіслані дані як об'єкт `ArrayHash`. Оскільки це загальний клас, щось на зразок `stdClass`, нам при роботі з ним бракуватиме певного комфорту, такого як підказка властивостей в редакторах або статичний аналіз коду. Це можна було б вирішити, маючи для кожної форми конкретний клас, властивості якого представляють окремі елементи. Наприклад: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Альтернативно, ви можете використовувати конструктор: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public ?int $age, - public string $password, - ) { - } -} -``` - -Властивості класу даних також можуть бути enum'ами, і вони будуть автоматично зіставлені. .{data-version:3.2.4} - -Як сказати Nette, щоб він повертав нам дані як об'єкти цього класу? Легше, ніж ви думаєте. Достатньо лише вказати клас як тип параметра `$data` в обробному методі: - -```php -public function formSucceeded(Form $form, RegistrationFormData $data): void -{ - // $data є екземпляром RegistrationFormData - $name = $data->name; - // ... -} -``` - -Як тип можна також вказати `array`, і тоді дані передадуться як масив. - -Аналогічним чином можна використовувати і функцію `getValues()`, якій назву класу або об'єкт для гідратації передамо як параметр: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Якщо форми утворюють багаторівневу структуру, що складається з контейнерів, створіть для кожного окремий клас: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Мапування потім з типу властивості `$person` дізнається, що контейнер потрібно мапувати на клас `PersonFormData`. Якщо властивість містила б масив контейнерів, вкажіть тип `array` і клас для мапування передайте безпосередньо контейнеру: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Проект класу даних форми можна згенерувати за допомогою методу `Nette\Forms\Blueprint::dataClass($form)`, який виведе його на сторінку браузера. Потім код достатньо клацнути, щоб виділити, і скопіювати до проекту. .{data-version:3.1.15} - - -Кілька кнопок -============= - -Якщо форма має більше однієї кнопки, зазвичай потрібно розрізнити, яка з них була натиснута. Ми можемо створити для кожної кнопки власну функцію-обробник. Встановимо її як обробник для [події |nette:glossary#Події události] `onClick`: - -```php -$form->addSubmit('save', 'Зберегти') - ->onClick[] = [$this, 'saveButtonPressed']; - -$form->addSubmit('delete', 'Видалити') - ->onClick[] = [$this, 'deleteButtonPressed']; -``` - -Ці обробники викликаються лише у випадку валідно заповненої форми, так само як і у випадку події `onSuccess`. Різниця полягає в тому, що як перший параметр замість форми може передаватися кнопка відправки, залежно від типу, який ви вкажете: - -```php -public function saveButtonPressed(Nette\Forms\Controls\Button $button, $data) -{ - $form = $button->getForm(); - // ... -} -``` - -Коли форма надсилається кнопкою <kbd>Enter</kbd>, це вважається так, ніби вона була надіслана першою кнопкою. - - -Подія onAnchor -============== - -Коли у фабричному методі (наприклад, `createComponentRegistrationForm`) ми створюємо форму, вона ще не знає, чи була вона надіслана, і з якими даними. Але є випадки, коли нам потрібно знати надіслані значення, наприклад, від них залежить подальший вигляд форми, або вони потрібні для залежних селектбоксів тощо. - -Тому частину коду, що створює форму, можна викликати лише в момент, коли вона так звано "заякорена", тобто вже пов'язана з presenter'ом і знає свої надіслані дані. Такий код передаємо до масиву `$onAnchor`: - -```php -$country = $form->addSelect('country', 'Країна:', $this->model->getCountries()); -$city = $form->addSelect('city', 'Місто:'); - -$form->onAnchor[] = function () use ($country, $city) { - // ця функція викликається, коли форма вже знає, чи була вона надіслана і з якими даними - // тому можна використовувати метод getValue() - $val = $country->getValue(); - $city->setItems($val ? $this->model->getCities($val) : []); -}; -``` - - -Захист від вразливостей -======================= - -Nette Framework надає великого значення безпеці, тому ретельно дбає про надійний захист форм. Це робиться повністю прозоро і не вимагає ручного налаштування. - -Крім того, що форми захищають від атаки [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] та [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], вони виконують багато дрібних заходів безпеки, про які вам вже не потрібно думати. - -Наприклад, вони фільтрують з вхідних даних усі керуючі символи та перевіряють валідність кодування UTF-8, тому дані з форми завжди будуть чистими. У селектбоксах та радіо-списках перевіряється, що вибрані елементи були дійсно з запропонованих і не відбулося підробки. Ми вже згадували, що в однорядкових текстових полях видаляються символи кінця рядка, які міг надіслати зловмисник. У багаторядкових полях, навпаки, нормалізуються символи кінця рядка. І так далі. - -Nette вирішує за вас ризики безпеки, про існування яких багато програмістів навіть не підозрюють. - -Згадана CSRF-атака полягає в тому, що зловмисник заманює жертву на сторінку, яка непомітно в браузері жертви виконує запит на сервер, на якому жертва авторизована, і сервер вважає, що запит виконала жертва за власною волею. Тому Nette запобігає надсиланню POST-форми з іншого домену. Якщо з якоїсь причини ви хочете вимкнути захист і дозволити надсилати форму з іншого домену, використовуйте: - -```php -$form->allowCrossOrigin(); // УВАГА! Вимикає захист! -``` - -Цей захист використовує SameSite cookie з назвою `_nss`. Захист за допомогою SameSite cookie може бути не 100% надійним, тому бажано увімкнути ще захист за допомогою токена: - -```php -$form->addProtection(); -``` - -Рекомендуємо таким чином захищати форми в адміністративній частині сайту, які змінюють чутливі дані в додатку. Фреймворк захищається від CSRF-атаки шляхом генерації та перевірки авторизаційного токена, який зберігається в сесії. Тому необхідно перед відображенням форми мати відкриту сесію. В адміністративній частині сайту зазвичай сесія вже запущена через авторизацію користувача. В іншому випадку запустіть сесію методом `Nette\Http\Session::start()`. - - -Однакова форма в кількох презентерах -==================================== - -Якщо вам потрібно використовувати одну й ту ж форму в кількох презентерах, рекомендуємо створити для неї фабрику, яку потім передасте до презентера. Підходящим місцем для такого класу є, наприклад, директорія `app/Forms`. - -Клас фабрики може виглядати приблизно так: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Ім\'я:'); - $form->addSubmit('send', 'Увійти'); - return $form; - } -} -``` - -Ми просимо клас створити форму у фабричному методі для компонентів у презентері: - -```php -public function __construct( - private SignInFormFactory $formFactory, -) { -} - -protected function createComponentSignInForm(): Form -{ - $form = $this->formFactory->create(); - // ми можемо змінити форму, тут, наприклад, змінюємо напис на кнопці - $form['send']->setCaption('Продовжити'); - $form->onSuccess[] = [$this, 'signInFormSuceeded']; // і додаємо обробник - return $form; -} -``` - -Обробник для обробки форми також може бути наданий вже з фабрики: - -```php -use Nette\Application\UI\Form; - -class SignInFormFactory -{ - public function create(): Form - { - $form = new Form; - $form->addText('name', 'Ім\'я:'); - $form->addSubmit('send', 'Увійти'); - $form->onSuccess[] = function (Form $form, $data): void { - // тут ми виконуємо обробку форми - }; - return $form; - } -} -``` - -Отже, ми пройшли швидкий вступ до форм у Nette. Спробуйте ще заглянути в директорію [examples |https://github.com/nette/forms/tree/master/examples] в дистрибутиві, де знайдете більше натхнення. diff --git a/forms/uk/rendering.texy b/forms/uk/rendering.texy deleted file mode 100644 index d6c1287888..0000000000 --- a/forms/uk/rendering.texy +++ /dev/null @@ -1,592 +0,0 @@ -Відображення форм -***************** - -Зовнішній вигляд форм може бути дуже різноманітним. На практиці ми можемо зіткнутися з двома крайнощами. З одного боку, існує потреба відображати в додатку низку форм, які візуально схожі одна на одну, як дві краплі води, і ми оцінимо легкість відображення без шаблону за допомогою `$form->render()`. Зазвичай це стосується адміністративних інтерфейсів. - -З іншого боку, існують різноманітні форми, де кожна форма є оригінальною. Їхній вигляд найкраще описувати мовою HTML у шаблоні форми. І, звісно, крім обох згаданих крайнощів, ми зустрінемо безліч форм, які знаходяться десь посередині. - - -Відображення за допомогою Latte -=============================== - -[Система шаблонів Latte|latte:] суттєво полегшує відображення форм та їхніх елементів. Спочатку ми покажемо, як відображати форми вручну по окремих елементах, щоб отримати повний контроль над кодом. Пізніше ми покажемо, як таке відображення можна [автоматизувати |#Автоматичне відображення]. - -Ви можете згенерувати дизайн шаблону форми Latte за допомогою методу `Nette\Forms\Blueprint::latte($form)`, який виведе його на сторінку браузера. Потім достатньо клацнути, щоб виділити код, і скопіювати його до вашого проєкту. .{data-version:3.1.15} - - -`{control}` ------------ - -Найпростіший спосіб відобразити форму — написати в шаблоні: - -```latte -{control signInForm} -``` - -Вплинути на вигляд так відображеної форми можна за допомогою конфігурації [#Renderer] та [окремих елементів |#HTML атрибути]. - - -`n:name` --------- - -Визначення форми в PHP-коді можна надзвичайно легко пов'язати з HTML-кодом. Достатньо лише додати атрибути `n:name`. Це так просто! - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - $form->addText('username')->setRequired(); - $form->addPassword('password')->setRequired(); - $form->addSubmit('send'); - return $form; -} -``` - -```latte -<form n:name=signInForm class=form> - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Вигляд кінцевого HTML-коду повністю у ваших руках. Якщо ви використовуєте атрибут `n:name` для елементів `<select>`, `<button>` або `<textarea>`, їхній внутрішній вміст автоматично заповнюється. Тег `<form n:name>` також створює локальну змінну `$form` з об'єктом відображуваної форми, а закриваючий тег `</form>` відображає всі невідображені приховані елементи (те саме стосується `{form} ... {/form}`). - -Однак не можна забувати про відображення можливих повідомлень про помилки. Як тих, що були додані до окремих елементів за допомогою методу `addError()` (за допомогою `{inputError}`), так і тих, що були додані безпосередньо до форми (повертаються методом `$form->getOwnErrors()`): - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - <label n:name=username>Username: <input n:name=username size=20 autofocus></label> - <span class=error n:ifcontent>{inputError username}</span> - </div> - <div> - <label n:name=password>Password: <input n:name=password></label> - <span class=error n:ifcontent>{inputError password}</span> - </div> - <div> - <input n:name=send class="btn btn-default"> - </div> -</form> -``` - -Складніші елементи форми, такі як RadioList або CheckboxList, можна таким чином відображати по окремих пунктах: - -```latte -{foreach $form[gender]->getItems() as $key => $label} - <label n:name="gender:$key"><input n:name="gender:$key"> {$label}</label> -{/foreach} -``` - - -`{label}` `{input}` -------------------- - -Не хочете думати для кожного елемента, який HTML-елемент використовувати в шаблоні, чи то `<input>`, `<textarea>` тощо? Рішенням є універсальний тег `{input}`: - -```latte -<form n:name=signInForm class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div> - {label username}Username: {input username, size: 20, autofocus: true}{/label} - {inputError username} - </div> - <div> - {label password}Password: {input password}{/label} - {inputError password} - </div> - <div> - {input send, class: "btn btn-default"} - </div> -</form> -``` - -Якщо форма використовує перекладач, текст усередині тегів `{label}` буде перекладено. - -Навіть у цьому випадку складніші елементи форми, такі як RadioList або CheckboxList, можна відображати по окремих пунктах: - -```latte -{foreach $form[gender]->items as $key => $label} - {label gender:$key}{input gender:$key} {$label}{/label} -{/foreach} -``` - -Для відображення самого `<input>` в елементі Checkbox використовуйте `{input myCheckbox:}`. HTML-атрибути в цьому випадку завжди розділяйте комою `{input myCheckbox:, class: required}`. - - -`{inputError}` --------------- - -Виводить повідомлення про помилку для елемента форми, якщо воно є. Повідомлення зазвичай загортають у HTML-елемент для стилізації. Запобігти відображенню порожнього елемента, якщо повідомлення немає, можна елегантно за допомогою `n:ifcontent`: - -```latte -<span class=error n:ifcontent>{inputError $input}</span> -``` - -Наявність помилки можна перевірити методом `hasErrors()` і відповідно встановити клас для батьківського елемента: - -```latte -<div n:class="$form[username]->hasErrors() ? 'error'"> - {input username} - {inputError username} -</div> -``` - - -`{form}` --------- - -Теги `{form signInForm}...{/form}` є альтернативою до `<form n:name="signInForm">...</form>`. - - -Автоматичне відображення ------------------------- - -Завдяки тегам `{input}` і `{label}` ми можемо легко створити загальний шаблон для будь-якої форми. Він буде послідовно ітерувати та відображати всі її елементи, крім прихованих елементів, які відображаються автоматично при закритті форми тегом `</form>`. Назва відображуваної форми очікується у змінній `$form`. - -```latte -<form n:name=$form class=form> - <ul class="errors" n:ifcontent> - <li n:foreach="$form->getOwnErrors() as $error">{$error}</li> - </ul> - - <div n:foreach="$form->getControls() as $input" - n:if="$input->getOption(type) !== hidden"> - {label $input /} - {input $input} - {inputError $input} - </div> -</form> -``` - -Використані самозакривні парні теги `{label .../}` відображають мітки, що походять з визначення форми в PHP-коді. - -Цей загальний шаблон збережіть, наприклад, у файлі `basic-form.latte`, і для відображення форми достатньо його включити та передати назву (або екземпляр) форми в параметр `$form`: - -```latte -{include basic-form.latte, form: signInForm} -``` - -Якщо б ви хотіли під час відображення однієї конкретної форми втрутитися в її вигляд і, наприклад, один елемент відобразити інакше, то найпростішим шляхом є підготувати в шаблоні блоки, які можна буде потім перезаписати. Блоки можуть мати також [динамічні імена |latte:template-inheritance#Динамічні назви блоків], тому в них можна вставити й ім'я відображуваного елемента. Наприклад: - -```latte -... - {label $input /} - {block "input-{$input->name}"}{input $input}{/block} -... -``` - -Для елемента, наприклад, `username` таким чином виникне блок `input-username`, який можна легко перезаписати за допомогою тегу [{embed} |latte:template-inheritance#Успадкування одиниць embed]: - -```latte -{embed basic-form.latte, form: signInForm} - {block input-username} - <span class=important> - {include parent} - </span> - {/block} -{/embed} -``` - -Альтернативно, весь вміст шаблону `basic-form.latte` можна [визначити |latte:template-inheritance#Визначення] як блок, включно з параметром `$form`: - -```latte -{define basic-form, $form} - <form n:name=$form class=form> - ... - </form> -{/define} -``` - -Завдяки цьому його виклик буде трохи простішим: - -```latte -{embed basic-form, signInForm} - ... -{/embed} -``` - -При цьому блок достатньо імпортувати лише в одному місці, а саме на початку шаблону layout: - -```latte -{import basic-form.latte} -``` - - -Спеціальні випадки ------------------- - -Якщо потрібно відобразити лише внутрішню частину форми без HTML-тегів `<form>`, наприклад, при надсиланні сніпетів, приховайте їх за допомогою атрибута `n:tag-if`: - -```latte -<form n:name=signInForm n:tag-if=false> - <div> - <label n:name=username>Username: <input n:name=username></label> - {inputError username} - </div> -</form> -``` - -З відображенням елементів усередині контейнера форми допоможе тег `{formContainer}`. - -```latte -<p>Які новини ви бажаєте отримувати:</p> - -{formContainer emailNews} -<ul> - <li>{input sport} {label sport /}</li> - <li>{input science} {label science /}</li> -</ul> -{/formContainer} -``` - - -Відображення без Latte -====================== - -Найпростіший спосіб відобразити форму — викликати: - -```php -$form->render(); -``` - -Вплинути на вигляд так відображеної форми можна за допомогою конфігурації [#Renderer] та [окремих елементів |#HTML атрибути]. - - -Ручне відображення ------------------- - -Кожен елемент форми має методи, які генерують HTML-код поля форми та мітки. Вони можуть повертати його або як рядок, або як об'єкт [Nette\Utils\Html|utils:html-elements]: - -- `getControl(): Html|string` повертає HTML-код елемента -- `getLabel($caption = null): Html|string|null` повертає HTML-код мітки, якщо вона існує - -Таким чином, форму можна відображати по окремих елементах: - -```php -<?php $form->render('begin') ?> -<?php $form->render('errors') ?> - -<div> - <?= $form['name']->getLabel() ?> - <?= $form['name']->getControl() ?> - <span class=error><?= htmlspecialchars($form['name']->getError()) ?></span> -</div> - -<div> - <?= $form['age']->getLabel() ?> - <?= $form['age']->getControl() ?> - <span class=error><?= htmlspecialchars($form['age']->getError()) ?></span> -</div> - -// ... - -<?php $form->render('end') ?> -``` - -У той час як для деяких елементів `getControl()` повертає єдиний HTML-елемент (наприклад, `<input>`, `<select>` тощо), для інших — цілий шматок HTML-коду (CheckboxList, RadioList). У такому випадку ви можете використовувати методи, які генерують окремі інпути та мітки для кожного пункту окремо: - -- `getControlPart($key = null): ?Html` повертає HTML-код одного пункту -- `getLabelPart($key = null): ?Html` повертає HTML-код мітки одного пункту - -.[note] -Ці методи з історичних причин мають префікс `get`, але краще було б `generate`, оскільки при кожному виклику вони створюють і повертають новий елемент `Html`. - - -Renderer -======== - -Це об'єкт, що забезпечує відображення форми. Його можна встановити за допомогою методу `$form->setRenderer`. Йому передається управління при виклику методу `$form->render()`. - -Якщо ми не встановимо власний рендерер, буде використано стандартний рендерер [api:Nette\Forms\Rendering\DefaultFormRenderer]. Він відображає елементи форми у вигляді HTML-таблиці. Вивід виглядає так: - -```latte -<table> -<tr class="required"> - <th><label class="required" for="frm-name">Ім'я:</label></th> - - <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">Вік:</label></th> - - <td><input type="text" class="text" name="age" id="frm-age" required value=""></td> -</tr> - -<tr> - <th><label>Стать:</label></th> - ... -``` - -Використовувати чи не використовувати таблицю для каркасу форми — питання спірне, і багато вебдизайнерів віддають перевагу іншій розмітці. Наприклад, списку визначень. Тому ми переконфігуруємо `DefaultFormRenderer` так, щоб він відображав форму у вигляді списку. Конфігурація здійснюється редагуванням масиву [$wrappers |api:Nette\Forms\Rendering\DefaultFormRenderer::$wrappers]. Перший індекс завжди представляє область, а другий — її атрибут. Окремі області зображені на малюнку: - -[* defaultformrenderer.webp *] - -Стандартно група елементів `controls` обгортається таблицею `<table>`, кожен `pair` представляє рядок таблиці `<tr>`, а пара `label` і `control` є комірками `<th>` і `<td>`. Тепер ми змінимо обгортаючі елементи. Область `controls` вставимо в контейнер `<dl>`, область `pair` залишимо без контейнера, `label` вставимо в `<dt>` і, нарешті, `control` обгорнемо тегами `<dd>`: - -```php -$renderer = $form->getRenderer(); -$renderer->wrappers['controls']['container'] = 'dl'; -$renderer->wrappers['pair']['container'] = null; -$renderer->wrappers['label']['container'] = 'dt'; -$renderer->wrappers['control']['container'] = 'dd'; - -$form->render(); -``` - -Результатом є такий HTML-код: - -```latte -<dl> - <dt><label class="required" for="frm-name">Ім'я:</label></dt> - - <dd><input type="text" class="text" name="name" id="frm-name" required value=""></dd> - - - <dt><label class="required" for="frm-age">Вік:</label></dt> - - <dd><input type="text" class="text" name="age" id="frm-age" required value=""></dd> - - - <dt><label>Стать:</label></dt> - ... -</dl> -``` - -У масиві wrappers можна вплинути на цілу низку інших атрибутів: - -- додавати CSS-класи окремим типам елементів форми -- розрізняти CSS-класом парні та непарні рядки -- візуально розрізняти обов'язкові та необов'язкові елементи -- визначати, чи відображатимуться повідомлення про помилки безпосередньо біля елементів чи над формою - - -Options -------- - -Поведінку Renderer можна контролювати також встановленням *options* на окремих елементах форми. Таким чином можна встановити опис, який буде виведений поруч із полем введення: - -```php -$form->addText('phone', 'Номер:') - ->setOption('description', 'Цей номер залишиться прихованим'); -``` - -Якщо ми хочемо розмістити в ньому HTML-вміст, використаємо клас [Html |utils:html-elements] - -```php -use Nette\Utils\Html; - -$form->addText('phone', 'Номер:') - ->setOption('description', Html::el('p') - ->setHtml('<a href="...">Умови зберігання Вашого номера</a>') - ); -``` - -.[tip] -Елемент Html можна використовувати також замість мітки: `$form->addCheckbox('conditions', $label)`. - - -Групування елементів --------------------- - -Renderer дозволяє групувати елементи у візуальні групи (fieldset): - -```php -$form->addGroup('Особисті дані'); -``` - -Після створення нової групи вона стає активною, і кожен новододаний елемент одночасно додається і до неї. Тож форму можна будувати таким чином: - -```php -$form = new Form; -$form->addGroup('Особисті дані'); -$form->addText('name', 'Ваше ім\'я:'); -$form->addInteger('age', 'Ваш вік:'); -$form->addEmail('email', 'Email:'); - -$form->addGroup('Адреса доставки'); -$form->addCheckbox('send', 'Надіслати на адресу'); -$form->addText('street', 'Вулиця:'); -$form->addText('city', 'Місто:'); -$form->addSelect('country', 'Країна:', $countries); -``` - -Renderer спочатку відображає групи, а потім елементи, які не належать до жодної групи. - - -Підтримка Bootstrap -------------------- - -[У прикладах |https://github.com/nette/forms/tree/master/examples] ви знайдете приклади, як налаштувати Renderer для [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] та [Bootstrap 4 |https://github.com/nette/forms/blob/96b3e90/examples/bootstrap4-rendering.php] - - -HTML атрибути -============= - -Для встановлення будь-яких HTML-атрибутів елементів форми використовуємо метод `setHtmlAttribute(string $name, $value = true)`: - -```php -$form->addInteger('number', 'Число:') - ->setHtmlAttribute('class', 'big-number'); - -$form->addSelect('rank', 'Сортувати за:', ['ціною', 'назвою']) - ->setHtmlAttribute('onchange', 'submit()'); // при зміні надіслати - - -// Для встановлення атрибутів самого <form> -$form->setHtmlAttribute('id', 'myForm'); -``` - -Специфікація типу елемента: - -```php -$form->addText('tel', 'Ваш телефон:') - ->setHtmlType('tel') - ->setHtmlAttribute('placeholder', 'напишіть телефон'); -``` - -.[warning] -Встановлення типу та інших атрибутів служить лише для візуальних цілей. Перевірка правильності введення має відбуватися на сервері, що забезпечується вибором відповідного [елемента форми|controls] та зазначенням [правил валідації|validation]. - -Окремим пунктам у списках radio або checkbox ми можемо встановити HTML-атрибут з різними значеннями для кожного з них. Зверніть увагу на двокрапку після `style:`, яка забезпечує вибір значення за ключем: - -```php -$colors = ['r' => 'червоний', 'g' => 'зелений', 'b' => 'синій']; -$styles = ['r' => 'background:red', 'g' => 'background:green']; -$form->addCheckboxList('colors', 'Кольори:', $colors) - ->setHtmlAttribute('style:', $styles); -``` - -Виведе: - -```latte -<label><input type="checkbox" name="colors[]" style="background:red" value="r">червоний</label> -<label><input type="checkbox" name="colors[]" style="background:green" value="g">зелений</label> -<label><input type="checkbox" name="colors[]" value="b">синій</label> -``` - -Для встановлення логічних атрибутів, таких як `readonly`, ми можемо використовувати запис зі знаком питання: - -```php -$form->addCheckboxList('colors', 'Кольори:', $colors) - ->setHtmlAttribute('readonly?', 'r'); // для кількох ключів використовуйте масив, напр. ['r', 'g'] -``` - -Виведе: - -```latte -<label><input type="checkbox" name="colors[]" readonly value="r">червоний</label> -<label><input type="checkbox" name="colors[]" value="g">зелений</label> -<label><input type="checkbox" name="colors[]" value="b">синій</label> -``` - -У випадку selectbox метод `setHtmlAttribute()` встановлює атрибути елемента `<select>`. Якщо ми хочемо встановити атрибути окремим `<option>`, використовуємо метод `setOptionAttribute()`. Також працюють записи з двокрапкою та знаком питання, зазначені вище: - -```php -$form->addSelect('colors', 'Кольори:', $colors) - ->setOptionAttribute('style:', $styles); -``` - -Виведе: - -```latte -<select name="colors"> - <option value="r" style="background:red">червоний</option> - <option value="g" style="background:green">зелений</option> - <option value="b">синій</option> -</select> -``` - - -Прототипи ---------- - -Альтернативний спосіб встановлення HTML-атрибутів полягає в модифікації шаблону, з якого генерується HTML-елемент. Шаблоном є об'єкт `Html`, і його повертає метод `getControlPrototype()`: - -```php -$input = $form->addInteger('number', 'Число:'); -$html = $input->getControlPrototype(); // <input> -$html->class('big-number'); // <input class="big-number"> -``` - -Таким чином можна модифікувати й шаблон мітки, який повертає `getLabelPrototype()`: - -```php -$html = $input->getLabelPrototype(); // <label> -$html->class('distinctive'); // <label class="distinctive"> -``` - -Для елементів Checkbox, CheckboxList та RadioList ви можете вплинути на шаблон елемента, який обгортає весь елемент. Його повертає `getContainerPrototype()`. За замовчуванням це «порожній» елемент, тому нічого не відображається, але якщо ми встановимо йому назву, він буде відображатися: - -```php -$input = $form->addCheckbox('send'); -$html = $input->getContainerPrototype(); -$html->setName('div'); // <div> -$html->class('check'); // <div class="check"> -echo $input->getControl(); -// <div class="check"><label><input type="checkbox" name="send"></label></div> -``` - -У випадку CheckboxList та RadioList можна також вплинути на шаблон роздільника окремих пунктів, який повертає метод `getSeparatorPrototype()`. За замовчуванням це елемент `<br>`. Якщо ви зміните його на парний елемент, він буде обгортати окремі пункти замість того, щоб розділяти їх. А також можна вплинути на шаблон HTML-елемента мітки біля окремих пунктів, який повертає `getItemLabelPrototype()`. - - -Переклад -======== - -Якщо ви програмуєте багатомовний додаток, вам, ймовірно, знадобиться відображати форму різними мовними версіями. Nette Framework для цієї мети визначає інтерфейс для перекладу [api:Nette\Localization\Translator]. У Nette немає стандартної реалізації, ви можете вибрати відповідно до своїх потреб з кількох готових рішень, які знайдете на [Componette |https://componette.org/search/localization]. У їхній документації ви дізнаєтеся, як конфігурувати перекладач. - -Форми підтримують виведення текстів через перекладач. Ми передаємо його їм за допомогою методу `setTranslator()`: - -```php -$form->setTranslator($translator); -``` - -З цього моменту не тільки всі мітки, але й усі повідомлення про помилки або пункти select box перекладаються іншою мовою. - -Для окремих елементів форми при цьому можна встановити інший перекладач або повністю вимкнути переклад значенням `null`: - -```php -$form->addSelect('carModel', 'Модель:', $cars) - ->setTranslator(null); -``` - -Для [правил валідації|validation] перекладачу передаються також специфічні параметри, наприклад, для правила: - -```php -$form->addPassword('password', 'Пароль:') - ->addRule($form::MinLength, 'Пароль повинен мати щонайменше %d символів', 8); -``` - -викликається перекладач з такими параметрами: - -```php -$translator->translate('Пароль повинен мати щонайменше %d символів', 8); -``` - -і таким чином може вибрати правильну форму множини для слова `символів` залежно від кількості. - - -Подія onRender -============== - -Безпосередньо перед тим, як форма буде відображена, ми можемо викликати наш код. Він може, наприклад, додати елементам форми HTML-класи для правильного відображення. Код додаємо до масиву `onRender`: - -```php -$form->onRender[] = function ($form) { - BootstrapCSS::initialize($form); -}; -``` diff --git a/forms/uk/standalone.texy b/forms/uk/standalone.texy deleted file mode 100644 index 2c3f33bcf5..0000000000 --- a/forms/uk/standalone.texy +++ /dev/null @@ -1,317 +0,0 @@ -Форми, що використовуються окремо -********************************* - -.[perex] -Nette Forms значно полегшують створення та обробку веб-форм. Ви можете використовувати їх у своїх програмах абсолютно окремо від решти фреймворку, що ми покажемо в цьому розділі. - -Однак, якщо ви використовуєте Nette Application та презентери, для вас призначений посібник для [використання в презентерах|in-presenter]. - - -Перша форма -=========== - -Спробуємо написати просту реєстраційну форму. Її код буде таким ("повний код":https://gist.github.com/dg/57878c1a413ae8ef0c1d83f02c43ef3f): - -```php -use Nette\Forms\Form; - -$form = new Form; -$form->addText('name', 'Ім\'я:'); -$form->addPassword('password', 'Пароль:'); -$form->addSubmit('send', 'Зареєструватися'); -``` - -Дуже легко її відобразимо: - -```php -$form->render(); -``` - -і в браузері вона зобразиться так: - -[* form-cs.webp *] - -Форма — це об'єкт класу `Nette\Forms\Form` (клас `Nette\Application\UI\Form` використовується в презентерах). Ми додали до неї так звані елементи: ім'я, пароль та кнопку відправки. - -А тепер оживимо форму. За допомогою запиту `$form->isSuccess()` ми дізнаємося, чи була форма відправлена і чи була вона заповнена валідно. Якщо так, виведемо дані. Отже, після визначення форми доповнимо: - -```php -if ($form->isSuccess()) { - echo 'Форма була правильно заповнена та відправлена'; - $data = $form->getValues(); - // $data->name містить ім'я - // $data->password містить пароль - var_dump($data); -} -``` - -Метод `getValues()` повертає відправлені дані у вигляді об'єкта [ArrayHash |utils:arrays#ArrayHash]. Як це змінити, ми покажемо [пізніше |#Мапінг на класи]. Об'єкт `$data` містить ключі `name` та `password` з даними, які ввів користувач. - -Зазвичай дані одразу надсилаються для подальшої обробки, наприклад, вставки в базу даних. Однак під час обробки може виникнути помилка, наприклад, ім'я користувача вже зайняте. У такому випадку ми передаємо помилку назад у форму за допомогою `addError()` і дозволяємо їй відобразитися знову, вже з повідомленням про помилку. - -```php -$form->addError('Вибачте, це ім\'я користувача вже використовується.'); -``` - -Після обробки форми перенаправимо на наступну сторінку. Це запобігає небажаному повторному відправленню форми кнопкою *оновити*, *назад* або рухом в історії браузера. - -Форма стандартно надсилається методом POST на ту саму сторінку. Обидва параметри можна змінити: - -```php -$form->setAction('/submit.php'); -$form->setMethod('GET'); -``` - -І це, власне, все :-) Ми маємо функціональну та ідеально [захищену |#Захист від вразливостей] форму. - -Спробуйте додати й інші [елементи форми|controls]. - - -Доступ до елементів -=================== - -Форму та її окремі елементи ми називаємо компонентами. Вони утворюють дерево компонентів, де коренем є саме форма. До окремих елементів форми можна отримати доступ таким чином: - -```php -$input = $form->getComponent('name'); -// альтернативний синтаксис: $input = $form['name']; - -$button = $form->getComponent('send'); -// альтернативний синтаксис: $button = $form['send']; -``` - -Елементи видаляються за допомогою unset: - -```php -unset($form['name']); -``` - - -Правила валідації -================= - -Тут прозвучало слово *валідна*, але форма поки що не має жодних правил валідації. Давайте це виправимо. - -Ім'я буде обов'язковим, тому позначимо його методом `setRequired()`, аргументом якого є текст повідомлення про помилку, яке відобразиться, якщо користувач не введе ім'я. Якщо аргумент не вказано, використовується стандартне повідомлення про помилку. - -```php -$form->addText('name', 'Ім\'я:') - ->setRequired('Будь ласка, введіть ім\'я'); -``` - -Спробуйте відправити форму без заповненого імені, і ви побачите, що з'явиться повідомлення про помилку, а браузер чи сервер відхилятимуть її доти, доки ви не заповните поле. - -Водночас ви не обдурите систему, написавши в полі, наприклад, лише пробіли. Ні. Nette автоматично видаляє пробіли зліва та справа. Спробуйте самі. Це те, що ви завжди повинні робити з кожним однорядковим полем введення, але часто про це забувають. Nette робить це автоматично. (Можете спробувати обдурити форму і надіслати як ім'я багаторядковий рядок. Навіть тут Nette не дасть себе обдурити і змінить переноси рядків на пробіли.) - -Форма завжди валідується на стороні сервера, але також генерується JavaScript валідація, яка відбувається миттєво, і користувач дізнається про помилку одразу, без необхідності надсилати форму на сервер. За це відповідає скрипт `netteForms.js`. Вставте його на сторінку: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Якщо ви подивитеся на вихідний код сторінки з формою, ви помітите, що Nette вставляє обов'язкові елементи в елементи з CSS-класом `required`. Спробуйте додати до шаблону наступну таблицю стилів, і мітка «Ім'я» стане червоною. Таким чином, ми елегантно позначаємо для користувачів обов'язкові елементи: - -```latte -<style> -.required label { color: maroon } -</style> -``` - -Інші правила валідації додамо методом `addRule()`. Перший параметр — це правило, другий — знову текст повідомлення про помилку, а третім може бути аргумент правила валідації. Що це означає? - -Розширимо форму новим необов'язковим полем «вік», яке має бути цілим числом (`addInteger()`) і, крім того, у допустимому діапазоні (`$form::Range`). І саме тут ми використаємо третій параметр методу `addRule()`, яким передамо валідатору потрібний діапазон як пару `[від, до]`: - -```php -$form->addInteger('age', 'Вік:') - ->addRule($form::Range, 'Вік має бути від 18 до 120', [18, 120]); -``` - -.[tip] -Якщо користувач не заповнить поле, правила валідації не перевірятимуться, оскільки елемент є необов'язковим. - -Тут виникає простір для невеликого рефакторингу. У повідомленні про помилку та в третьому параметрі числа вказані дубльовано, що не ідеально. Якби ми створювали [багатомовні форми |rendering#Переклад] і повідомлення, що містить числа, було б перекладено кількома мовами, це ускладнило б можливу зміну значень. З цієї причини можна використовувати плейсхолдери `%d`, і Nette доповнить значення: - -```php - ->addRule($form::Range, 'Вік має бути від %d до %d років', [18, 120]); -``` - -Повернемося до елемента `password`, який також зробимо обов'язковим і ще перевіримо мінімальну довжину пароля (`$form::MinLength`), знову ж таки з використанням плейсхолдера: - -```php -$form->addPassword('password', 'Пароль:') - ->setRequired('Виберіть пароль') - ->addRule($form::MinLength, 'Пароль повинен мати щонайменше %d символів', 8); -``` - -Додамо до форми ще поле `passwordVerify`, де користувач введе пароль ще раз для перевірки. За допомогою правил валідації перевіримо, чи обидва паролі однакові (`$form::Equal`). А як параметр дамо посилання на перший пароль за допомогою [квадратних дужок |#Доступ до елементів]: - -```php -$form->addPassword('passwordVerify', 'Пароль для перевірки:') - ->setRequired('Будь ласка, введіть пароль ще раз для перевірки') - ->addRule($form::Equal, 'Паролі не співпадають', $form['password']) - ->setOmitted(); -``` - -За допомогою `setOmitted()` ми позначили елемент, значення якого насправді не має значення і який існує лише для валідації. Значення не передається до `$data`. - -Таким чином, ми маємо готову повнофункціональну форму з валідацією в PHP та JavaScript. Можливості валідації Nette набагато ширші, можна створювати умови, дозволяти на їх основі показувати та приховувати частини сторінки тощо. Все це ви дізнаєтеся в розділі про [валідацію форм|validation]. - - -Значення за замовчуванням -========================= - -Елементам форми зазвичай встановлюємо значення за замовчуванням: - -```php -$form->addEmail('email', 'E-mail') - ->setDefaultValue($lastUsedEmail); -``` - -Часто буває зручно встановити значення за замовчуванням для всіх елементів одночасно. Наприклад, коли форма служить для редагування записів. Читаємо запис з бази даних і встановлюємо значення за замовчуванням: - -```php -//$row = ['name' => 'John', 'age' => '33', /* ... */]; -$form->setDefaults($row); -``` - -Викликайте `setDefaults()` після визначення елементів. - - -Відображення форми -================== - -Стандартно форма відображається як таблиця. Окремі елементи відповідають основному правилу доступності — всі мітки записані як `<label>` і пов'язані з відповідним елементом форми. При кліку на мітку курсор автоматично з'являється у полі форми. - -Кожному елементу ми можемо встановлювати будь-які HTML-атрибути. Наприклад, додати placeholder: - -```php -$form->addInteger('age', 'Вік:') - ->setHtmlAttribute('placeholder', 'Будь ласка, заповніть вік'); -``` - -Способів відображення форми є справді багато, тому цьому присвячено [окремий розділ про відображення|rendering]. - - -Мапінг на класи -=============== - -Повернемося до обробки даних форми. Метод `getValues()` повертав нам відправлені дані як об'єкт `ArrayHash`. Оскільки це загальний клас, щось на зразок `stdClass`, при роботі з ним нам бракуватиме певного комфорту, наприклад, автодоповнення властивостей у редакторах або статичного аналізу коду. Це можна було б вирішити, маючи для кожної форми конкретний клас, властивості якого представляють окремі елементи. Наприклад: - -```php -class RegistrationFormData -{ - public string $name; - public ?int $age; - public string $password; -} -``` - -Альтернативно, ви можете використовувати конструктор: - -```php -class RegistrationFormData -{ - public function __construct( - public string $name, - public ?int $age, - public string $password, - ) { - } -} -``` - -Властивості класу даних також можуть бути enum-ами і будуть автоматично зіставлені. .{data-version:3.2.4} - -Як сказати Nette, щоб він повертав нам дані як об'єкти цього класу? Легше, ніж ви думаєте. Достатньо лише вказати назву класу або об'єкт для гідратації як параметр: - -```php -$data = $form->getValues(RegistrationFormData::class); -$name = $data->name; -``` - -Як параметр можна також вказати `'array'`, і тоді дані повернуться як масив. - -Якщо форми утворюють багаторівневу структуру, що складається з контейнерів, створіть для кожного окремий клас: - -```php -$form = new Form; -$person = $form->addContainer('person'); -$person->addText('firstName'); -/* ... */ - -class PersonFormData -{ - public string $firstName; - public string $lastName; -} - -class RegistrationFormData -{ - public PersonFormData $person; - public ?int $age; - public string $password; -} -``` - -Мапінг потім з типу властивості `$person` дізнається, що контейнер потрібно зіставити з класом `PersonFormData`. Якщо властивість містить масив контейнерів, вкажіть тип `array` і передайте клас для мапінгу безпосередньо контейнеру: - -```php -$person->setMappedType(PersonFormData::class); -``` - -Проект класу даних форми можна згенерувати за допомогою методу `Nette\Forms\Blueprint::dataClass($form)`, який виведе його на сторінку браузера. Потім код достатньо виділити кліком і скопіювати в проект. .{data-version:3.1.15} - - -Кілька кнопок -============= - -Якщо форма має більше однієї кнопки, зазвичай потрібно розрізнити, яка з них була натиснута. Цю інформацію нам поверне метод `isSubmittedBy()` кнопки: - -```php -$form->addSubmit('save', 'Зберегти'); -$form->addSubmit('delete', 'Видалити'); - -if ($form->isSuccess()) { - if ($form['save']->isSubmittedBy()) { - // ... - } - - if ($form['delete']->isSubmittedBy()) { - // ... - } -} -``` - -Не пропускайте запит `$form->isSuccess()`, він перевірить валідність даних. - -Коли форма надсилається кнопкою <kbd>Enter</kbd>, це вважається так, ніби вона була надіслана першою кнопкою. - - -Захист від вразливостей -======================= - -Nette Framework приділяє велику увагу безпеці, тому ретельно дбає про надійний захист форм. - -Крім того, що форми захищають від атак [Cross Site Scripting (XSS) |nette:glossary#Cross-Site Scripting XSS] та [Cross-Site Request Forgery (CSRF) |nette:glossary#Cross-Site Request Forgery CSRF], він виконує багато дрібних заходів безпеки, про які вам вже не потрібно думати. - -Наприклад, він фільтрує всі керуючі символи з вхідних даних і перевіряє валідність кодування UTF-8, тому дані з форми завжди будуть чистими. У select box-ах та radio list-ах він перевіряє, чи вибрані елементи дійсно були з запропонованих і чи не відбулася підробка. Ми вже згадували, що для однорядкових текстових полів він видаляє символи кінця рядка, які міг надіслати зловмисник. Для багаторядкових полів він нормалізує символи кінця рядка. І так далі. - -Nette вирішує за вас ризики безпеки, про існування яких багато програмістів навіть не здогадуються. - -Згадана атака CSRF полягає в тому, що зловмисник заманює жертву на сторінку, яка непомітно в браузері жертви виконує запит на сервер, на якому жертва залогінена, і сервер вважає, що запит виконала жертва за власним бажанням. Тому Nette запобігає надсиланню POST-форми з іншого домену. Якщо з якоїсь причини ви хочете вимкнути захист і дозволити надсилати форму з іншого домену, використовуйте: - -```php -$form->allowCrossOrigin(); // УВАГА! Вимикає захист! -``` - -Цей захист використовує SameSite cookie з назвою `_nss`. Тому створюйте об'єкт форми ще до надсилання першого виводу, щоб можна було надіслати cookie. - -Захист за допомогою SameSite cookie може бути не 100% надійним, тому рекомендується увімкнути ще захист за допомогою токена: - -```php -$form->addProtection(); -``` - -Рекомендуємо так захищати форми в адміністративній частині сайту, які змінюють чутливі дані в програмі. Фреймворк захищається від атаки CSRF шляхом генерації та перевірки авторизаційного токена, який зберігається в сесії. Тому необхідно перед відображенням форми мати відкриту сесію. В адміністративній частині сайту зазвичай сесія вже запущена через вхід користувача. В іншому випадку запустіть сесію методом `Nette\Http\Session::start()`. - -Отже, ми пройшли швидкий вступ до форм у Nette. Спробуйте ще заглянути в каталог [examples|https://github.com/nette/forms/tree/master/examples] у дистрибутиві, де ви знайдете більше натхнення. diff --git a/forms/uk/validation.texy b/forms/uk/validation.texy deleted file mode 100644 index 1c2bc94484..0000000000 --- a/forms/uk/validation.texy +++ /dev/null @@ -1,376 +0,0 @@ -Валідація форм -************** - - -Обов'язкові елементи -==================== - -Обов'язкові елементи позначаємо методом `setRequired()`, аргументом якого є текст [#Повідомлення про помилки], який відобразиться, якщо користувач не заповнить елемент. Якщо аргумент не вказано, використовується стандартне повідомлення про помилку. - -```php -$form->addText('name', 'Ім\'я:') - ->setRequired('Будь ласка, введіть ім\'я'); -``` - - -Правила -======= - -Правила валідації додаємо до елементів методом `addRule()`. Перший параметр — це правило, другий — текст [#Повідомлення про помилки], а третій — аргумент правила валідації. - -```php -$form->addPassword('password', 'Пароль:') - ->addRule($form::MinLength, 'Пароль повинен мати щонайменше %d символів', 8); -``` - -**Правила валідації перевіряються лише в тому випадку, якщо користувач заповнив елемент.** - -Nette постачається з цілою низкою передбачених правил, назви яких є константами класу `Nette\Forms\Form`. Для всіх елементів ми можемо використовувати ці правила: - -| константа | опис | тип аргументу -|------- -| `Required` | обов'язковий елемент, псевдонім для `setRequired()` | - -| `Filled` | обов'язковий елемент, псевдонім для `setRequired()` | - -| `Blank` | елемент не повинен бути заповнений | - -| `Equal` | значення дорівнює параметру | `mixed` -| `NotEqual` | значення не дорівнює параметру | `mixed` -| `IsIn` | значення дорівнює одному з елементів у масиві | `array` -| `IsNotIn` | значення не дорівнює жодному з елементів у масиві | `array` -| `Valid` | чи елемент заповнений правильно? (для [#Умови]) | - - - -Текстові поля -------------- - -Для елементів `addText()`, `addPassword()`, `addTextArea()`, `addEmail()`, `addInteger()`, `addFloat()` можна також використовувати деякі з наступних правил: - -| `MinLength` | мінімальна довжина тексту | `int` -| `MaxLength` | максимальна довжина тексту | `int` -| `Length` | довжина в діапазоні або точна довжина | пара `[int, int]` або `int` -| `Email` | дійсна електронна адреса | - -| `URL` | абсолютний URL | - -| `Pattern` | відповідає регулярному виразу | `string` -| `PatternInsensitive` | як `Pattern`, але нечутливий до регістру | `string` -| `Integer` | цілочисельне значення | - -| `Numeric` | псевдонім для `Integer` | - -| `Float` | число | - -| `Min` | мінімальне значення числового елемента | `int\|float` -| `Max` | максимальне значення числового елемента | `int\|float` -| `Range` | значення в діапазоні | пара `[int\|float, int\|float]` - -Правила валідації `Integer`, `Numeric` та `Float` одразу перетворюють значення на integer відповідно float. Крім того, правило `URL` приймає також адресу без схеми (наприклад, `nette.org`) і доповнює схему (`https://nette.org`). Вираз у `Pattern` та `PatternIcase` повинен відповідати всьому значенню, тобто ніби він був обгорнутий символами `^` та `$`. - - -Кількість елементів -------------------- - -Для елементів `addMultiUpload()`, `addCheckboxList()`, `addMultiSelect()` можна також використовувати наступні правила для обмеження кількості вибраних елементів або завантажених файлів: - -| `MinLength` | мінімальна кількість | `int` -| `MaxLength` | максимальна кількість | `int` -| `Length` | кількість у діапазоні або точна кількість | пара `[int, int]` або `int` - - -Завантаження файлів -------------------- - -Для елементів `addUpload()`, `addMultiUpload()` можна також використовувати наступні правила: - -| `MaxFileSize` | максимальний розмір файлу в байтах | `int` -| `MimeType` | MIME-тип, дозволені плейсхолдери (`'video/*'`) | `string\|string[]` -| `Image` | зображення JPEG, PNG, GIF, WebP, AVIF | - -| `Pattern` | ім'я файлу відповідає регулярному виразу | `string` -| `PatternInsensitive` | як `Pattern`, але нечутливий до регістру | `string` - -`MimeType` та `Image` вимагають PHP-розширення `fileinfo`. Те, що файл чи зображення є потрібного типу, визначається на основі його сигнатури, і **не перевіряється цілісність усього файлу.** Чи не пошкоджене зображення, можна з'ясувати, наприклад, спробувавши його [завантажити |http:request#toImage]. - - -Повідомлення про помилки -======================== - -Усі передбачені правила, за винятком `Pattern` та `PatternInsensitive`, мають стандартне повідомлення про помилку, тому його можна пропустити. Однак, вказавши та сформулювавши всі повідомлення індивідуально, ви зробите форму більш зручною для користувача. - -Змінити стандартні повідомлення можна в [конфігурації|forms:configuration], змінивши тексти в масиві `Nette\Forms\Validator::$messages` або використовуючи [перекладач |rendering#Переклад]. - -У тексті повідомлень про помилки можна використовувати ці рядки-заповнювачі: - -| `%d` | замінюється послідовно на аргументи правила -| `%n$d` | замінюється на n-й аргумент правила -| `%label` | замінюється на мітку елемента (без двокрапки) -| `%name` | замінюється на ім'я елемента (наприклад, `name`) -| `%value` | замінюється на значення, введене користувачем - -```php -$form->addText('name', 'Ім\'я:') - ->setRequired('Заповніть, будь ласка, %label'); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'щонайменше %d і щонайбільше %d', [5, 10]); - -$form->addInteger('id', 'ID:') - ->addRule($form::Range, 'щонайбільше %2$d і щонайменше %1$d', [5, 10]); -``` - - -Умови -===== - -Крім правил, можна додавати також умови. Вони записуються подібно до правил, тільки замість `addRule()` використовуємо метод `addCondition()` і, зрозуміло, не вказуємо жодного повідомлення про помилку (умова лише запитує): - -```php -$form->addPassword('password', 'Пароль:') - // якщо пароль не довший за 8 символів - ->addCondition($form::MaxLength, 8) - // тоді він повинен містити цифру - ->addRule($form::Pattern, 'Повинен містити цифру', '.*[0-9].*'); -``` - -Умову можна прив'язати і до іншого елемента, ніж поточний, за допомогою `addConditionOn()`. Як перший параметр вкажемо посилання на елемент. У цьому прикладі e-mail буде обов'язковим лише тоді, коли буде відмічено checkbox (його значення буде true): - -```php -$form->addCheckbox('newsletters', 'надсилайте мені розсилки'); - -$form->addEmail('email', 'E-mail:') - // якщо checkbox відмічено - ->addConditionOn($form['newsletters'], $form::Equal, true) - // тоді вимагай e-mail - ->setRequired('Введіть адресу електронної пошти'); -``` - -З умов можна створювати складні структури за допомогою `elseCondition()` та `endCondition()`: - -```php -$form->addText(/* ... */) - ->addCondition(/* ... */) // якщо виконана перша умова - ->addConditionOn(/* ... */) // і друга умова на іншому елементі - ->addRule(/* ... */) // вимагай це правило - ->elseCondition() // якщо друга умова не виконана - ->addRule(/* ... */) // вимагай ці правила - ->addRule(/* ... */) - ->endCondition() // повертаємося до першої умови - ->addRule(/* ... */); -``` - -У Nette можна дуже легко реагувати на виконання чи невиконання умови також на стороні JavaScript за допомогою методу `toggle()`, див. [#Динамічний JavaScript]. - - -Посилання на інший елемент -========================== - -Як аргумент правила чи умови можна передати й інший елемент форми. Правило тоді використає значення, введене пізніше користувачем у браузері. Таким чином можна, наприклад, динамічно валідувати, що елемент `password` містить той самий рядок, що й елемент `password_confirm`: - -```php -$form->addPassword('password', 'Пароль'); -$form->addPassword('password_confirm', 'Підтвердіть пароль') - ->addRule($form::Equal, 'Введені паролі не співпадають', $form['password']); -``` - - -Власні правила та умови -======================= - -Іноді ми потрапляємо в ситуацію, коли вбудованих правил валідації в Nette недостатньо, і нам потрібно валідувати дані від користувача по-своєму. У Nette це дуже просто! - -Методам `addRule()` чи `addCondition()` можна як перший параметр передати будь-який callback. Він приймає як перший параметр сам елемент і повертає булеве значення, що визначає, чи валідація пройшла успішно. При додаванні правила за допомогою `addRule()` можна вказати й інші аргументи, які потім передаються як другий параметр. - -Власний набір валідаторів ми можемо створити як клас зі статичними методами: - -```php -class MyValidators -{ - // перевіряє, чи значення ділиться на аргумент - public static function validateDivisibility(BaseControl $input, $arg): bool - { - return $input->getValue() % $arg === 0; - } - - public static function validateEmailDomain(BaseControl $input, $domain) - { - // інші валідатори - } -} -``` - -Використання тоді дуже просте: - -```php -$form->addInteger('num') - ->addRule( - [MyValidators::class, 'validateDivisibility'], - 'Значення має бути кратним числу %d', - 8, - ); -``` - -Власні правила валідації можна додавати і до JavaScript. Умовою є те, що правило має бути статичним методом. Його назва для JavaScript-валідатора утворюється шляхом об'єднання назви класу без зворотних слешів `\`, підкреслення `_` та назви методу. Наприклад, `App\MyValidators::validateDivisibility` запишемо як `AppMyValidators_validateDivisibility` і додамо до об'єкта `Nette.validators`: - -```js -Nette.validators['AppMyValidators_validateDivisibility'] = (elem, args, val) => { - return val % args === 0; -}; -``` - - -Подія onValidate -================ - -Після надсилання форми проводиться валідація, під час якої перевіряються окремі правила, додані за допомогою `addRule()`, а потім викликається [подія |nette:glossary#Події události] `onValidate`. Її обробник можна використовувати для додаткової валідації, зазвичай для перевірки правильної комбінації значень у кількох елементах форми. - -Якщо виявлено помилку, передаємо її до форми методом `addError()`. Його можна викликати або на конкретному елементі, або безпосередньо на формі. - -```php -protected function createComponentSignInForm(): Form -{ - $form = new Form; - // ... - $form->onValidate[] = [$this, 'validateSignInForm']; - return $form; -} - -public function validateSignInForm(Form $form, \stdClass $data): void -{ - if ($data->foo > 1 && $data->bar > 5) { - $form->addError('Ця комбінація неможлива.'); - } -} -``` - - -Помилки під час обробки -======================= - -У багатьох випадках про помилку ми дізнаємося лише тоді, коли обробляємо валідну форму, наприклад, записуємо новий елемент у базу даних і натрапляємо на дублювання ключів. У такому випадку помилку знову передаємо до форми методом `addError()`. Його можна викликати або на конкретному елементі, або безпосередньо на формі: - -```php -try { - $data = $form->getValues(); - $this->user->login($data->username, $data->password); - $this->redirect('Home:'); - -} catch (Nette\Security\AuthenticationException $e) { - if ($e->getCode() === Nette\Security\Authenticator::InvalidCredential) { - $form->addError('Неправильний пароль.'); - } -} -``` - -Якщо можливо, рекомендуємо прикріпити помилку безпосередньо до елемента форми, оскільки вона відобразиться поруч із ним при використанні стандартного візуалізатора. - -```php -$form['date']->addError('Вибачте, але ця дата вже зайнята.'); -``` - -Ви можете викликати `addError()` повторно і таким чином передати формі або елементу кілька повідомлень про помилки. Отримати їх можна за допомогою `getErrors()`. - -Увага, `$form->getErrors()` повертає зведення всіх повідомлень про помилки, включаючи ті, що були передані безпосередньо окремим елементам, а не лише безпосередньо формі. Повідомлення про помилки, передані лише формі, можна отримати через `$form->getOwnErrors()`. - - -Зміна вводу -=========== - -За допомогою методу `addFilter()` ми можемо змінити значення, введене користувачем. У цьому прикладі ми будемо толерувати та видаляти пробіли в поштовому індексі: - -```php -$form->addText('zip', 'Поштовий індекс:') - ->addFilter(function ($value) { - return str_replace(' ', '', $value); // видалимо пробіли з поштового індексу - }) - ->addRule($form::Pattern, 'Поштовий індекс не у форматі п\'яти цифр', '\d{5}'); -``` - -Фільтр включається між правилами валідації та умовами, тому порядок методів має значення, тобто фільтр і правило викликаються в тому порядку, в якому вказані методи `addFilter()` та `addRule()`. - - -JavaScript валідація -==================== - -Мова для формулювання умов і правил дуже потужна. Усі конструкції при цьому працюють як на стороні сервера, так і на стороні JavaScript. Вони передаються в HTML-атрибутах `data-nette-rules` як JSON. Саму валідацію потім виконує скрипт, який перехоплює подію форми `submit`, проходить по окремих елементах і виконує відповідну валідацію. - -Цим скриптом є `netteForms.js`, і він доступний з кількох можливих джерел: - -Скрипт можна вставити безпосередньо в HTML-сторінку з CDN: - -```latte -<script src="https://unpkg.com/nette-forms@3"></script> -``` - -Або скопіювати локально в публічний каталог проекту (наприклад, з `vendor/nette/forms/src/assets/netteForms.min.js`): - -```latte -<script src="/path/to/netteForms.min.js"></script> -``` - -Або встановити через [npm|https://www.npmjs.com/package/nette-forms]: - -```shell -npm install nette-forms -``` - -А потім завантажити та запустити: - -```js -import netteForms from 'nette-forms'; -netteForms.initOnLoad(); -``` - -Альтернативно, його можна завантажити безпосередньо з каталогу `vendor`: - -```js -import netteForms from '../path/to/vendor/nette/forms/src/assets/netteForms.js'; -netteForms.initOnLoad(); -``` - - -Динамічний JavaScript -===================== - -Хочете відображати поля для введення адреси лише якщо користувач вибере доставку товару поштою? Без проблем. Ключем є пара методів `addCondition()` & `toggle()`: - -```php -$form->addCheckbox('send_it') - ->addCondition($form::Equal, true) - ->toggle('#address-container'); -``` - -Цей код говорить, що коли умова виконана, тобто коли відмічено checkbox, буде видимим HTML-елемент `#address-container`. І навпаки. Елементи форми з адресою одержувача ми розмістимо в контейнері з цим ID, і при кліку на checkbox вони будуть приховані або показані. Це забезпечує скрипт `netteForms.js`. - -Як аргумент методу `toggle()` можна передати будь-який селектор. З історичних причин буквено-цифровий рядок без інших спеціальних символів розуміється як ID елемента, тобто так само, якби йому передував символ `#`. Другий необов'язковий параметр дозволяє інвертувати поведінку, тобто якби ми використали `toggle('#address-container', false)`, елемент би, навпаки, відображався лише тоді, коли checkbox не був би відмічений. - -Стандартна реалізація в JavaScript змінює властивість `hidden` елементів. Однак поведінку можна легко змінити, наприклад, додати анімацію. Достатньо в JavaScript перезаписати метод `Nette.toggle` власним рішенням: - -```js -Nette.toggle = (selector, visible, srcElement, event) => { - document.querySelectorAll(selector).forEach((el) => { - // приховаємо або покажемо 'el' залежно від значення 'visible' - }); -}; -``` - - -Вимкнення валідації -=================== - -Іноді може знадобитися вимкнути валідацію. Якщо натискання кнопки відправки не повинно виконувати валідацію (підходить для кнопок *Cancel* або *Preview*), вимкнемо її методом `$submit->setValidationScope([])`. Якщо вона повинна виконувати лише часткову валідацію, ми можемо вказати, які поля або контейнери форми мають валідуватися. - -```php -$form->addText('name') - ->setRequired(); - -$details = $form->addContainer('details'); -$details->addInteger('age') - ->setRequired('age'); -$details->addInteger('age2') - ->setRequired('age2'); - -$form->addSubmit('send1'); // Валідує всю форму -$form->addSubmit('send2') - ->setValidationScope([]); // Не валідує взагалі -$form->addSubmit('send3') - ->setValidationScope([$form['name']]); // Валідує лише елемент name -$form->addSubmit('send4') - ->setValidationScope([$form['details']['age']]); // Валідує лише елемент age -$form->addSubmit('send5') - ->setValidationScope([$form['details']]); // Валідує контейнер details -``` - -`setValidationScope` не впливає на [#подія onValidate] у формі, яка буде викликана завжди. Подія `onValidate` у контейнері буде викликана лише якщо цей контейнер позначений для часткової валідації. diff --git a/http/bg/@home.texy b/http/bg/@home.texy deleted file mode 100644 index 5dc37c2679..0000000000 --- a/http/bg/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -Пакетът `nette/http` капсулира [HTTP request|request] & [response], работа със [сесии|sessions] и [парсване и съставяне на URL |urls]. - - -Инсталация ----------- - -Изтеглете и инсталирайте библиотеката с помощта на [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/bg/@left-menu.texy b/http/bg/@left-menu.texy deleted file mode 100644 index 1f9b67ea4c..0000000000 --- a/http/bg/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Въведение |@home] -- [HTTP request|request] -- [HTTP response|response] -- [Сесии |Sessions] -- [URL utilities |urls] -- [Конфигурация |configuration] diff --git a/http/bg/@meta.texy b/http/bg/@meta.texy deleted file mode 100644 index 57804a1127..0000000000 --- a/http/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Nette}} diff --git a/http/bg/configuration.texy b/http/bg/configuration.texy deleted file mode 100644 index 56fe18000c..0000000000 --- a/http/bg/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -HTTP конфигурация -***************** - -.[perex] -Преглед на опциите за конфигурация за Nette HTTP. - -Ако не използвате целия framework, а само тази библиотека, прочетете [как да заредите конфигурацията|bootstrap:]. - - -HTTP хедъри -=========== - -```neon -http: - # хедъри, които се изпращат с всяка заявка - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # влияе на хедъра X-Frame-Options - frames: ... # (string|bool) по подразбиране е 'SAMEORIGIN' -``` - -Framework-ът по съображения за сигурност изпраща хедъра `X-Frame-Options: SAMEORIGIN`, който казва, че страницата може да бъде показана вътре в друга страница (в елемента `<iframe>`) само ако се намира на същия домейн. Това може да бъде нежелателно в някои ситуации (например, ако разработвате приложение за Facebook), затова поведението може да бъде променено чрез настройка `frames: http://allowed-host.com` или `frames: true`. - - -Content Security Policy ------------------------ - -Лесно могат да се съставят хедъри `Content-Security-Policy` (по-нататък CSP), тяхното описание ще намерите в [описанието на CSP |https://content-security-policy.com]. CSP директивите (като напр. `script-src`) могат да бъдат записани или като низове според спецификацията, или като масив от стойности за по-добра четимост. Тогава не е необходимо около ключовите думи, като например `'self'`, да се пишат кавички. Nette също автоматично генерира стойност `nonce`, така че в хедъра ще има например `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # низ във формат според спецификацията на CSP - default-src: "'self' https://example.com" - - # масив от стойности - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool в случай на превключватели - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -В шаблоните използвайте `<script n:nonce>...</script>` и стойността nonce ще се допълни автоматично. Създаването на безопасни сайтове в Nette е наистина лесно. - -Подобно могат да се съставят и хедъри `Content-Security-Policy-Report-Only` (които могат да се използват паралелно с CSP) и [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -HTTP бисквитки --------------- - -Могат да се променят стойностите по подразбиране на някои параметри на метода [Nette\Http\Response::setCookie() |response#setCookie] и сесията. - -```neon -http: - # обхват на бисквитката според пътя - cookiePath: ... # (string) по подразбиране е '/' - - # домейни, които приемат бисквитката - cookieDomain: 'example.com' # (string|domain) по подразбиране не е зададено - - # изпращане на бисквитка само през HTTPS? - cookieSecure: ... # (bool|auto) по подразбиране е auto - - # изключва изпращането на бисквитка, която Nette използва за защита срещу CSRF - disableNetteCookie: ... # (bool) по подразбиране е false -``` - -Атрибутът `cookieDomain` определя кои домейни могат да приемат бисквитката. Ако не е посочен, бисквитката се приема от същия (под)домейн, който я е задал, *но не* и от неговите поддомейни. Ако `cookieDomain` е зададен, са включени и поддомейните. Затова посочването на `cookieDomain` е по-малко ограничаващо от пропускането му. - -Например при `cookieDomain: nette.org` бисквитките са достъпни и на всички поддомейни като `doc.nette.org`. Същото може да се постигне и с помощта на специалната стойност `domain`, т.е. `cookieDomain: domain`. - -Стойността по подразбиране `auto` при атрибута `cookieSecure` означава, че ако сайтът работи на HTTPS, бисквитките ще се изпращат с флаг `Secure` и следователно ще бъдат достъпни само през HTTPS. - - -HTTP прокси ------------ - -Ако сайтът работи зад HTTP прокси, въведете неговия IP адрес, за да работи правилно откриването на връзка през HTTPS и също IP адресите на клиента. Тоест, за да функциите [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] и [isSecured() |request#isSecured] връщат правилните стойности и в шаблоните да се генерират връзки с `https:` протокол. - -```neon -http: - # IP адрес, обхват (напр. 127.0.0.1/8) или масив от тези стойности - proxy: 127.0.0.1 # (string|string[]) по подразбиране не е зададено -``` - - -Сесия -===== - -Основни настройки на [сесиите|sessions]: - -```neon -session: - # показване на панела за сесии в Tracy Bar? - debugger: ... # (bool) по подразбиране е false - - # период на неактивност, след който сесията изтича - expiration: 14 days # (string) по подразбиране е '3 hours' - - # кога да се стартира сесията? - autoStart: ... # (smart|always|never) по подразбиране е 'smart' - - # handler, сървис, имплементиращ интерфейса SessionHandlerInterface - handler: @handlerService -``` - -Опцията `autoStart` контролира кога да се стартира сесията. Стойността `always` означава, че сесията ще се стартира винаги при стартиране на приложението. Стойността `smart` означава, че сесията ще се стартира при стартиране на приложението само тогава, когато вече съществува, или в момента, в който искаме да четем от нея или да записваме в нея. И накрая стойността `never` забранява автоматичното стартиране на сесията. - -Освен това могат да се настройват всички PHP [session директиви |https://www.php.net/manual/en/session.configuration.php] (във формат camelCase) и също [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Пример: - -```neon -session: - # 'session.name' записваме като 'name' - name: MYID - - # 'session.save_path' записваме като 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Бисквитка за сесия ------------------- - -Бисквитката за сесия се изпраща със същите параметри като [други бисквитки |#HTTP бисквитки], но тези можете да промените за нея: - -```neon -session: - # домейни, които приемат бисквитката - cookieDomain: 'example.com' # (string|domain) - - # ограничение при достъп от друг домейн - cookieSamesite: None # (Strict|Lax|None) по подразбиране е Lax -``` - -Атрибутът `cookieSamesite` влияе дали бисквитката ще бъде изпратена при [достъп от друг домейн |nette:glossary#SameSite cookie], което предоставя известна защита срещу атаки [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -DI сървиси -========== - -Тези сървиси се добавят към DI контейнера: - -| Име | Тип | Описание -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP заявка| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP отговор| response] -| `session.session` | [api:Nette\Http\Session] | [управление на сесии| sessions] diff --git a/http/bg/request.texy b/http/bg/request.texy deleted file mode 100644 index a807ef95ff..0000000000 --- a/http/bg/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -HTTP заявка -*********** - -.[perex] -Nette капсулира HTTP заявката в обекти с разбираем API и същевременно предоставя саниращ филтър. - -HTTP заявката представлява обект [api:Nette\Http\Request]. Ако работите с Nette, този обект се създава автоматично от framework-а и можете да го получите чрез [dependency injection |dependency-injection:passing-dependencies]. В презентерите е достатъчно само да извикате метода `$this->getHttpRequest()`. Ако работите извън Nette Framework, можете да създадете обект с помощта на [#RequestFactory]. - -Голямо предимство на Nette е, че при създаването на обекта автоматично почиства всички входни параметри GET, POST, COOKIE, както и URL от контролни знаци и невалидни UTF-8 последователности. С тези данни след това можете безопасно да работите по-нататък. Почистените данни след това се използват в презентерите и формите. - -→ [Инсталация и изисквания |@home#Инсталация] - - -Nette\Http\Request -================== - -Този обект е immutable (непроменлив). Няма никакви сетъри, има само един т.нар. wither `withUrl()`, който не променя обекта, а връща нов екземпляр с променена стойност. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Връща клонинг с различен URL. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Връща URL на заявката като обект [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Предупреждение: браузърите не изпращат фрагмент на сървъра, така че `$url->getFragment()` ще връща празен низ. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Връща параметрите на GET заявката. - -```php -$all = $httpRequest->getQuery(); // връща масив с всички параметри от URL -$id = $httpRequest->getQuery('id'); // връща GET параметър 'id' (или null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Връща параметрите на POST заявката. - -```php -$all = $httpRequest->getPost(); // връща масив с всички параметри от POST -$id = $httpRequest->getPost('id'); // връща POST параметър 'id' (или null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Връща [качване |#Качени файлове] като обект [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // качен ли е файл? - $file->getUntrustedName(); // име на файла, изпратено от потребителя - $file->getSanitizedName(); // име без опасни символи -} -``` - -За достъп до вложена структура посочете масив от ключове. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Тъй като не може да се вярва на данни отвън и следователно не може да се разчита на структурата на файловете, този начин е по-безопасен от например `$request->getFiles()['my-form']['details']['avatar']`, който може да се провали. - - -getFiles(): array .[method] ---------------------------- -Връща дърво на [всички качвания |#Качени файлове] в нормализирана структура, чиито листа са обекти [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Връща бисквитка или `null`, когато не съществува. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Връща всички бисквитки. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Връща HTTP метода, с който е направена заявката. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Тества HTTP метода, с който е направена заявката. Параметърът е case-insensitive. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Връща HTTP хедър или `null`, ако не съществува. Параметърът е case-insensitive. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Връща всички HTTP хедъри като асоциативен масив. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Връзката шифрована ли е (HTTPS)? За правилната функционалност може да е необходимо [да се настрои прокси |configuration#HTTP прокси]. - - -isSameSite(): bool .[method] ----------------------------- -Заявката идва ли от същия (под)домейн и е инициирана чрез кликване върху връзка? Nette използва бисквитката `_nss` (преди `nette-samesite`) за откриване. - - -isAjax(): bool .[method] ------------------------- -Това AJAX заявка ли е? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Връща IP адреса на потребителя. За правилната функционалност може да е необходимо [да се настрои прокси |configuration#HTTP прокси]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Връща DNS превода на IP адреса на потребителя. За правилната функционалност може да е необходимо [да се настрои прокси |configuration#HTTP прокси]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Връща данните за удостоверяване за [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Връща тялото на HTTP заявката. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Открива езика. Като параметър `$lang` предаваме масив с езиците, които приложението поддържа, и тя връща този, който браузърът на посетителя би предпочел най-много. Това не са никакви магии, просто се използва хедърът `Accept-Language`. Ако не се намери съвпадение, връща `null`. - -```php -// браузърът изпраща напр. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'en']; // езици, поддържани от приложението -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -Класът [api:Nette\Http\RequestFactory] служи за създаване на екземпляр на `Nette\Http\Request`, който представлява текущата HTTP заявка. (Ако работите с Nette, обектът на HTTP заявката се създава автоматично от framework-а.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -Методът `fromGlobals()` създава обект на заявката въз основа на текущите глобални променливи на PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` и `$_SERVER`). При създаването на обекта автоматично почиства всички входни параметри GET, POST, COOKIE, както и URL от контролни знаци и невалидни UTF-8 последователности, което осигурява безопасност при по-нататъшна работа с тези данни. - -RequestFactory може да се конфигурира преди извикването на `fromGlobals()`: - -- с метода `$factory->setBinary()` изключвате автоматичното почистване на входните параметри от контролни знаци и невалидни UTF-8 последователности. -- с метода `$factory->setProxy(...)` посочвате IP адреса на [прокси сървъра |configuration#HTTP прокси], което е необходимо за правилното откриване на IP адреса на потребителя. - -RequestFactory позволява да се дефинират филтри, които автоматично трансформират части от URL на заявката. Тези филтри премахват нежелани знаци от URL, които могат да бъдат вмъкнати там например поради неправилна имплементация на системи за коментари на различни сайтове: - -```php -// премахване на интервали от пътя -$requestFactory->urlFilters['path']['%20'] = ''; - -// премахване на точка, запетая или дясна скоба от края на URI -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// почистване на пътя от двойни наклонени черти (филтър по подразбиране) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Първият ключ `'path'` или `'url'` определя към коя част на URL ще се приложи филтърът. Вторият ключ е регулярен израз, който трябва да се търси, а стойността е заместителят, който ще се използва вместо намерения текст. - - -Качени файлове -============== - -Методът `Nette\Http\Request::getFiles()` връща масив от всички качвания в нормализирана структура, чиито листа са обекти [api:Nette\Http\FileUpload]. Те капсулират данните, изпратени от елемента на формата `<input type=file>`. - -Структурата отразява именуването на елементите в HTML. В най-простия случай това може да бъде единствен именуван елемент на формата, изпратен като: - -```latte -<input type="file" name="avatar"> -``` - -В този случай `$request->getFiles()` връща масив: - -```php -[ - 'avatar' => /* FileUpload instance */ -] -``` - -Обектът `FileUpload` се създава и в случай, че потребителят не е изпратил никакъв файл или изпращането е неуспешно. Дали файлът е бил изпратен връща методът `hasFile()`: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -В случай на име на елемент, използващо нотация за масив: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -върнатото дърво изглежда така: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* FileUpload instance */ - ], - ], -] -``` - -Може да се създаде и масив от файлове: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -В такъв случай структурата изглежда така: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, - ], - ], - ], -] -``` - -Достъпът до индекс 1 на вложения масив се осъществява най-добре така: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Тъй като не може да се вярва на данни отвън и следователно не може да се разчита на структурата на файловете, този начин е по-безопасен от например `$request->getFiles()['my-form']['details']['avatars'][1]`, който може да се провали. - - -Преглед на методите на `FileUpload` .{toc: FileUpload} ------------------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Връща `true`, ако потребителят е качил някакъв файл. - - -isOk(): bool .[method] ----------------------- -Връща `true`, ако файлът е бил качен успешно. - - -getError(): int .[method] -------------------------- -Връща кода на грешката при качване на файла. Това е една от константите [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. В случай, че качването е преминало успешно, връща `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Премества качения файл на ново място. Ако целевият файл вече съществува, той ще бъде презаписан. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Връща съдържанието на качения файл. В случай, че качването не е било успешно, връща `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Открива MIME content type на качения файл въз основа на неговата сигнатура. В случай, че качването не е било успешно или откриването не е успяло, връща `null`. - -.[caution] -Изисква PHP разширението `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Връща оригиналното име на файла, както го е изпратил браузърът. - -.[caution] -Не вярвайте на стойността, върната от този метод. Клиентът може да е изпратил злонамерено име на файл с намерение да повреди или хакне вашето приложение. - - -getSanitizedName(): string .[method] ------------------------------------- -Връща санираното име на файла. Съдържа само ASCII знаци `[a-zA-Z0-9.-]`. Ако името не съдържа такива знаци, връща `'unknown'`. Ако файлът е изображение във формат JPEG, PNG, GIF, WebP или AVIF, връща и правилното разширение. - -.[caution] -Изисква PHP разширението `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Връща подходящо разширение на файла (без точка), съответстващо на открития MIME тип. - -.[caution] -Изисква PHP разширението `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Връща оригиналния път до файла, както го е изпратил браузърът при качване на папка. Целият път е достъпен само в PHP 8.1 и по-нови версии. В предишни версии този метод връща оригиналното име на файла. - -.[caution] -Не вярвайте на стойността, върната от този метод. Клиентът може да е изпратил злонамерено име на файл с намерение да повреди или хакне вашето приложение. - - -getSize(): int .[method] ------------------------- -Връща размера на качения файл. В случай, че качването не е било успешно, връща `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Връща пътя до временната локация на качения файл. В случай, че качването не е било успешно, връща `''`. - - -isImage(): bool .[method] -------------------------- -Връща `true`, ако каченият файл е изображение във формат JPEG, PNG, GIF, WebP или AVIF. Откриването се извършва въз основа на неговата сигнатура и не се проверява целостта на целия файл. Дали изображението не е повредено може да се установи например чрез опит за неговото [зареждане |#toImage]. - -.[caution] -Изисква PHP разширението `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Връща двойка `[ширина, височина]` с размерите на каченото изображение. В случай, че качването не е било успешно или не е валидно изображение, връща `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Зарежда изображението като обект [Image|utils:images]. В случай, че качването не е било успешно или не е валидно изображение, хвърля изключение `Nette\Utils\ImageException`. diff --git a/http/bg/response.texy b/http/bg/response.texy deleted file mode 100644 index 461e66a71a..0000000000 --- a/http/bg/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -HTTP отговор -************ - -.[perex] -Nette капсулира HTTP отговора в обекти с разбираем API. - -HTTP отговорът представлява обект [api:Nette\Http\Response]. Ако работите с Nette, този обект се създава автоматично от framework-а и можете да го получите чрез [dependency injection |dependency-injection:passing-dependencies]. В презентерите е достатъчно само да извикате метода `$this->getHttpResponse()`. - -→ [Инсталация и изисквания |@home#Инсталация] - - -Nette\Http\Response -=================== - -Обектът, за разлика от [Nette\Http\Request|request], е mutable, т.е. с помощта на сетъри можете да променяте състоянието, например да изпращате хедъри. Не забравяйте, че всички сетъри трябва да бъдат извикани **преди изпращането на какъвто и да е изход.** Дали вече е бил изпратен изход показва методът `isSent()`. Ако връща `true`, всеки опит за изпращане на хедър ще предизвика изключение `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Променя [кода на състоянието на отговора |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. За по-добра разбираемост на изходния код препоръчваме за кода да се използват вместо числа [предварително дефинирани константи |api:Nette\Http\IResponse]. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Връща кода на състоянието на отговора. - - -isSent(): bool .[method] ------------------------- -Връща дали вече са били изпратени хедъри от сървъра към браузъра и следователно вече не е възможно да се изпращат хедъри или да се променя кодът на състоянието. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Изпраща HTTP хедър и **презаписва** предишно изпратен хедър със същото име. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Изпраща HTTP хедър и **не презаписва** предишно изпратен хедър със същото име. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Изтрива предишно изпратен HTTP хедър. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Връща изпратен HTTP хедър или `null`, ако такъв не съществува. Параметърът е case-insensitive. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Връща всички изпратени HTTP хедъри като асоциативен масив. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Променя хедъра `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Пренасочва към друг URL. Не забравяйте след това да прекратите скрипта. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Задава изтичането на HTTP документа с помощта на хедърите `Cache-Control` и `Expires`. Параметърът е или времеви интервал (като текст), или `null`, което забранява кеширането. - -```php -// кешът в браузъра изтича след час -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -Отговорът ще бъде изтеглен с помощта на диалоговия прозорец *Запиши като* под посоченото име. Самият файл при това не се изпраща. - -```php -$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] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Изпраща бисквитка. Стойностите по подразбиране на параметрите: - -| `$path` | `'/'` | бисквитката има обхват за всички пътища в (под)домейна *(конфигурируемо)* -| `$domain` | `null` | което означава с обхват за текущия (под)домейн, но не и неговите поддомейни *(конфигурируемо)* -| `$secure` | `true` | ако сайтът работи на HTTPS, иначе `false` *(конфигурируемо)* -| `$httpOnly` | `true` | бисквитката е недостъпна за JavaScript -| `$sameSite` | `'Lax'` | бисквитката може да не бъде изпратена при [достъп от друг домейн |nette:glossary#SameSite cookie] - -Стойностите по подразбиране на параметрите `$path`, `$domain` и `$secure` можете да промените в [конфигурацията |configuration#HTTP бисквитки]. - -Времето може да се посочва като брой секунди или низ: - -```php -$httpResponse->setCookie('lang', 'bg', '100 days'); -``` - -Параметърът `$domain` определя кои домейни могат да приемат бисквитката. Ако не е посочен, бисквитката се приема от същия (под)домейн, който я е задал, но не и от неговите поддомейни. Ако `$domain` е зададен, са включени и поддомейните. Затова посочването на `$domain` е по-малко ограничаващо от пропускането му. Например при `$domain = 'nette.org'` бисквитките са достъпни и на всички поддомейни като `doc.nette.org`. - -За стойността `$sameSite` можете да използвате константите `Response::SameSiteLax`, `SameSiteStrict` и `SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Изтрива бисквитка. Стойностите по подразбиране на параметрите са: -- `$path` с обхват за всички директории (`'/'`) -- `$domain` с обхват за текущия (под)домейн, но не и неговите поддомейни -- `$secure` се управлява според настройките в [конфигурацията |configuration#HTTP бисквитки] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/bg/sessions.texy b/http/bg/sessions.texy deleted file mode 100644 index 73b3cb8c28..0000000000 --- a/http/bg/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Сесии -***** - -<div class=perex> - -HTTP е протокол без състояние, но почти всяко приложение трябва да съхранява състояние между заявките, например съдържанието на количката за пазаруване. Именно за това служат сесиите. Ще покажем, - -- как да използваме сесии -- как да предотвратим конфликти на имена -- как да настроим изтичане - -</div> - -При използване на сесии всеки потребител получава уникален идентификатор, наречен session ID, който се предава в бисквитка. Той служи като ключ към данните на сесията. За разлика от бисквитките, които се съхраняват от страна на браузъра, данните в сесията се съхраняват от страна на сървъра. - -Сесията се настройва в [конфигурацията |configuration#Сесия], важен е особено изборът на времето за изтичане. - -Управлението на сесията се осъществява от обекта [api:Nette\Http\Session], до който можете да стигнете, като го получите чрез [dependency injection |dependency-injection:passing-dependencies]. В презентерите е достатъчно само да извикате `$session = $this->getSession()`. - -→ [Инсталация и изисквания |@home#Инсталация] - - -Стартиране на сесия -=================== - -Nette по подразбиране автоматично стартира сесията в момента, когато започнем да четем от нея или да записваме данни в нея. Ръчно сесията се стартира с `$session->start()`. - -PHP изпраща при стартиране на сесията HTTP хедъри, влияещи на кеширането, виж [php:session_cache_limiter], и евентуално и бисквитка със session ID. Затова е необходимо винаги да стартирате сесията преди изпращането на какъвто и да е изход към браузъра, иначе ще бъде хвърлено изключение. Ако знаете, че по време на рендирането на страницата ще се използва сесия, стартирайте я ръчно преди това, например в презентера. - -В режим на разработка сесията се стартира от Tracy, тъй като я използва за показване на ленти с пренасочвания и AJAX заявки в Tracy Bar. - - -Секции -====== - -В чист PHP хранилището на данни на сесията се реализира като масив, достъпен чрез глобалната променлива `$_SESSION`. Проблемът е, че приложенията обикновено се състоят от цяла редица взаимно независими части и ако всички имат на разположение само един масив, рано или късно ще възникне конфликт на имена. - -Nette Framework решава проблема, като разделя цялото пространство на секции (обекти [api:Nette\Http\SessionSection]). Всяка единица след това използва своя собствена секция с уникално име и вече не може да възникне никакъв конфликт. - -Секцията получаваме от сесията: - -```php -$section = $session->getSection('уникално_име'); -``` - -В презентера е достатъчно да използвате `getSession()` с параметър: - -```php -// $this е Presenter -$section = $this->getSession('уникално_име'); -``` - -Проверката за съществуване на секция може да се направи с метода `$session->hasSection('уникално_име')`. - -Със самата секция след това се работи много лесно с помощта на методите `set()`, `get()` и `remove()`: - -```php -// запис на променлива -$section->set('userName', 'franta'); - -// четене на променлива, връща null, ако не съществува -echo $section->get('userName'); - -// изтриване на променлива -$section->remove('userName'); -``` - -За получаване на всички променливи от секцията може да се използва цикъл `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Настройка на изтичане ---------------------- - -За отделни секции или дори отделни променливи е възможно да се настрои изтичане. Можем така да оставим изтичането на влизането на потребителя след 20 минути, но същевременно да продължим да помним съдържанието на количката. - -```php -// секцията изтича след 20 минути -$section->setExpiration('20 minutes'); -``` - -За настройка на изтичането на отделни променливи служи третият параметър на метода `set()`: - -```php -// променливата 'flash' изтича след 30 секунди -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Не забравяйте, че времето за изтичане на цялата сесия (виж [конфигурация на сесията |configuration#Сесия]) трябва да бъде същото или по-голямо от времето, зададено за отделните секции или променливи. - -Отмяната на предишно зададено изтичане се постига с метода `removeExpiration()`. Незабавното отменяне на цялата секция осигурява методът `remove()`. - - -Събития $onStart, $onBeforeWrite --------------------------------- - -Обектът `Nette\Http\Session` има [събития |nette:glossary#Събития events] `$onStart` и `$onBeforeWrite`, така че можете да добавите callback-ове, които се извикват след стартиране на сесията или преди нейното записване на диска и последващо прекратяване. - -```php -$session->onBeforeWrite[] = function () { - // записваме данни в сесията - $this->section->set('basket', $this->basket); -}; -``` - - -Управление на сесии -=================== - -Преглед на методите на класа `Nette\Http\Session` за управление на сесии: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Стартира сесията. - - -isStarted(): bool .[method] ---------------------------- -Сесията стартирана ли е? - - -close(): void .[method] ------------------------ -Прекратява сесията. Сесията автоматично се прекратява в края на изпълнението на скрипта. - - -destroy(): void .[method] -------------------------- -Прекратява и изтрива сесията. - - -exists(): bool .[method] ------------------------- -HTTP заявката съдържа ли бисквитка със session ID? - - -regenerateId(): void .[method] ------------------------------- -Генерира нов случаен session ID. Данните остават запазени. - - -getId(): string .[method] -------------------------- -Връща session ID. - -</div> - - -Конфигурация ------------- - -Сесията се настройва в [конфигурацията |configuration#Сесия]. Ако пишете приложение, което не използва DI контейнер, за конфигурация служат тези методи. Трябва да бъдат извикани преди стартирането на сесията. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Задава името на бисквитката, в която се пренася session ID. Стандартното име е `PHPSESSID`. Полезно е в случай, че в рамките на един сайт поддържате няколко различни приложения. - - -getName(): string .[method] ---------------------------- -Връща името на бисквитката, в която се пренася session ID. - - -setOptions(array $options): static .[method] --------------------------------------------- -Конфигурира сесията. Могат да се настройват всички PHP [session директиви |https://www.php.net/manual/en/session.configuration.php] (във формат camelCase, напр. вместо `session.save_path` записваме `savePath`) и също [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Задава времето на неактивност, след което сесията изтича. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Настройка на параметрите за бисквитката. Стойностите по подразбиране на параметрите можете да промените в [конфигурацията |configuration#Бисквитка за сесия]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Задава директорията, където се съхраняват файловете със сесиите. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Настройка на собствен handler, виж [документацията на PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Сигурността преди всичко -======================== - -Сървърът предполага, че комуникира постоянно със същия потребител, докато заявките са придружени от същия session ID. Задачата на механизмите за сигурност е да гарантират, че това наистина е така и не е възможно идентификаторът да бъде откраднат или подменен. - -Nette Framework затова правилно конфигурира PHP директивите, така че session ID да се пренася само в бисквитка, да го направи недостъпен за JavaScript и да игнорира евентуални идентификатори в URL. Освен това в критични моменти, като например влизане на потребителя, генерира нов session ID. - -.[note] -За конфигурация на PHP се използва функцията ini_set, която за съжаление някои хостинги забраняват. Ако това е случаят и с вашия хостинг, опитайте да се договорите с него да ви разреши функцията или поне да конфигурира сървъра. diff --git a/http/bg/urls.texy b/http/bg/urls.texy deleted file mode 100644 index cdbf111e0a..0000000000 --- a/http/bg/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Работа с URL адреси -******************* - -.[perex] -Класовете [#Url], [#UrlImmutable] и [#UrlScript] позволяват лесно генериране, парсиране и манипулиране на URL адреси. - -→ [Инсталация и изисквания |@home#Инсталация] - - -Url -=== - -Класът [api:Nette\Http\Url] позволява лесно да се работи с URL и неговите отделни компоненти, които са показани на тази схема: - -/--pre - схема потребител парола хост порт път заявка фрагмент - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -Генерирането на URL е интуитивно: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Може също да се парсира URL и да се манипулира по-нататък: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -Класът `Url` имплементира интерфейса `JsonSerializable` и има метод `__toString()`, така че обектът може да бъде изведен или използван в данни, предавани на `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -URL компоненти .[method] ------------------------- - -За връщане или промяна на отделните компоненти на URL са ви на разположение тези методи: - -.[language-php] -| Setter | Getter | Върната стойност -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | цял URL - -Предупреждение: Когато работите с URL, който е получен от [HTTP заявка|request], имайте предвид, че той няма да съдържа фрагмент, тъй като браузърът не го изпраща на сървъра. - -Можем да работим и с отделните query параметри с помощта на: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Връща дясната или лявата част на хоста. Така работи, ако хостът е `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)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Проверява дали два URL адреса са идентични. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Проверява дали URL адресът е абсолютен. URL се счита за абсолютен, ако започва със схема (напр. http, https, ftp), последвана от двоеточие. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Нормализира пътя в URL чрез премахване на специалните сегменти `.` и `..`. Методът премахва излишните елементи на пътя по същия начин, както го правят уеб браузърите. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -Класът [api:Nette\Http\UrlImmutable] е immutable (непроменлива) алтернатива на класа [#Url] (подобно на това как в PHP `DateTimeImmutable` е непроменлива алтернатива на `DateTime`). Вместо сетъри има т.нар. withery, които не променят обекта, а връщат нови екземпляри с променена стойност: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -Класът `UrlImmutable` имплементира интерфейса `JsonSerializable` и има метод `__toString()`, така че обектът може да бъде изведен или използван в данни, предавани на `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -URL компоненти .[method] ------------------------- - -За връщане или промяна на отделните компоненти на URL служат методите: - -.[language-php] -| Wither | Getter | Върната стойност -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | цял URL - -Методът `withoutUserInfo()` премахва `user` и `password`. - -Можем да работим и с отделните query параметри с помощта на: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Връща дясната или лявата част на хоста. Така работи, ако хостът е `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} ----------------------------------------------------------------------- -Извежда абсолютен URL по същия начин, по който браузърът обработва връзките на HTML страница: -- ако връзката е абсолютен URL (съдържа схема), тя се използва без промяна -- ако връзката започва с `//`, се приема само схемата от текущия URL -- ако връзката започва с `/`, се създава абсолютен път от корена на домейна -- в останалите случаи URL се съставя относително спрямо текущия път - -```php -$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' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Проверява дали два URL адреса са идентични. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -Класът [api:Nette\Http\UrlScript] е наследник на [#UrlImmutable] и го разширява с допълнителни виртуални компоненти на URL, като например коренната директория на проекта и др. Подобно на родителския клас, той е immutable (непроменлив) обект. - -Следващата диаграма показва компонентите, които UrlScript разпознава: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` е основният URL адрес на приложението, включително домейна и частта от пътя до коренната директория на приложението -- `basePath` е частта от пътя до коренната директория на приложението -- `scriptPath` е пътят до текущия скрипт -- `relativePath` е името на скрипта (евентуално и други сегменти от пътя) относително спрямо basePath -- `relativeUrl` е цялата част от URL след baseUrl, включително query string и фрагмент. -- `pathInfo` днес вече малко използвана част от URL след името на скрипта - -За връщане на части от URL са на разположение методите: - -.[language-php] -| Getter | Върната стойност -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Обектите `UrlScript` обикновено не ги създаваме директно, а ги връща методът [Nette\Http\Request::getUrl()|request] с вече правилно зададени компоненти за текущата HTTP заявка. diff --git a/http/el/@home.texy b/http/el/@home.texy deleted file mode 100644 index 7f99e07aa7..0000000000 --- a/http/el/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -Το πακέτο `nette/http` ενσωματώνει το [HTTP request |request] & [response], την εργασία με [sessions] και την [ανάλυση και σύνθεση URL |urls]. - - -Εγκατάσταση ------------ - -Κατεβάστε και εγκαταστήστε τη βιβλιοθήκη χρησιμοποιώντας το εργαλείο [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/el/@left-menu.texy b/http/el/@left-menu.texy deleted file mode 100644 index a8ca399134..0000000000 --- a/http/el/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Εισαγωγή |@home] -- [HTTP request|request] -- [HTTP response|response] -- [Sessions] -- [Βοηθητικά προγράμματα URL |urls] -- [Διαμόρφωση |configuration] diff --git a/http/el/@meta.texy b/http/el/@meta.texy deleted file mode 100644 index 88e29852c7..0000000000 --- a/http/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Τεκμηρίωση}} diff --git a/http/el/configuration.texy b/http/el/configuration.texy deleted file mode 100644 index 828c56c70c..0000000000 --- a/http/el/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -Διαμόρφωση HTTP -*************** - -.[perex] -Επισκόπηση των επιλογών διαμόρφωσης για το Nette HTTP. - -Εάν δεν χρησιμοποιείτε ολόκληρο το framework, αλλά μόνο αυτή τη βιβλιοθήκη, διαβάστε [πώς να φορτώσετε τη διαμόρφωση|bootstrap:]. - - -Κεφαλίδες HTTP -============== - -```neon -http: - # κεφαλίδες που αποστέλλονται με κάθε αίτημα - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # επηρεάζει την κεφαλίδα X-Frame-Options - frames: ... # (string|bool) προεπιλογή είναι 'SAMEORIGIN' -``` - -Για λόγους ασφαλείας, το framework στέλνει την κεφαλίδα `X-Frame-Options: SAMEORIGIN`, η οποία δηλώνει ότι η σελίδα μπορεί να εμφανιστεί μέσα σε άλλη σελίδα (στο στοιχείο `<iframe>`) μόνο εάν βρίσκεται στο ίδιο domain. Αυτό μπορεί να είναι ανεπιθύμητο σε ορισμένες περιπτώσεις (για παράδειγμα, εάν αναπτύσσετε μια εφαρμογή για το Facebook), οπότε η συμπεριφορά μπορεί να αλλάξει ορίζοντας `frames: http://allowed-host.com` ή `frames: true`. - - -Πολιτική Ασφάλειας Περιεχομένου -------------------------------- - -Μπορείτε εύκολα να δημιουργήσετε κεφαλίδες `Content-Security-Policy` (εφεξής CSP), η περιγραφή τους βρίσκεται στην [περιγραφή CSP |https://content-security-policy.com]. Οι οδηγίες CSP (όπως `script-src`) μπορούν να γραφτούν είτε ως συμβολοσειρές σύμφωνα με την προδιαγραφή, είτε ως πίνακες τιμών για καλύτερη αναγνωσιμότητα. Τότε δεν χρειάζεται να γράψετε εισαγωγικά γύρω από λέξεις-κλειδιά όπως `'self'`. Το Nette δημιουργεί επίσης αυτόματα μια τιμή `nonce`, οπότε η κεφαλίδα θα περιέχει κάτι σαν `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # συμβολοσειρά στη μορφή σύμφωνα με την προδιαγραφή CSP - default-src: "'self' https://example.com" - - # πίνακας τιμών - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool στην περίπτωση διακοπτών - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -Στα templates, χρησιμοποιήστε `<script n:nonce>...</script>` και η τιμή nonce θα συμπληρωθεί αυτόματα. Η δημιουργία ασφαλών ιστότοπων στο Nette είναι πραγματικά εύκολη. - -Ομοίως, μπορείτε να δημιουργήσετε κεφαλίδες `Content-Security-Policy-Report-Only` (οι οποίες μπορούν να χρησιμοποιηθούν παράλληλα με το CSP) και [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -HTTP cookie ------------ - -Μπορείτε να αλλάξετε τις προεπιλεγμένες τιμές ορισμένων παραμέτρων της μεθόδου [Nette\Http\Response::setCookie() |response#setCookie] και του session. - -```neon -http: - # εμβέλεια cookie ανά διαδρομή - cookiePath: ... # (string) προεπιλογή είναι '/' - - # domains που δέχονται cookies - cookieDomain: 'example.com' # (string|domain) προεπιλογή είναι μη ορισμένο - - # αποστολή cookies μόνο μέσω HTTPS; - cookieSecure: ... # (bool|auto) προεπιλογή είναι auto - - # απενεργοποιεί την αποστολή του cookie που χρησιμοποιείται από το Nette για προστασία από CSRF - disableNetteCookie: ... # (bool) προεπιλογή είναι false -``` - -Το attribute `cookieDomain` καθορίζει ποια domains μπορούν να δέχονται cookies. Εάν δεν καθοριστεί, το cookie γίνεται αποδεκτό από το ίδιο (υπο)domain που το όρισε, *αλλά όχι* από τα υποdomains του. Εάν το `cookieDomain` καθοριστεί, περιλαμβάνονται και τα υποdomains. Επομένως, ο καθορισμός του `cookieDomain` είναι λιγότερο περιοριστικός από την παράλειψή του. - -Για παράδειγμα, με `cookieDomain: nette.org`, τα cookies είναι επίσης διαθέσιμα σε όλα τα υποdomains όπως το `doc.nette.org`. Αυτό μπορεί επίσης να επιτευχθεί χρησιμοποιώντας την ειδική τιμή `domain`, δηλαδή `cookieDomain: domain`. - -Η προεπιλεγμένη τιμή `auto` για το attribute `cookieSecure` σημαίνει ότι εάν ο ιστότοπος εκτελείται σε HTTPS, τα cookies θα αποστέλλονται με τη σημαία `Secure` και επομένως θα είναι διαθέσιμα μόνο μέσω HTTPS. - - -HTTP proxy ----------- - -Εάν ο ιστότοπος εκτελείται πίσω από ένα HTTP proxy, καθορίστε τη διεύθυνση IP του, ώστε η ανίχνευση σύνδεσης μέσω HTTPS και η διεύθυνση IP του client να λειτουργούν σωστά. Δηλαδή, ώστε οι συναρτήσεις [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] και [isSecured() |request#isSecured] να επιστρέφουν τις σωστές τιμές και οι σύνδεσμοι με το πρωτόκολλο `https:` να δημιουργούνται στα templates. - -```neon -http: - # Διεύθυνση IP, εύρος (π.χ., 127.0.0.1/8), ή πίνακας αυτών των τιμών - proxy: 127.0.0.1 # (string|string[]) προεπιλογή είναι μη ορισμένο -``` - - -Session -======= - -Βασικές ρυθμίσεις [sessions |sessions]: - -```neon -session: - # εμφάνιση του πίνακα session στο Tracy Bar; - debugger: ... # (bool) προεπιλογή είναι false - - # χρόνος αδράνειας μετά τον οποίο λήγει το session - expiration: 14 days # (string) προεπιλογή είναι '3 hours' - - # πότε πρέπει να ξεκινήσει το session; - autoStart: ... # (smart|always|never) προεπιλογή είναι 'smart' - - # handler, μια υπηρεσία που υλοποιεί το interface SessionHandlerInterface - handler: @handlerService -``` - -Η επιλογή `autoStart` ελέγχει πότε πρέπει να ξεκινήσει το session. Η τιμή `always` σημαίνει ότι το session θα ξεκινά πάντα με την εκκίνηση της εφαρμογής. Η τιμή `smart` σημαίνει ότι το session θα ξεκινά κατά την εκκίνηση της εφαρμογής μόνο εάν υπάρχει ήδη, ή τη στιγμή που θέλουμε να διαβάσουμε ή να γράψουμε σε αυτό. Τέλος, η τιμή `never` απενεργοποιεί την αυτόματη έναρξη του session. - -Επιπλέον, μπορείτε να ορίσετε όλες τις PHP [session directives |https://www.php.net/manual/en/session.configuration.php] (σε μορφή camelCase) καθώς και το [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Παράδειγμα: - -```neon -session: - # γράψτε το 'session.name' ως 'name' - name: MYID - - # γράψτε το 'session.save_path' ως 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Session cookie --------------- - -Το session cookie αποστέλλεται με τις ίδιες παραμέτρους όπως [άλλα cookie |#HTTP cookie], αλλά μπορείτε να τις αλλάξετε για αυτό: - -```neon -session: - # domains που δέχονται cookies - cookieDomain: 'example.com' # (string|domain) - - # περιορισμός κατά την πρόσβαση από άλλο domain - cookieSamesite: None # (Strict|Lax|None) προεπιλογή είναι Lax -``` - -Το attribute `cookieSamesite` επηρεάζει εάν το cookie θα αποσταλεί κατά την [πρόσβαση από άλλο domain |nette:glossary#SameSite cookie], το οποίο παρέχει κάποια προστασία έναντι επιθέσεων [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -Υπηρεσίες DI -============ - -Αυτές οι υπηρεσίες προστίθενται στο DI container: - -| Όνομα | Τύπος | Περιγραφή -|----------------------------------------------------- -| `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| sessions] diff --git a/http/el/request.texy b/http/el/request.texy deleted file mode 100644 index 9171ba5cd2..0000000000 --- a/http/el/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -Αίτημα HTTP -*********** - -.[perex] -Το Nette ενσωματώνει το αίτημα HTTP σε αντικείμενα με ένα κατανοητό API και ταυτόχρονα παρέχει ένα φίλτρο εξυγίανσης. - -Το αίτημα HTTP αντιπροσωπεύεται από το αντικείμενο [api:Nette\Http\Request]. Εάν εργάζεστε με το Nette, αυτό το αντικείμενο δημιουργείται αυτόματα από το framework και μπορείτε να το λάβετε μέσω [έγχυσης εξάρτησης |dependency-injection:passing-dependencies]. Στους presenters, απλά καλέστε τη μέθοδο `$this->getHttpRequest()`. Εάν εργάζεστε εκτός του Nette Framework, μπορείτε να δημιουργήσετε το αντικείμενο χρησιμοποιώντας το [#RequestFactory]. - -Ένα μεγάλο πλεονέκτημα του Nette είναι ότι κατά τη δημιουργία του αντικειμένου, καθαρίζει αυτόματα όλες τις παραμέτρους εισόδου GET, POST, COOKIE, καθώς και το URL από χαρακτήρες ελέγχου και μη έγκυρες ακολουθίες UTF-8. Στη συνέχεια, μπορείτε να εργαστείτε με ασφάλεια με αυτά τα δεδομένα. Τα καθαρισμένα δεδομένα χρησιμοποιούνται στη συνέχεια σε presenters και φόρμες. - -→ [Εγκατάσταση και απαιτήσεις |@home#Εγκατάσταση] - - -Nette\Http\Request -================== - -Αυτό το αντικείμενο είναι αμετάβλητο (immutable). Δεν έχει setters, έχει μόνο έναν λεγόμενο wither `withUrl()`, ο οποίος δεν αλλάζει το αντικείμενο, αλλά επιστρέφει μια νέα παρουσία με την αλλαγμένη τιμή. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Επιστρέφει έναν κλώνο με διαφορετικό URL. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Επιστρέφει το URL του αιτήματος ως αντικείμενο [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Προειδοποίηση: οι περιηγητές δεν στέλνουν το fragment στον διακομιστή, οπότε το `$url->getFragment()` θα επιστρέψει μια κενή συμβολοσειρά. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Επιστρέφει τις παραμέτρους GET του αιτήματος. - -```php -$all = $httpRequest->getQuery(); // επιστρέφει έναν πίνακα όλων των παραμέτρων από το URL -$id = $httpRequest->getQuery('id'); // επιστρέφει την παράμετρο GET 'id' (ή null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Επιστρέφει τις παραμέτρους POST του αιτήματος. - -```php -$all = $httpRequest->getPost(); // επιστρέφει έναν πίνακα όλων των παραμέτρων από το POST -$id = $httpRequest->getPost('id'); // επιστρέφει την παράμετρο POST 'id' (ή null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Επιστρέφει το [ανέβασμα |#Ανεβασμένα Αρχεία] ως αντικείμενο [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // ανέβηκε κάποιο αρχείο; - $file->getUntrustedName(); // όνομα αρχείου που στάλθηκε από τον χρήστη - $file->getSanitizedName(); // όνομα χωρίς επικίνδυνους χαρακτήρες -} -``` - -Για πρόσβαση σε μια ένθετη δομή, καθορίστε έναν πίνακα κλειδιών. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Επειδή δεν μπορείτε να εμπιστευτείτε δεδομένα από έξω και επομένως ούτε να βασιστείτε στη μορφή της δομής των αρχείων, αυτή η μέθοδος είναι ασφαλέστερη από, για παράδειγμα, `$request->getFiles()['my-form']['details']['avatar']`, η οποία μπορεί να αποτύχει. - - -getFiles(): array .[method] ---------------------------- -Επιστρέφει ένα δέντρο [όλων των ανεβασμάτων |#Ανεβασμένα Αρχεία] σε μια κανονικοποιημένη δομή, της οποίας τα φύλλα είναι αντικείμενα [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Επιστρέφει ένα cookie ή `null` εάν δεν υπάρχει. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Επιστρέφει όλα τα cookies. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Επιστρέφει τη μέθοδο HTTP με την οποία έγινε το αίτημα. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Ελέγχει τη μέθοδο HTTP με την οποία έγινε το αίτημα. Η παράμετρος δεν κάνει διάκριση πεζών-κεφαλαίων. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Επιστρέφει μια κεφαλίδα HTTP ή `null` εάν δεν υπάρχει. Η παράμετρος δεν κάνει διάκριση πεζών-κεφαλαίων. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Επιστρέφει όλες τις κεφαλίδες HTTP ως συσχετιστικό πίνακα. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Είναι η σύνδεση κρυπτογραφημένη (HTTPS); Μπορεί να χρειαστεί να [ρυθμίσετε έναν proxy |configuration#HTTP proxy] για σωστή λειτουργία. - - -isSameSite(): bool .[method] ----------------------------- -Προέρχεται το αίτημα από το ίδιο (υπο)domain και ξεκίνησε κάνοντας κλικ σε έναν σύνδεσμο; Το Nette χρησιμοποιεί το cookie `_nss` (παλαιότερα `nette-samesite`) για ανίχνευση. - - -isAjax(): bool .[method] ------------------------- -Είναι αυτό ένα αίτημα AJAX; - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Επιστρέφει τη διεύθυνση IP του χρήστη. Μπορεί να χρειαστεί να [ρυθμίσετε έναν proxy |configuration#HTTP proxy] για σωστή λειτουργία. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Επιστρέφει τη μετάφραση DNS της διεύθυνσης IP του χρήστη. Μπορεί να χρειαστεί να [ρυθμίσετε έναν proxy |configuration#HTTP proxy] για σωστή λειτουργία. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Επιστρέφει τα διαπιστευτήρια ελέγχου ταυτότητας για [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Επιστρέφει το σώμα του αιτήματος HTTP. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Ανιχνεύει τη γλώσσα. Ως παράμετρο `$lang`, περνάμε έναν πίνακα με τις γλώσσες που υποστηρίζει η εφαρμογή, και επιστρέφει αυτή που θα προτιμούσε να δει ο περιηγητής του επισκέπτη. Δεν είναι μαγεία, απλά χρησιμοποιεί την κεφαλίδα `Accept-Language`. Εάν δεν βρεθεί αντιστοιχία, επιστρέφει `null`. - -```php -// ο περιηγητής στέλνει π.χ. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'en']; // γλώσσες που υποστηρίζονται από την εφαρμογή -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -Η κλάση [api:Nette\Http\RequestFactory] χρησιμοποιείται για τη δημιουργία μιας παρουσίας του `Nette\Http\Request`, η οποία αντιπροσωπεύει το τρέχον αίτημα HTTP. (Εάν εργάζεστε με το Nette, το αντικείμενο αιτήματος HTTP δημιουργείται αυτόματα από το framework.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -Η μέθοδος `fromGlobals()` δημιουργεί το αντικείμενο αιτήματος με βάση τις τρέχουσες καθολικές μεταβλητές της PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` και `$_SERVER`). Κατά τη δημιουργία του αντικειμένου, καθαρίζει αυτόματα όλες τις παραμέτρους εισόδου GET, POST, COOKIE, καθώς και το URL από χαρακτήρες ελέγχου και μη έγκυρες ακολουθίες UTF-8, γεγονός που διασφαλίζει την ασφάλεια κατά την περαιτέρω εργασία με αυτά τα δεδομένα. - -Το RequestFactory μπορεί να διαμορφωθεί πριν από την κλήση του `fromGlobals()`: - -- με τη μέθοδο `$factory->setBinary()`, απενεργοποιείτε τον αυτόματο καθαρισμό των παραμέτρων εισόδου από χαρακτήρες ελέγχου και μη έγκυρες ακολουθίες UTF-8. -- με τη μέθοδο `$factory->setProxy(...)`, καθορίζετε τη διεύθυνση IP του [proxy server |configuration#HTTP proxy], η οποία είναι απαραίτητη για τη σωστή ανίχνευση της διεύθυνσης IP του χρήστη. - -Το RequestFactory επιτρέπει τον ορισμό φίλτρων που μετασχηματίζουν αυτόματα τμήματα του URL του αιτήματος. Αυτά τα φίλτρα αφαιρούν ανεπιθύμητους χαρακτήρες από το URL, οι οποίοι μπορεί να έχουν εισαχθεί εκεί, για παράδειγμα, από λανθασμένη υλοποίηση συστημάτων σχολιασμού σε διάφορους ιστότοπους: - -```php -// αφαίρεση κενών από τη διαδρομή -$requestFactory->urlFilters['path']['%20'] = ''; - -// αφαίρεση τελείας, κόμματος ή δεξιάς παρένθεσης από το τέλος του URI -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// καθαρισμός της διαδρομής από διπλές καθέτους (προεπιλεγμένο φίλτρο) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Το πρώτο κλειδί `'path'` ή `'url'` καθορίζει σε ποιο τμήμα του URL θα εφαρμοστεί το φίλτρο. Το δεύτερο κλειδί είναι η κανονική έκφραση που πρέπει να βρεθεί, και η τιμή είναι η αντικατάσταση που θα χρησιμοποιηθεί αντί για το κείμενο που βρέθηκε. - - -Ανεβασμένα Αρχεία -================= - -Η μέθοδος `Nette\Http\Request::getFiles()` επιστρέφει έναν πίνακα όλων των ανεβασμάτων σε μια κανονικοποιημένη δομή, της οποίας τα φύλλα είναι αντικείμενα [api:Nette\Http\FileUpload]. Αυτά ενσωματώνουν τα δεδομένα που αποστέλλονται από το στοιχείο φόρμας `<input type=file>`. - -Η δομή αντικατοπτρίζει την ονομασία των στοιχείων στο HTML. Στην απλούστερη περίπτωση, μπορεί να είναι ένα μόνο ονομασμένο στοιχείο φόρμας που αποστέλλεται ως: - -```latte -<input type="file" name="avatar"> -``` - -Σε αυτή την περίπτωση, το `$request->getFiles()` επιστρέφει έναν πίνακα: - -```php -[ - 'avatar' => /* Παράδειγμα FileUpload */ -] -``` - -Το αντικείμενο `FileUpload` δημιουργείται ακόμη και αν ο χρήστης δεν ανέβασε κανένα αρχείο ή το ανέβασμα απέτυχε. Η μέθοδος `hasFile()` επιστρέφει εάν ένα αρχείο ανέβηκε: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -Στην περίπτωση ενός ονόματος στοιχείου που χρησιμοποιεί σημειογραφία πίνακα: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -το επιστρεφόμενο δέντρο μοιάζει με αυτό: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* Παράδειγμα FileUpload */ - ], - ], -] -``` - -Μπορείτε επίσης να δημιουργήσετε έναν πίνακα αρχείων: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -Σε αυτή την περίπτωση, η δομή μοιάζει με αυτό: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* Παράδειγμα FileUpload */, - 1 => /* Παράδειγμα FileUpload */, - 2 => /* Παράδειγμα FileUpload */, - ], - ], - ], -] -``` - -Η πρόσβαση στο ευρετήριο 1 του ένθετου πίνακα γίνεται καλύτερα ως εξής: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Επειδή δεν μπορείτε να εμπιστευτείτε δεδομένα από έξω και επομένως ούτε να βασιστείτε στη μορφή της δομής των αρχείων, αυτή η μέθοδος είναι ασφαλέστερη από, για παράδειγμα, `$request->getFiles()['my-form']['details']['avatars'][1]`, η οποία μπορεί να αποτύχει. - - -Επισκόπηση των μεθόδων `FileUpload` .{toc: FileUpload} ------------------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Επιστρέφει `true` εάν ο χρήστης ανέβασε κάποιο αρχείο. - - -isOk(): bool .[method] ----------------------- -Επιστρέφει `true` εάν το αρχείο ανέβηκε με επιτυχία. - - -getError(): int .[method] -------------------------- -Επιστρέφει τον κωδικό σφάλματος κατά το ανέβασμα του αρχείου. Είναι μία από τις σταθερές [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. Εάν το ανέβασμα ήταν επιτυχές, επιστρέφει `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Μετακινεί το ανεβασμένο αρχείο σε νέα τοποθεσία. Εάν το αρχείο προορισμού υπάρχει ήδη, θα αντικατασταθεί. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Επιστρέφει τα περιεχόμενα του ανεβασμένου αρχείου. Εάν το ανέβασμα δεν ήταν επιτυχές, επιστρέφει `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Ανιχνεύει τον τύπο περιεχομένου MIME του ανεβασμένου αρχείου με βάση την υπογραφή του. Εάν το ανέβασμα δεν ήταν επιτυχές ή η ανίχνευση απέτυχε, επιστρέφει `null`. - -.[caution] -Απαιτεί την επέκταση PHP `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Επιστρέφει το αρχικό όνομα του αρχείου, όπως στάλθηκε από τον περιηγητή. - -.[caution] -Μην εμπιστεύεστε την τιμή που επιστρέφεται από αυτή τη μέθοδο. Ο πελάτης θα μπορούσε να έχει στείλει ένα κακόβουλο όνομα αρχείου με σκοπό να βλάψει ή να παραβιάσει την εφαρμογή σας. - - -getSanitizedName(): string .[method] ------------------------------------- -Επιστρέφει το εξυγιασμένο όνομα αρχείου. Περιέχει μόνο χαρακτήρες ASCII `[a-zA-Z0-9.-]`. Εάν το όνομα δεν περιέχει τέτοιους χαρακτήρες, επιστρέφει `'unknown'`. Εάν το αρχείο είναι εικόνα σε μορφή JPEG, PNG, GIF, WebP ή AVIF, επιστρέφει επίσης τη σωστή επέκταση. - -.[caution] -Απαιτεί την επέκταση PHP `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Επιστρέφει την κατάλληλη επέκταση αρχείου (χωρίς την τελεία) που αντιστοιχεί στον ανιχνευμένο τύπο MIME. - -.[caution] -Απαιτεί την επέκταση PHP `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Επιστρέφει την αρχική διαδρομή του αρχείου, όπως στάλθηκε από τον περιηγητή κατά το ανέβασμα ενός φακέλου. Η πλήρης διαδρομή είναι διαθέσιμη μόνο σε PHP 8.1 και νεότερες εκδόσεις. Σε προηγούμενες εκδόσεις, αυτή η μέθοδος επιστρέφει το αρχικό όνομα αρχείου. - -.[caution] -Μην εμπιστεύεστε την τιμή που επιστρέφεται από αυτή τη μέθοδο. Ο πελάτης θα μπορούσε να έχει στείλει ένα κακόβουλο όνομα αρχείου με σκοπό να βλάψει ή να παραβιάσει την εφαρμογή σας. - - -getSize(): int .[method] ------------------------- -Επιστρέφει το μέγεθος του ανεβασμένου αρχείου. Εάν το ανέβασμα δεν ήταν επιτυχές, επιστρέφει `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Επιστρέφει τη διαδρομή προς την προσωρινή τοποθεσία του ανεβασμένου αρχείου. Εάν το ανέβασμα δεν ήταν επιτυχές, επιστρέφει `''`. - - -isImage(): bool .[method] -------------------------- -Επιστρέφει `true` εάν το ανεβασμένο αρχείο είναι εικόνα σε μορφή JPEG, PNG, GIF, WebP ή AVIF. Η ανίχνευση βασίζεται στην υπογραφή του και δεν επαληθεύει την ακεραιότητα ολόκληρου του αρχείου. Το αν μια εικόνα είναι κατεστραμμένη μπορεί να προσδιοριστεί, για παράδειγμα, προσπαθώντας να την [φορτώσετε |#toImage]. - -.[caution] -Απαιτεί την επέκταση PHP `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Επιστρέφει ένα ζεύγος `[πλάτος, ύψος]` με τις διαστάσεις της ανεβασμένης εικόνας. Εάν το ανέβασμα δεν ήταν επιτυχές ή δεν είναι έγκυρη εικόνα, επιστρέφει `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Φορτώνει την εικόνα ως αντικείμενο [Image|utils:images]. Εάν το ανέβασμα δεν ήταν επιτυχές ή δεν είναι έγκυρη εικόνα, δημιουργεί μια εξαίρεση `Nette\Utils\ImageException`. diff --git a/http/el/response.texy b/http/el/response.texy deleted file mode 100644 index 0d3eea73ab..0000000000 --- a/http/el/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -Απόκριση HTTP -************* - -.[perex] -Το Nette ενσωματώνει την απόκριση HTTP σε αντικείμενα με ένα κατανοητό API. - -Η απόκριση HTTP αντιπροσωπεύεται από το αντικείμενο [api:Nette\Http\Response]. Εάν εργάζεστε με το Nette, αυτό το αντικείμενο δημιουργείται αυτόματα από το framework και μπορείτε να το λάβετε μέσω [έγχυσης εξάρτησης |dependency-injection:passing-dependencies]. Στους presenters, απλά καλέστε τη μέθοδο `$this->getHttpResponse()`. - -→ [Εγκατάσταση και απαιτήσεις |@home#Εγκατάσταση] - - -Nette\Http\Response -=================== - -Το αντικείμενο, σε αντίθεση με το [Nette\Http\Request|request], είναι μεταβλητό (mutable), οπότε μπορείτε να αλλάξετε την κατάσταση χρησιμοποιώντας setters, π.χ. να στείλετε κεφαλίδες. Θυμηθείτε ότι όλοι οι setters πρέπει να κληθούν **πριν από την αποστολή οποιασδήποτε εξόδου.** Η μέθοδος `isSent()` υποδεικνύει εάν η έξοδος έχει ήδη σταλεί. Εάν επιστρέφει `true`, κάθε προσπάθεια αποστολής κεφαλίδας θα προκαλέσει μια εξαίρεση `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Αλλάζει τον [κωδικό κατάστασης της απόκρισης |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Για καλύτερη κατανόηση του πηγαίου κώδικα, συνιστούμε τη χρήση [προκαθορισμένων σταθερών |api:Nette\Http\IResponse] αντί για αριθμούς για τον κωδικό. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Επιστρέφει τον κωδικό κατάστασης της απόκρισης. - - -isSent(): bool .[method] ------------------------- -Επιστρέφει εάν οι κεφαλίδες έχουν ήδη σταλεί από τον διακομιστή στον περιηγητή, και επομένως δεν είναι πλέον δυνατό να σταλούν κεφαλίδες ή να αλλάξει ο κωδικός κατάστασης. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Στέλνει μια κεφαλίδα HTTP και **αντικαθιστά** μια προηγουμένως σταλμένη κεφαλίδα με το ίδιο όνομα. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Στέλνει μια κεφαλίδα HTTP και **δεν αντικαθιστά** μια προηγουμένως σταλμένη κεφαλίδα με το ίδιο όνομα. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Διαγράφει μια προηγουμένως σταλμένη κεφαλίδα HTTP. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Επιστρέφει μια σταλμένη κεφαλίδα HTTP ή `null` εάν δεν υπάρχει. Η παράμετρος δεν κάνει διάκριση πεζών-κεφαλαίων. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Επιστρέφει όλες τις σταλμένες κεφαλίδες HTTP ως συσχετιστικό πίνακα. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Αλλάζει την κεφαλίδα `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Ανακατευθύνει σε άλλο URL. Μην ξεχάσετε να τερματίσετε το σενάριο μετά. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Ορίζει τη λήξη του εγγράφου HTTP χρησιμοποιώντας τις κεφαλίδες `Cache-Control` και `Expires`. Η παράμετρος είναι είτε ένα χρονικό διάστημα (ως κείμενο) είτε `null`, το οποίο απενεργοποιεί την προσωρινή αποθήκευση. - -```php -// η cache στον περιηγητή θα λήξει σε μία ώρα -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -Η απόκριση θα ληφθεί μέσω του διαλόγου *Αποθήκευση ως* με το καθορισμένο όνομα. Δεν στέλνει το ίδιο το αρχείο. - -```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] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Στέλνει ένα cookie. Οι προεπιλεγμένες τιμές των παραμέτρων είναι: - -| `$path` | `'/'` | το cookie έχει εμβέλεια σε όλες τις διαδρομές στο (υπο)domain *(διαμορφώσιμο)* -| `$domain` | `null` | που σημαίνει με εμβέλεια στο τρέχον (υπο)domain, αλλά όχι στα υποdomains του *(διαμορφώσιμο)* -| `$secure` | `true` | εάν ο ιστότοπος εκτελείται σε HTTPS, διαφορετικά `false` *(διαμορφώσιμο)* -| `$httpOnly` | `true` | το cookie δεν είναι προσβάσιμο από JavaScript -| `$sameSite` | `'Lax'` | το cookie μπορεί να μην αποσταλεί κατά την [πρόσβαση από άλλο domain |nette:glossary#SameSite cookie] - -Μπορείτε να αλλάξετε τις προεπιλεγμένες τιμές των παραμέτρων `$path`, `$domain` και `$secure` στην [διαμόρφωση |configuration#HTTP cookie]. - -Ο χρόνος μπορεί να καθοριστεί ως αριθμός δευτερολέπτων ή ως συμβολοσειρά: - -```php -$httpResponse->setCookie('lang', 'el', '100 days'); -``` - -Η παράμετρος `$domain` καθορίζει ποια domains μπορούν να δέχονται cookies. Εάν δεν καθοριστεί, το cookie γίνεται αποδεκτό από το ίδιο (υπο)domain που το όρισε, αλλά όχι από τα υποdomains του. Εάν το `$domain` καθοριστεί, περιλαμβάνονται και τα υποdomains. Επομένως, ο καθορισμός του `$domain` είναι λιγότερο περιοριστικός από την παράλειψή του. Για παράδειγμα, με `$domain = 'nette.org'`, τα cookies είναι επίσης διαθέσιμα σε όλα τα υποdomains όπως το `doc.nette.org`. - -Για την τιμή `$sameSite`, μπορείτε να χρησιμοποιήσετε τις σταθερές `Response::SameSiteLax`, `SameSiteStrict` και `SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Διαγράφει ένα cookie. Οι προεπιλεγμένες τιμές των παραμέτρων είναι: -- `$path` με εμβέλεια σε όλους τους καταλόγους (`'/'`) -- `$domain` με εμβέλεια στο τρέχον (υπο)domain, αλλά όχι στα υποdomains του -- `$secure` καθορίζεται από τις ρυθμίσεις στην [διαμόρφωση |configuration#HTTP cookie] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/el/sessions.texy b/http/el/sessions.texy deleted file mode 100644 index d5d76fc14e..0000000000 --- a/http/el/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Sessions -******** - -<div class=perex> - -Το HTTP είναι ένα πρωτόκολλο χωρίς κατάσταση, αλλά σχεδόν κάθε εφαρμογή χρειάζεται να διατηρεί την κατάσταση μεταξύ των αιτημάτων, για παράδειγμα, το περιεχόμενο ενός καλαθιού αγορών. Αυτός είναι ακριβώς ο σκοπός των sessions. Θα δείξουμε: - -- πώς να χρησιμοποιείτε τα sessions -- πώς να αποφύγετε τις συγκρούσεις ονομάτων -- πώς να ορίσετε τη λήξη - -</div> - -Όταν χρησιμοποιείτε sessions, κάθε χρήστης λαμβάνει ένα μοναδικό αναγνωριστικό που ονομάζεται session ID, το οποίο μεταδίδεται σε ένα cookie. Αυτό χρησιμεύει ως κλειδί για τα δεδομένα του session. Σε αντίθεση με τα cookies, τα οποία αποθηκεύονται στην πλευρά του προγράμματος περιήγησης, τα δεδομένα του session αποθηκεύονται στην πλευρά του διακομιστή. - -Ρυθμίζουμε το session στην [διαμόρφωση |configuration#Session], η επιλογή του χρόνου λήξης είναι ιδιαίτερα σημαντική. - -Η διαχείριση του session γίνεται από το αντικείμενο [api:Nette\Http\Session], στο οποίο μπορείτε να αποκτήσετε πρόσβαση ζητώντας το μέσω [έγχυσης εξάρτησης |dependency-injection:passing-dependencies]. Στους presenters, απλά καλέστε `$session = $this->getSession()`. - -→ [Εγκατάσταση και απαιτήσεις |@home#Εγκατάσταση] - - -Έναρξη Session -============== - -Από προεπιλογή, το Nette ξεκινά αυτόματα το session τη στιγμή που αρχίζουμε να διαβάζουμε ή να γράφουμε δεδομένα σε αυτό. Μπορείτε να ξεκινήσετε το session χειροκίνητα χρησιμοποιώντας το `$session->start()`. - -Όταν ξεκινά ένα session, η PHP στέλνει κεφαλίδες HTTP που επηρεάζουν την προσωρινή αποθήκευση, δείτε [php:session_cache_limiter], και ενδεχομένως ένα cookie με το session ID. Επομένως, είναι απαραίτητο να ξεκινάτε πάντα το session πριν στείλετε οποιαδήποτε έξοδο στο πρόγραμμα περιήγησης, διαφορετικά θα προκληθεί εξαίρεση. Εάν γνωρίζετε ότι το session θα χρησιμοποιηθεί κατά την απόδοση της σελίδας, ξεκινήστε το χειροκίνητα εκ των προτέρων, για παράδειγμα, στον presenter. - -Στη λειτουργία ανάπτυξης, το Tracy ξεκινά το session επειδή το χρησιμοποιεί για την εμφάνιση των γραμμών με ανακατευθύνσεις και αιτήματα AJAX στο Tracy Bar. - - -Ενότητες -======== - -Στην καθαρή PHP, ο χώρος αποθήκευσης δεδομένων του session υλοποιείται ως ένας πίνακας προσβάσιμος μέσω της καθολικής μεταβλητής `$_SESSION`. Το πρόβλημα είναι ότι οι εφαρμογές συνήθως αποτελούνται από έναν αριθμό ανεξάρτητων τμημάτων, και εάν όλα έχουν πρόσβαση μόνο σε έναν πίνακα, αργά ή γρήγορα θα προκύψει σύγκρουση ονομάτων. - -Το Nette Framework λύνει αυτό το πρόβλημα διαιρώντας ολόκληρο τον χώρο σε ενότητες (αντικείμενα [api:Nette\Http\SessionSection]). Κάθε μονάδα χρησιμοποιεί τότε τη δική της ενότητα με ένα μοναδικό όνομα, και δεν μπορεί να προκύψει σύγκρουση. - -Λαμβάνουμε μια ενότητα από το session: - -```php -$section = $session->getSection('μοναδικό όνομα'); -``` - -Στον presenter, απλά χρησιμοποιήστε το `getSession()` με μια παράμετρο: - -```php -// $this είναι ένας Presenter -$section = $this->getSession('μοναδικό όνομα'); -``` - -Η ύπαρξη μιας ενότητας μπορεί να ελεγχθεί με τη μέθοδο `$session->hasSection('μοναδικό όνομα')`. - -Η εργασία με την ίδια την ενότητα είναι τότε πολύ εύκολη χρησιμοποιώντας τις μεθόδους `set()`, `get()` και `remove()`: - -```php -// εγγραφή μεταβλητής -$section->set('userName', 'franta'); - -// ανάγνωση μεταβλητής, επιστρέφει null εάν δεν υπάρχει -echo $section->get('userName'); - -// διαγραφή μεταβλητής -$section->remove('userName'); -``` - -Για να λάβετε όλες τις μεταβλητές από την ενότητα, μπορείτε να χρησιμοποιήσετε έναν βρόχο `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Ρύθμιση Λήξης -------------- - -Είναι δυνατό να οριστεί η λήξη για μεμονωμένες ενότητες ή ακόμη και για μεμονωμένες μεταβλητές. Μπορούμε να αφήσουμε τη σύνδεση του χρήστη να λήξει σε 20 λεπτά, αλλά ταυτόχρονα να θυμόμαστε το περιεχόμενο του καλαθιού αγορών. - -```php -// η ενότητα λήγει μετά από 20 λεπτά -$section->setExpiration('20 minutes'); -``` - -Για να ορίσετε τη λήξη μεμονωμένων μεταβλητών, χρησιμοποιήστε την τρίτη παράμετρο της μεθόδου `set()`: - -```php -// η μεταβλητή 'flash' λήγει μετά από 30 δευτερόλεπτα -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Μην ξεχνάτε ότι ο χρόνος λήξης ολόκληρου του session (δείτε [διαμόρφωση session |configuration#Session]) πρέπει να είναι ίσος ή μεγαλύτερος από τον χρόνο που ορίζεται για μεμονωμένες ενότητες ή μεταβλητές. - -Η ακύρωση μιας προηγουμένως ορισμένης λήξης επιτυγχάνεται με τη μέθοδο `removeExpiration()`. Η άμεση ακύρωση ολόκληρης της ενότητας διασφαλίζεται από τη μέθοδο `remove()`. - - -Συμβάντα $onStart, $onBeforeWrite ---------------------------------- - -Το αντικείμενο `Nette\Http\Session` έχει [συμβάντα |nette:glossary#Events] `$onStart` και `$onBeforeWrite`, οπότε μπορείτε να προσθέσετε επανακλήσεις που καλούνται μετά την έναρξη του session ή πριν από την εγγραφή του στον δίσκο και τον επακόλουθο τερματισμό του. - -```php -$session->onBeforeWrite[] = function () { - // γράφουμε δεδομένα στο session - $this->section->set('basket', $this->basket); -}; -``` - - -Διαχείριση Session -================== - -Επισκόπηση των μεθόδων της κλάσης `Nette\Http\Session` για τη διαχείριση του session: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Ξεκινά το session. - - -isStarted(): bool .[method] ---------------------------- -Έχει ξεκινήσει το session; - - -close(): void .[method] ------------------------ -Τερματίζει το session. Το session τερματίζεται αυτόματα στο τέλος της εκτέλεσης του σεναρίου. - - -destroy(): void .[method] -------------------------- -Τερματίζει και διαγράφει το session. - - -exists(): bool .[method] ------------------------- -Περιέχει το αίτημα HTTP ένα cookie με το session ID; - - -regenerateId(): void .[method] ------------------------------- -Δημιουργεί ένα νέο τυχαίο session ID. Τα δεδομένα διατηρούνται. - - -getId(): string .[method] -------------------------- -Επιστρέφει το session ID. - -</div> - - -Διαμόρφωση ----------- - -Ρυθμίζουμε το session στην [διαμόρφωση |configuration#Session]. Εάν γράφετε μια εφαρμογή που δεν χρησιμοποιεί DI container, αυτές οι μέθοδοι χρησιμοποιούνται για τη διαμόρφωση. Πρέπει να κληθούν πριν από την έναρξη του session. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Ορίζει το όνομα του cookie στο οποίο μεταδίδεται το session ID. Το προεπιλεγμένο όνομα είναι `PHPSESSID`. Είναι χρήσιμο εάν εκτελείτε πολλές διαφορετικές εφαρμογές στον ίδιο ιστότοπο. - - -getName(): string .[method] ---------------------------- -Επιστρέφει το όνομα του cookie στο οποίο μεταδίδεται το session ID. - - -setOptions(array $options): static .[method] --------------------------------------------- -Διαμορφώνει το session. Μπορείτε να ορίσετε όλες τις PHP [οδηγίες session |https://www.php.net/manual/en/session.configuration.php] (σε μορφή camelCase, π.χ. αντί για `session.save_path` γράφουμε `savePath`) καθώς και το [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Ορίζει τον χρόνο αδράνειας μετά τον οποίο λήγει το session. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Ρύθμιση παραμέτρων για το cookie. Μπορείτε να αλλάξετε τις προεπιλεγμένες τιμές των παραμέτρων στην [διαμόρφωση |configuration#Session cookie]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Ορίζει τον κατάλογο όπου αποθηκεύονται τα αρχεία session. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Ορίζει έναν προσαρμοσμένο χειριστή, δείτε την [τεκμηρίωση της PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Ασφάλεια Πρώτα -============== - -Ο διακομιστής υποθέτει ότι επικοινωνεί πάντα με τον ίδιο χρήστη, εφόσον τα αιτήματα συνοδεύονται από το ίδιο session ID. Ο ρόλος των μηχανισμών ασφαλείας είναι να διασφαλίσουν ότι αυτό συμβαίνει πραγματικά και ότι δεν είναι δυνατό να κλαπεί ή να πλαστογραφηθεί το αναγνωριστικό. - -Επομένως, το Nette Framework διαμορφώνει σωστά τις οδηγίες PHP έτσι ώστε το session ID να μεταδίδεται μόνο σε cookies, να το καθιστά μη προσβάσιμο από JavaScript και να αγνοεί τυχόν αναγνωριστικά στο URL. Επιπλέον, σε κρίσιμες στιγμές, όπως η σύνδεση του χρήστη, δημιουργεί ένα νέο session ID. - -.[note] -Η συνάρτηση ini_set χρησιμοποιείται για τη διαμόρφωση της PHP, την οποία δυστυχώς ορισμένοι πάροχοι φιλοξενίας απαγορεύουν. Εάν αυτό συμβαίνει και με τον δικό σας πάροχο, προσπαθήστε να διαπραγματευτείτε μαζί του για να σας επιτρέψει τη συνάρτηση ή τουλάχιστον να διαμορφώσει τον διακομιστή. diff --git a/http/el/urls.texy b/http/el/urls.texy deleted file mode 100644 index 129f400099..0000000000 --- a/http/el/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Εργασία με URLs -*************** - -.[perex] -Οι κλάσεις [#Url], [#UrlImmutable] και [#UrlScript] επιτρέπουν την εύκολη δημιουργία, ανάλυση και χειρισμό URLs. - -→ [Εγκατάσταση και απαιτήσεις |@home#Εγκατάσταση] - - -Url -=== - -Η κλάση [api:Nette\Http\Url] επιτρέπει την εύκολη εργασία με URLs και τα μεμονωμένα συστατικά τους, τα οποία αποτυπώνονται σε αυτό το διάγραμμα: - -/--pre - scheme user password host port path query fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -Η δημιουργία URLs είναι διαισθητική: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Μπορείτε επίσης να αναλύσετε ένα URL και να το χειριστείτε περαιτέρω: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -Η κλάση `Url` υλοποιεί τη διεπαφή `JsonSerializable` και έχει μια μέθοδο `__toString()`, οπότε το αντικείμενο μπορεί να εκτυπωθεί ή να χρησιμοποιηθεί σε δεδομένα που περνούν στο `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Συστατικά URL .[method] ------------------------ - -Για την επιστροφή ή την αλλαγή μεμονωμένων συστατικών του URL, είναι διαθέσιμες οι ακόλουθες μέθοδοι: - -.[language-php] -| Setter | Getter | Επιστρεφόμενη τιμή -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | ολόκληρο το URL - -Προειδοποίηση: Όταν εργάζεστε με ένα URL που λαμβάνεται από ένα [αίτημα HTTP|request], λάβετε υπόψη ότι δεν θα περιέχει το fragment, καθώς ο περιηγητής δεν το στέλνει στον διακομιστή. - -Μπορούμε επίσης να εργαστούμε με μεμονωμένες παραμέτρους query χρησιμοποιώντας: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Επιστρέφει το δεξί ή το αριστερό τμήμα του host. Λειτουργεί ως εξής εάν ο host είναι `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)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Επαληθεύει εάν δύο URLs είναι πανομοιότυπα. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Επαληθεύει εάν ένα URL είναι απόλυτο. Ένα URL θεωρείται απόλυτο εάν ξεκινά με ένα σχήμα (π.χ. http, https, ftp) ακολουθούμενο από άνω και κάτω τελεία. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Κανονικοποιεί τη διαδρομή σε ένα URL αφαιρώντας τα ειδικά τμήματα `.` και `..`. Η μέθοδος αφαιρεί τα περιττά στοιχεία διαδρομής με τον ίδιο τρόπο που το κάνουν οι περιηγητές ιστού. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -Η κλάση [api:Nette\Http\UrlImmutable] είναι μια αμετάβλητη (immutable) εναλλακτική της κλάσης [#Url] (παρόμοια με το πώς το `DateTimeImmutable` είναι η αμετάβλητη εναλλακτική του `DateTime` στην PHP). Αντί για setters, έχει τους λεγόμενους withers, οι οποίοι δεν αλλάζουν το αντικείμενο, αλλά επιστρέφουν νέες παρουσίες με την τροποποιημένη τιμή: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -Η κλάση `UrlImmutable` υλοποιεί τη διεπαφή `JsonSerializable` και έχει μια μέθοδο `__toString()`, οπότε το αντικείμενο μπορεί να εκτυπωθεί ή να χρησιμοποιηθεί σε δεδομένα που περνούν στο `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Συστατικά URL .[method] ------------------------ - -Για την επιστροφή ή την αλλαγή μεμονωμένων συστατικών του URL, χρησιμοποιούνται οι ακόλουθες μέθοδοι: - -.[language-php] -| Wither | Getter | Επιστρεφόμενη τιμή -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | ολόκληρο το URL - -Η μέθοδος `withoutUserInfo()` αφαιρεί τα `user` και `password`. - -Μπορούμε επίσης να εργαστούμε με μεμονωμένες παραμέτρους query χρησιμοποιώντας: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Επιστρέφει το δεξί ή το αριστερό τμήμα του host. Λειτουργεί ως εξής εάν ο host είναι `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} ----------------------------------------------------------------------- -Παράγει ένα απόλυτο URL με τον ίδιο τρόπο που ένας περιηγητής επεξεργάζεται συνδέσμους σε μια σελίδα HTML: -- εάν ο σύνδεσμος είναι ένα απόλυτο URL (περιέχει σχήμα), χρησιμοποιείται αμετάβλητος -- εάν ο σύνδεσμος ξεκινά με `//`, λαμβάνεται μόνο το σχήμα από το τρέχον URL -- εάν ο σύνδεσμος ξεκινά με `/`, δημιουργείται μια απόλυτη διαδρομή από τη ρίζα του domain -- σε άλλες περιπτώσεις, το URL δημιουργείται σχετικά με την τρέχουσα διαδρομή - -```php -$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' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Επαληθεύει εάν δύο URLs είναι πανομοιότυπα. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -Η κλάση [api:Nette\Http\UrlScript] είναι απόγονος του [#UrlImmutable] και το επεκτείνει με πρόσθετα εικονικά συστατικά URL, όπως ο ριζικός κατάλογος του έργου κ.λπ. Όπως και η γονική κλάση, είναι ένα αμετάβλητο (immutable) αντικείμενο. - -Το ακόλουθο διάγραμμα δείχνει τα συστατικά που αναγνωρίζει το UrlScript: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` είναι η βασική διεύθυνση URL της εφαρμογής, συμπεριλαμβανομένου του domain και του τμήματος της διαδρομής προς τον ριζικό κατάλογο της εφαρμογής -- `basePath` είναι το τμήμα της διαδρομής προς τον ριζικό κατάλογο της εφαρμογής -- `scriptPath` είναι η διαδρομή προς το τρέχον σενάριο -- `relativePath` είναι το όνομα του σεναρίου (και ενδεχομένως περαιτέρω τμήματα διαδρομής) σχετικά με το basePath -- `relativeUrl` είναι ολόκληρο το τμήμα του URL μετά το baseUrl, συμπεριλαμβανομένης της συμβολοσειράς query και του fragment. -- `pathInfo` είναι ένα τμήμα του URL που χρησιμοποιείται σπάνια σήμερα, μετά το όνομα του σεναρίου - -Για την επιστροφή τμημάτων του URL, είναι διαθέσιμες οι ακόλουθες μέθοδοι: - -.[language-php] -| Getter | Επιστρεφόμενη τιμή -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Συνήθως δεν δημιουργούμε απευθείας αντικείμενα `UrlScript`, αλλά η μέθοδος [Nette\Http\Request::getUrl()|request] τα επιστρέφει με τα συστατικά ήδη σωστά ρυθμισμένα για το τρέχον αίτημα HTTP. diff --git a/http/hu/@home.texy b/http/hu/@home.texy deleted file mode 100644 index 3539e4b800..0000000000 --- a/http/hu/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -A `nette/http` csomag magába foglalja a [HTTP kérést|request] & [választ|response], a [sessionok |sessions] kezelését és az [URL-ek feldolgozását és összeállítását |urls]. - - -Telepítés ---------- - -A könyvtárat a [Composer|best-practices:composer] eszközzel töltheti le és telepítheti: - -```shell -composer require nette/http -``` diff --git a/http/hu/@left-menu.texy b/http/hu/@left-menu.texy deleted file mode 100644 index 09f9a24d93..0000000000 --- a/http/hu/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Bevezetés |@home] -- [HTTP kérés|request] -- [HTTP válasz|response] -- [Sessions |sessions] -- [URL utilities |urls] -- [Konfiguráció |configuration] diff --git a/http/hu/@meta.texy b/http/hu/@meta.texy deleted file mode 100644 index c172d1cda5..0000000000 --- a/http/hu/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette dokumentáció}} diff --git a/http/hu/configuration.texy b/http/hu/configuration.texy deleted file mode 100644 index 9c8242f099..0000000000 --- a/http/hu/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -HTTP konfiguráció -***************** - -.[perex] -A Nette HTTP konfigurációs opcióinak áttekintése. - -Ha nem a teljes keretrendszert használja, csak ezt a könyvtárat, olvassa el, [hogyan kell betölteni a konfigurációt|bootstrap:]. - - -HTTP fejlécek -============= - -```neon -http: - # fejlécek, amelyek minden kéréssel elküldésre kerülnek - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # befolyásolja az X-Frame-Options fejlécet - frames: ... # (string|bool) alapértelmezett 'SAMEORIGIN' -``` - -A keretrendszer biztonsági okokból elküldi az `X-Frame-Options: SAMEORIGIN` fejlécet, amely azt mondja, hogy az oldalt csak akkor lehet megjeleníteni egy másik oldalon belül (az `<iframe>` elemben), ha ugyanazon a domainen található. Ez bizonyos helyzetekben nem kívánatos lehet (például ha Facebook alkalmazást fejleszt), a viselkedés ezért megváltoztatható a `frames: http://allowed-host.com` vagy `frames: true` beállítással. - - -Content Security Policy ------------------------ - -Könnyen összeállíthatók a `Content-Security-Policy` (továbbiakban CSP) fejlécek, leírásukat a [CSP leírásában |https://content-security-policy.com] találja. A CSP direktívák (mint pl. `script-src`) megadhatók akár stringként a specifikáció szerint, akár értékek tömbjeként a jobb olvashatóság érdekében. Ekkor nincs szükség idézőjelek írására a kulcsszavak, mint például a `'self'`, köré. A Nette automatikusan generál egy `nonce` értéket is, így a fejlécben például `'nonce-y4PopTLM=='` lesz. - -```neon -http: - # Content Security Policy - csp: - # string a CSP specifikáció szerinti formátumban - default-src: "'self' https://example.com" - - # értékek tömbje - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool kapcsolók esetén - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -A sablonokban használja a `<script n:nonce>...</script>`-et, és a nonce érték automatikusan kiegészül. Biztonságos webhelyek készítése a Nette-ben valóban egyszerű. - -Hasonlóan összeállíthatók a `Content-Security-Policy-Report-Only` (amelyek a CSP-vel párhuzamosan használhatók) és a [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy] fejlécek is: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -HTTP cookie ------------ - -Megváltoztathatók a [Nette\Http\Response::setCookie() |response#setCookie] metódus és a session egyes paramétereinek alapértelmezett értékei. - -```neon -http: - # cookie hatóköre útvonal szerint - cookiePath: ... # (string) alapértelmezett '/' - - # domainek, amelyek elfogadják a cookie-t - cookieDomain: 'example.com' # (string|domain) alapértelmezett nincs beállítva - - # csak HTTPS-en keresztül küldeni a cookie-t? - cookieSecure: ... # (bool|auto) alapértelmezett auto - - # kikapcsolja a Nette által CSRF védelemként használt cookie küldését - disableNetteCookie: ... # (bool) alapértelmezett false -``` - -A `cookieDomain` attribútum meghatározza, mely domainek fogadhatják el a cookie-t. Ha nincs megadva, a cookie-t ugyanaz a (sub)domain fogadja el, amelyik beállította, *de nem* annak aldomainjei. Ha a `cookieDomain` meg van adva, az aldomainek is beletartoznak. Ezért a `cookieDomain` megadása kevésbé korlátozó, mint annak elhagyása. - -Például a `cookieDomain: nette.org` esetén a cookie-k minden aldomainen, mint például a `doc.nette.org`, is elérhetők. Ugyanezt elérhetjük a speciális `domain` értékkel is, tehát `cookieDomain: domain`. - -A `cookieSecure` attribútum `auto` alapértelmezett értéke azt jelenti, hogy ha a webhely HTTPS-en fut, a cookie-k a `Secure` jelzővel kerülnek elküldésre, és így csak HTTPS-en keresztül lesznek elérhetők. - - -HTTP proxy ----------- - -Ha a webhely HTTP proxy mögött fut, adja meg annak IP címét, hogy a HTTPS-en keresztüli kapcsolat és a kliens IP címének észlelése megfelelően működjön. Tehát hogy a [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] és [isSecured() |request#isSecured] függvények helyes értékeket adjanak vissza, és a sablonokban a linkek `https:` protokollal generálódjanak. - -```neon -http: - # IP cím, tartomány (pl. 127.0.0.1/8) vagy ezen értékek tömbje - proxy: 127.0.0.1 # (string|string[]) alapértelmezett nincs beállítva -``` - - -Session -======= - -Alapvető [session |sessions] beállítások: - -```neon -session: - # session panel megjelenítése a Tracy Bar-ban? - debugger: ... # (bool) alapértelmezett false - - # inaktivitási idő, amely után a session lejár - expiration: 14 days # (string) alapértelmezett '3 hours' - - # mikor kell elindítani a sessiont? - autoStart: ... # (smart|always|never) alapértelmezett 'smart' - - # handler, a SessionHandlerInterface interfészt implementáló szolgáltatás - handler: @handlerService -``` - -Az `autoStart` opció vezérli, hogy mikor kell elindítani a sessiont. Az `always` érték azt jelenti, hogy a session mindig elindul az alkalmazás indításakor. A `smart` érték azt jelenti, hogy a session csak akkor indul el az alkalmazás indításakor, ha már létezik, vagy abban a pillanatban, amikor olvasni vagy írni akarunk belőle. Végül a `never` érték letiltja a session automatikus indítását. - -Továbbá beállíthatók az összes PHP [session direktíva |https://www.php.net/manual/en/session.configuration.php] (camelCase formátumban) és a [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] is. Példa: - -```neon -session: - # 'session.name' írjuk 'name'-ként - name: MYID - - # 'session.save_path' írjuk 'savePath'-ként - savePath: "%tempDir%/sessions" -``` - - -Session cookie --------------- - -A session cookie ugyanazokkal a paraméterekkel kerül elküldésre, mint a [más cookie-k |#HTTP cookie], de ezeket megváltoztathatja számára: - -```neon -session: - # domainek, amelyek elfogadják a cookie-t - cookieDomain: 'example.com' # (string|domain) - - # korlátozások más domainről való hozzáférés esetén - cookieSamesite: None # (Strict|Lax|None) alapértelmezett Lax -``` - -A `cookieSamesite` attribútum befolyásolja, hogy a cookie elküldésre kerül-e [más domainről való hozzáférés |nette:glossary#SameSite cookie] esetén, ami bizonyos védelmet nyújt a [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF) támadások ellen. - - -DI szolgáltatások -================= - -Ezek a szolgáltatások kerülnek hozzáadásra a DI konténerhez: - -| Név | Típus | Leírás -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP kérés| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP válasz| response] -| `session.session`| [api:Nette\Http\Session] | [session kezelés| sessions] diff --git a/http/hu/request.texy b/http/hu/request.texy deleted file mode 100644 index c3bac10ef1..0000000000 --- a/http/hu/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -HTTP kérés -********** - -.[perex] -A Nette a HTTP kérést érthető API-val rendelkező objektumokba zárja, és egyúttal szanitizáló szűrőt is biztosít. - -A HTTP kérést a [api:Nette\Http\Request] objektum képviseli. Ha a Nette-tel dolgozik, ezt az objektumot a keretrendszer automatikusan létrehozza, és [dependency injection |dependency-injection:passing-dependencies] segítségével átadhatja magának. A presenterekben elég csak a `$this->getHttpRequest()` metódust meghívni. Ha a Nette Frameworkön kívül dolgozik, létrehozhatja az objektumot a [#RequestFactory] segítségével. - -A Nette nagy előnye, hogy az objektum létrehozásakor automatikusan megtisztítja az összes GET, POST, COOKIE bemeneti paramétert, valamint az URL-t a vezérlőkarakterektől és az érvénytelen UTF-8 szekvenciáktól. Ezekkel az adatokkal ezután biztonságosan dolgozhat tovább. A megtisztított adatokat ezután a presenterekben és az űrlapokban használják. - -→ [Telepítés és követelmények |@home#Telepítés] - - -Nette\Http\Request -================== - -Ez az objektum immutable (megváltoztathatatlan). Nincsenek setterei, csak egy ún. wither `withUrl()` metódusa van, amely nem változtatja meg az objektumot, hanem egy új példányt ad vissza megváltozott értékkel. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Egy klónt ad vissza más URL-lel. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Visszaadja a kérés URL-jét [UrlScript |urls#UrlScript] objektumként. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Figyelmeztetés: a böngészők nem küldik el a fragmentet a szerverre, így a `$url->getFragment()` üres stringet fog visszaadni. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Visszaadja a GET kérés paramétereit. - -```php -$all = $httpRequest->getQuery(); // visszaadja az összes paraméter tömbjét az URL-ből -$id = $httpRequest->getQuery('id'); // visszaadja a 'id' GET paramétert (vagy null-t) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Visszaadja a POST kérés paramétereit. - -```php -$all = $httpRequest->getPost(); // visszaadja az összes paraméter tömbjét a POST-ból -$id = $httpRequest->getPost('id'); // visszaadja a 'id' POST paramétert (vagy null-t) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Visszaadja a [feltöltést |#Feltöltött fájlok] [api:Nette\Http\FileUpload] objektumként: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // feltöltöttek valamilyen fájlt? - $file->getUntrustedName(); // a felhasználó által küldött fájlnév - $file->getSanitizedName(); // név veszélyes karakterek nélkül -} -``` - -A beágyazott struktúrához való hozzáféréshez adjon meg egy kulcsokból álló tömböt. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Mivel nem lehet megbízni a kívülről érkező adatokban, és így a fájlok struktúrájának formájában sem, ez a módszer biztonságosabb, mint például a `$request->getFiles()['my-form']['details']['avatar']`, amely meghiúsulhat. - - -getFiles(): array .[method] ---------------------------- -Visszaadja az [összes feltöltés |#Feltöltött fájlok] fáját normalizált struktúrában, amelynek levelei [api:Nette\Http\FileUpload] objektumok: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Visszaadja a cookie-t vagy `null`-t, ha nem létezik. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Visszaadja az összes cookie-t. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Visszaadja a HTTP metódust, amellyel a kérés történt. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Teszteli a HTTP metódust, amellyel a kérés történt. A paraméter kis- és nagybetű érzéketlen. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Visszaadja a HTTP fejlécet vagy `null`-t, ha nem létezik. A paraméter kis- és nagybetű érzéketlen. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Visszaadja az összes HTTP fejlécet asszociatív tömbként. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Titkosított a kapcsolat (HTTPS)? A megfelelő működéshez szükség lehet a [proxy beállítására |configuration#HTTP proxy]. - - -isSameSite(): bool .[method] ----------------------------- -Ugyanarról a (sub)domainről érkezik a kérés, és egy linkre kattintással indították? A Nette a `_nss` cookie-t (korábban `nette-samesite`) használja az észleléshez. - - -isAjax(): bool .[method] ------------------------- -AJAX kérésről van szó? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Visszaadja a felhasználó IP címét. A megfelelő működéshez szükség lehet a [proxy beállítására |configuration#HTTP proxy]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Visszaadja a felhasználó IP címének DNS fordítását. A megfelelő működéshez szükség lehet a [proxy beállítására |configuration#HTTP proxy]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Visszaadja a [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication] hitelesítési adatait. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Visszaadja a HTTP kérés törzsét. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Észleli a nyelvet. Paraméterként `$lang` átadjuk az alkalmazás által támogatott nyelvek tömbjét, és visszaadja azt, amelyet a látogató böngészője legszívesebben látna. Ez nem varázslat, csak az `Accept-Language` fejlécet használja. Ha nincs egyezés, `null`-t ad vissza. - -```php -// a böngésző pl. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 küld - -$langs = ['hu', 'pl', 'en']; // az alkalmazás által támogatott nyelvek -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -A [api:Nette\Http\RequestFactory] osztály egy `Nette\Http\Request` példány létrehozására szolgál, amely az aktuális HTTP kérést reprezentálja. (Ha a Nette-tel dolgozik, a HTTP kérés objektumot a keretrendszer automatikusan létrehozza.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -A `fromGlobals()` metódus létrehozza a kérés objektumot az aktuális PHP globális változók (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` és `$_SERVER`) alapján. Az objektum létrehozásakor automatikusan megtisztítja az összes GET, POST, COOKIE bemeneti paramétert, valamint az URL-t a vezérlőkarakterektől és az érvénytelen UTF-8 szekvenciáktól, ami biztosítja a biztonságot ezen adatok további feldolgozása során. - -A RequestFactory konfigurálható a `fromGlobals()` meghívása előtt: - -- a `$factory->setBinary()` metódussal kikapcsolhatja a bemeneti paraméterek automatikus tisztítását a vezérlőkarakterektől és az érvénytelen UTF-8 szekvenciáktól. -- a `$factory->setProxy(...)` metódussal megadhatja a [proxy szerver |configuration#HTTP proxy] IP címét, ami szükséges a felhasználó IP címének helyes észleléséhez. - -A RequestFactory lehetővé teszi szűrők definiálását, amelyek automatikusan átalakítják a kérés URL-jének részeit. Ezek a szűrők eltávolítják a nem kívánt karaktereket az URL-ből, amelyeket például a különböző webhelyeken lévő kommentrendszerek helytelen implementációja miatt helyezhettek oda: - -```php -// szóközök eltávolítása az útvonalból -$requestFactory->urlFilters['path']['%20'] = ''; - -// pont, vessző vagy jobb zárójel eltávolítása az URI végéről -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// az útvonal tisztítása a dupla perjelektől (alapértelmezett szűrő) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Az első kulcs, a `'path'` vagy `'url'`, meghatározza, hogy a szűrő az URL melyik részére vonatkozik. A második kulcs a keresendő reguláris kifejezés, az érték pedig a helyettesítés, amelyet a talált szöveg helyett használnak. - - -Feltöltött fájlok -================= - -A `Nette\Http\Request::getFiles()` metódus visszaadja az összes feltöltés tömbjét normalizált struktúrában, amelynek levelei [api:Nette\Http\FileUpload] objektumok. Ezek az `<input type=file>` űrlap elem által küldött adatokat zárják magukba. - -A struktúra tükrözi az elemek elnevezését a HTML-ben. A legegyszerűbb esetben ez egyetlen elnevezett űrlap elem lehet, amelyet így küldtek: - -```latte -<input type="file" name="avatar"> -``` - -Ebben az esetben a `$request->getFiles()` a következő tömböt adja vissza: - -```php -[ - 'avatar' => /* FileUpload instance */ -] -``` - -A `FileUpload` objektum akkor is létrejön, ha a felhasználó nem küldött fájlt, vagy a küldés sikertelen volt. Azt, hogy a fájl elküldésre került-e, a `hasFile()` metódus adja vissza: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -Ha az elem neve tömb jelölést használ: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -a visszaadott fa így néz ki: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* FileUpload instance */ - ], - ], -] -``` - -Létrehozhatunk fájlok tömbjét is: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -Ebben az esetben a struktúra így néz ki: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, - ], - ], - ], -] -``` - -A beágyazott tömb 1-es indexéhez való hozzáférés a legjobb módja a következő: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Mivel nem lehet megbízni a kívülről érkező adatokban, és így a fájlok struktúrájának formájában sem, ez a módszer biztonságosabb, mint például a `$request->getFiles()['my-form']['details']['avatars'][1]`, amely meghiúsulhat. - - -A `FileUpload` metódusainak áttekintése .{toc: FileUpload} ----------------------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Visszaadja a `true` értéket, ha a felhasználó feltöltött valamilyen fájlt. - - -isOk(): bool .[method] ----------------------- -Visszaadja a `true` értéket, ha a fájl sikeresen feltöltésre került. - - -getError(): int .[method] -------------------------- -Visszaadja a fájlfeltöltés hibakódját. Ez az egyik [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php] konstans. Ha a feltöltés rendben lezajlott, `UPLOAD_ERR_OK`-t ad vissza. - - -move(string $dest) .[method] ----------------------------- -Áthelyezi a feltöltött fájlt egy új helyre. Ha a célfájl már létezik, felülíródik. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Visszaadja a feltöltött fájl tartalmát. Ha a feltöltés sikertelen volt, `null`-t ad vissza. - - -getContentType(): ?string .[method] ------------------------------------ -Észleli a feltöltött fájl MIME content type-ját az aláírása alapján. Ha a feltöltés sikertelen volt, vagy az észlelés nem sikerült, `null`-t ad vissza. - -.[caution] -Szükséges a `fileinfo` PHP kiterjesztés. - - -getUntrustedName(): string .[method] ------------------------------------- -Visszaadja a fájl eredeti nevét, ahogy a böngésző küldte. - -.[caution] -Ne bízzon a metódus által visszaadott értékben. A kliens rosszindulatú fájlnevet küldhetett azzal a szándékkal, hogy károsítsa vagy feltörje az alkalmazását. - - -getSanitizedName(): string .[method] ------------------------------------- -Visszaadja a szanitizált fájlnevet. Csak ASCII karaktereket `[a-zA-Z0-9.-]` tartalmaz. Ha a név nem tartalmaz ilyen karaktereket, `'unknown'`-t ad vissza. Ha a fájl JPEG, PNG, GIF, WebP vagy AVIF formátumú kép, akkor a helyes kiterjesztést is visszaadja. - -.[caution] -Szükséges a `fileinfo` PHP kiterjesztés. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Visszaadja a fájl megfelelő kiterjesztését (pont nélkül), amely megfelel az észlelt MIME típusnak. - -.[caution] -Szükséges a `fileinfo` PHP kiterjesztés. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Visszaadja a fájl eredeti elérési útját, ahogy a böngésző küldte a mappa feltöltésekor. A teljes elérési út csak PHP 8.1 és újabb verziókban érhető el. Korábbi verziókban ez a metódus az eredeti fájlnevet adja vissza. - -.[caution] -Ne bízzon a metódus által visszaadott értékben. A kliens rosszindulatú fájlnevet küldhetett azzal a szándékkal, hogy károsítsa vagy feltörje az alkalmazását. - - -getSize(): int .[method] ------------------------- -Visszaadja a feltöltött fájl méretét. Ha a feltöltés sikertelen volt, `0`-t ad vissza. - - -getTemporaryFile(): string .[method] ------------------------------------- -Visszaadja a feltöltött fájl ideiglenes helyének elérési útját. Ha a feltöltés sikertelen volt, `''`-t ad vissza. - - -isImage(): bool .[method] -------------------------- -Visszaadja a `true` értéket, ha a feltöltött fájl JPEG, PNG, GIF, WebP vagy AVIF formátumú kép. Az észlelés az aláírása alapján történik, és nem ellenőrzi az egész fájl integritását. Azt, hogy a kép nem sérült-e, például a [betöltésével |#toImage] lehet megállapítani. - -.[caution] -Szükséges a `fileinfo` PHP kiterjesztés. - - -getImageSize(): ?array .[method] --------------------------------- -Visszaadja a `[szélesség, magasság]` párt a feltöltött kép méreteivel. Ha a feltöltés sikertelen volt, vagy nem érvényes képről van szó, `null`-t ad vissza. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Betölti a képet [Image|utils:images] objektumként. Ha a feltöltés sikertelen volt, vagy nem érvényes képről van szó, `Nette\Utils\ImageException` kivételt dob. diff --git a/http/hu/response.texy b/http/hu/response.texy deleted file mode 100644 index 96f9da71cb..0000000000 --- a/http/hu/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -HTTP válasz -*********** - -.[perex] -A Nette a HTTP választ érthető API-val rendelkező objektumokba zárja. - -A HTTP választ a [api:Nette\Http\Response] objektum képviseli. Ha a Nette-tel dolgozik, ezt az objektumot a keretrendszer automatikusan létrehozza, és [dependency injection |dependency-injection:passing-dependencies] segítségével átadhatja magának. A presenterekben elég csak a `$this->getHttpResponse()` metódust meghívni. - -→ [Telepítés és követelmények |@home#Telepítés] - - -Nette\Http\Response -=================== - -Az objektum, ellentétben a [Nette\Http\Request|request]-tel, mutable (megváltoztatható), tehát setterek segítségével megváltoztathatja az állapotot, például fejléceket küldhet. Ne felejtse el, hogy minden settert **bármilyen kimenet elküldése előtt** kell meghívni. Azt, hogy a kimenet már elküldésre került-e, az `isSent()` metódus árulja el. Ha `true`-t ad vissza, minden fejléc küldési kísérlet `Nette\InvalidStateException` kivételt vált ki. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Megváltoztatja a [válasz állapotkódját |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. A forráskód jobb érthetősége érdekében javasoljuk, hogy a kódhoz számok helyett [előre definiált konstansokat |api:Nette\Http\IResponse] használjon. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Visszaadja a válasz állapotkódját. - - -isSent(): bool .[method] ------------------------- -Visszaadja, hogy a fejlécek már elküldésre kerültek-e a szerverről a böngészőbe, és így már nem lehet fejléceket küldeni vagy az állapotkódot megváltoztatni. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Elküld egy HTTP fejlécet és **felülírja** a korábban elküldött, azonos nevű fejlécet. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Elküld egy HTTP fejlécet és **nem írja felül** a korábban elküldött, azonos nevű fejlécet. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Törli a korábban elküldött HTTP fejlécet. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Visszaadja az elküldött HTTP fejlécet vagy `null`-t, ha ilyen nem létezik. A paraméter kis- és nagybetű érzéketlen. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Visszaadja az összes elküldött HTTP fejlécet asszociatív tömbként. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Megváltoztatja a `Content-Type` fejlécet. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Átirányít egy másik URL-re. Ne felejtse el utána leállítani a szkriptet. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Beállítja a HTTP dokumentum lejáratát a `Cache-Control` és `Expires` fejlécek segítségével. A paraméter vagy egy időintervallum (szövegként), vagy `null`, ami letiltja a gyorsítótárazást. - -```php -// a böngésző gyorsítótára egy óra múlva lejár -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -A választ a *Mentés másként* párbeszédablak segítségével tölti le a megadott néven. Magát a fájlt nem küldi el. - -```php -$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] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Elküld egy cookie-t. A paraméterek alapértelmezett értékei: - -| `$path` | `'/'` | a cookie hatóköre az összes útvonalra kiterjed a (sub)domainen *(konfigurálható)* -| `$domain` | `null` | ami azt jelenti, hogy a hatókör az aktuális (sub)domainre terjed ki, de nem annak aldomainjeire *(konfigurálható)* -| `$secure` | `true` | ha a webhely HTTPS-en fut, egyébként `false` *(konfigurálható)* -| `$httpOnly` | `true` | a cookie JavaScript számára nem hozzáférhető -| `$sameSite` | `'Lax'` | a cookie nem feltétlenül kerül elküldésre [más domainről való hozzáférés |nette:glossary#SameSite cookie] esetén - -A `$path`, `$domain` és `$secure` paraméterek alapértelmezett értékeit megváltoztathatja a [konfigurációban |configuration#HTTP cookie]. - -Az időt megadhatja másodpercek számaként vagy stringként: - -```php -$httpResponse->setCookie('lang', 'cs', '100 days'); -``` - -A `$domain` paraméter meghatározza, mely domainek fogadhatják el a cookie-t. Ha nincs megadva, a cookie-t ugyanaz a (sub)domain fogadja el, amelyik beállította, de nem annak aldomainjei. Ha a `$domain` meg van adva, az aldomainek is beletartoznak. Ezért a `$domain` megadása kevésbé korlátozó, mint annak elhagyása. Például a `$domain = 'nette.org'` esetén a cookie-k minden aldomainen, mint például a `doc.nette.org`, is elérhetők. - -A `$sameSite` értékhez használhatja a `Response::SameSiteLax`, `Response::SameSiteStrict` és `Response::SameSiteNone` konstansokat. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Törli a cookie-t. A paraméterek alapértelmezett értékei: -- `$path` hatókörrel az összes könyvtárra (`'/'`) -- `$domain` hatókörrel az aktuális (sub)domainre, de nem annak aldomainjeire -- `$secure` a [konfigurációban |configuration#HTTP cookie] beállítottak szerint - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/hu/sessions.texy b/http/hu/sessions.texy deleted file mode 100644 index e6719c82d3..0000000000 --- a/http/hu/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Sessionök -********* - -<div class=perex> - -A HTTP egy állapot nélküli protokoll, azonban szinte minden alkalmazásnak szüksége van az állapot megőrzésére a kérések között, például a bevásárlókosár tartalmának megőrzésére. Pontosan erre szolgál a session vagy munkamenet. Megmutatjuk, - -- hogyan használjuk a sessionöket -- hogyan kerüljük el a névütközéseket -- hogyan állítsuk be a lejárati időt - -</div> - -Sessionök használatakor minden felhasználó egyedi azonosítót kap, az úgynevezett session ID-t, amelyet cookie-ban továbbítanak. Ez kulcsként szolgál a session adatokhoz. Ellentétben a cookie-kkal, amelyek a böngésző oldalán tárolódnak, a session adatok a szerver oldalán tárolódnak. - -A sessiont a [konfigurációban |configuration#Session] állítjuk be, különösen fontos a lejárati idő megválasztása. - -A session kezeléséért a [api:Nette\Http\Session] objektum felelős, amelyhez úgy juthat hozzá, hogy [dependency injection |dependency-injection:passing-dependencies] segítségével átadja magának. A presenterekben elég csak a `$session = $this->getSession()` metódust meghívni. - -→ [Telepítés és követelmények |@home#Telepítés] - - -Session indítása -================ - -A Nette alapértelmezés szerint automatikusan elindítja a sessiont abban a pillanatban, amikor elkezdünk olvasni belőle vagy adatokat írni bele. Manuálisan a session a `$session->start()` segítségével indítható el. - -A PHP a session indításakor HTTP fejléceket küld, amelyek befolyásolják a gyorsítótárazást, lásd [php:session_cache_limiter], és adott esetben a session ID-t tartalmazó cookie-t is. Ezért mindig el kell indítani a sessiont még azelőtt, hogy bármilyen kimenetet küldenénk a böngészőbe, különben kivétel váltódik ki. Ha tehát tudja, hogy az oldal megjelenítése során sessiont fog használni, indítsa el manuálisan előtte, például a presenterben. - -Fejlesztői módban a Tracy indítja el a sessiont, mert azt használja az átirányítási és AJAX kérések sávjainak megjelenítésére a Tracy Barban. - - -Szekciók -======== - -Tiszta PHP-ban a session adattárolója egy tömbként valósul meg, amely a `$_SESSION` globális változón keresztül érhető el. A probléma az, hogy az alkalmazások általában számos egymástól független részből állnak, és ha mindegyiknek csak egy tömb áll rendelkezésére, előbb-utóbb névütközés következik be. - -A Nette Framework ezt a problémát úgy oldja meg, hogy az egész teret szekciókra ( [api:Nette\Http\SessionSection] objektumokra) osztja. Minden egység ezután saját, egyedi nevű szekciót használ, és így már nem fordulhat elő ütközés. - -A szekciót a sessionből kapjuk meg: - -```php -$section = $session->getSection('unikatni nazev'); -``` - -A presenterben elég a `getSession()`-t használni paraméterrel: - -```php -// $this egy Presenter -$section = $this->getSession('unikatni nazev'); -``` - -A szekció létezését a `$session->hasSection('unikatni nazev')` metódussal ellenőrizhetjük. - -Magával a szekcióval ezután nagyon egyszerűen dolgozhatunk a `set()`, `get()` és `remove()` metódusokkal: - -```php -// változó írása -$section->set('userName', 'franta'); - -// változó olvasása, null-t ad vissza, ha nem létezik -echo $section->get('userName'); - -// változó törlése -$section->remove('userName'); -``` - -Az összes változó megszerzéséhez a szekcióból használhatjuk a `foreach` ciklust: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Lejárati idő beállítása ------------------------ - -Az egyes szekciókhoz vagy akár egyes változókhoz is beállítható lejárati idő. Így például hagyhatjuk, hogy a felhasználó bejelentkezése 20 perc múlva lejárjon, miközben továbbra is megjegyezzük a kosár tartalmát. - -```php -// a szekció 20 perc múlva lejár -$section->setExpiration('20 minutes'); -``` - -Az egyes változók lejárati idejének beállítására a `set()` metódus harmadik paramétere szolgál: - -```php -// a 'flash' változó már 30 másodperc múlva lejár -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Ne felejtse el, hogy az egész session lejárati ideje (lásd [session konfiguráció |configuration#Session]) meg kell hogy egyezzen vagy magasabb legyen, mint az egyes szekciókhoz vagy változókhoz beállított idő. - -A korábban beállított lejárati idő törlését a `removeExpiration()` metódussal érhetjük el. Az egész szekció azonnali törlését a `remove()` metódus biztosítja. - - -$onStart, $onBeforeWrite események ----------------------------------- - -A `Nette\Http\Session` objektumnak vannak [$onStart és $onBeforeWrite eseményei |nette:glossary#Eventek események], így hozzáadhat callbackeket, amelyek a session indítása után vagy a lemezre írása és az azt követő befejezése előtt hívódnak meg. - -```php -$session->onBeforeWrite[] = function () { - // adatokat írunk a sessionbe - $this->section->set('basket', $this->basket); -}; -``` - - -Session kezelés -=============== - -A `Nette\Http\Session` osztály metódusainak áttekintése a session kezeléséhez: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Elindítja a sessiont. - - -isStarted(): bool .[method] ---------------------------- -El van indítva a session? - - -close(): void .[method] ------------------------ -Befejezi a sessiont. A session automatikusan befejeződik a szkript futásának végén. - - -destroy(): void .[method] -------------------------- -Befejezi és törli a sessiont. - - -exists(): bool .[method] ------------------------- -Tartalmaz a HTTP kérés cookie-t session ID-vel? - - -regenerateId(): void .[method] ------------------------------- -Új, véletlenszerű session ID-t generál. Az adatok megmaradnak. - - -getId(): string .[method] -------------------------- -Visszaadja a session ID-t. - -</div> - - -Konfiguráció ------------- - -A sessiont a [konfigurációban |configuration#Session] állítjuk be. Ha olyan alkalmazást ír, amely nem használ DI konténert, ezek a metódusok szolgálnak a konfigurációhoz. Ezeket még a session elindítása előtt kell meghívni. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Beállítja annak a cookie-nak a nevét, amelyben a session ID-t továbbítják. A standard név `PHPSESSID`. Hasznos abban az esetben, ha egy webhelyen belül több különböző alkalmazást üzemeltet. - - -getName(): string .[method] ---------------------------- -Visszaadja annak a cookie-nak a nevét, amelyben a session ID-t továbbítják. - - -setOptions(array $options): static .[method] --------------------------------------------- -Konfigurálja a sessiont. Beállíthatók az összes PHP [session direktíva |https://www.php.net/manual/en/session.configuration.php] (camelCase formátumban, pl. `session.save_path` helyett `savePath`-t írunk) és a [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters] is. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Beállítja az inaktivitási időt, amely után a session lejár. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Cookie paraméterek beállítása. A paraméterek alapértelmezett értékeit megváltoztathatja a [konfigurációban |configuration#Session cookie]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Beállítja a könyvtárat, ahová a session fájlok mentésre kerülnek. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Saját handler beállítása, lásd [PHP dokumentáció|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Biztonság mindenekelőtt -======================= - -A szerver feltételezi, hogy ugyanazzal a felhasználóval kommunikál, amíg a kéréseket ugyanaz a session ID kíséri. A biztonsági mechanizmusok feladata annak biztosítása, hogy ez valóban így legyen, és ne lehessen az azonosítót ellopni vagy meghamisítani. - -A Nette Framework ezért helyesen konfigurálja a PHP direktívákat, hogy a session ID-t csak cookie-ban továbbítsa, JavaScript számára hozzáférhetetlenné tegye, és az URL-ben lévő esetleges azonosítókat figyelmen kívül hagyja. Ezenkívül kritikus pillanatokban, mint például a felhasználó bejelentkezésekor, új session ID-t generál. - -.[note] -A PHP konfigurálásához az ini_set függvényt használják, amelyet sajnos néhány hosting szolgáltató letilt. Ha ez az Ön szolgáltatójának esete is, próbáljon meg velük megegyezni, hogy engedélyezzék a függvényt, vagy legalább konfigurálják a szervert. diff --git a/http/hu/urls.texy b/http/hu/urls.texy deleted file mode 100644 index d53b089012..0000000000 --- a/http/hu/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -URL-ekkel való munka -******************** - -.[perex] -Az [#Url], [#UrlImmutable] és [#UrlScript] osztályok lehetővé teszik az URL-ek egyszerű generálását, elemzését és manipulálását. - -→ [Telepítés és követelmények |@home#Telepítés] - - -Url -=== - -A [api:Nette\Http\Url] osztály lehetővé teszi az URL-ekkel és azok egyes komponenseivel való egyszerű munkát, amelyeket ez a rajz ábrázol: - -/--pre - scheme user password host port path query fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -Az URL generálása intuitív: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Lehetőség van az URL elemzésére és további manipulálására is: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -A `Url` osztály implementálja a `JsonSerializable` interfészt, és rendelkezik a `__toString()` metódussal, így az objektumot ki lehet írni, vagy fel lehet használni a `json_encode()`-nak átadott adatokban. - -```php -echo $url; -echo json_encode([$url]); -``` - - -URL komponensek .[method] -------------------------- - -Az URL egyes komponenseinek visszaadására vagy megváltoztatására a következő metódusok állnak rendelkezésre: - -.[language-php] -| Setter | Getter | Visszaadott érték -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | teljes URL - -Figyelmeztetés: Amikor olyan URL-lel dolgozik, amelyet a [HTTP kérésből|request] szereztek be, vegye figyelembe, hogy nem fogja tartalmazni a fragmentet, mivel a böngésző nem küldi el azt a szerverre. - -Az egyes query paraméterekkel is dolgozhatunk a következők segítségével: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Visszaadja a hoszt jobb vagy bal részét. Így működik, ha a hoszt `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)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Ellenőrzi, hogy két URL megegyezik-e. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Ellenőrzi, hogy az URL abszolút-e. Az URL abszolútnak tekintendő, ha sémával kezdődik (pl. http, https, ftp), amelyet kettőspont követ. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Normalizálja az URL elérési útját a speciális `.` és `..` szegmensek eltávolításával. A metódus eltávolítja a felesleges elérési út elemeket ugyanúgy, ahogy a webböngészők teszik. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -A [api:Nette\Http\UrlImmutable] osztály az [#Url] osztály immutable (megváltoztathatatlan) alternatívája (hasonlóan ahhoz, ahogy a PHP-ban a `DateTimeImmutable` a `DateTime` megváltoztathatatlan alternatívája). Setterek helyett ún. withereket használ, amelyek nem változtatják meg az objektumot, hanem új példányokat adnak vissza módosított értékkel: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -A `UrlImmutable` osztály implementálja a `JsonSerializable` interfészt, és rendelkezik a `__toString()` metódussal, így az objektumot ki lehet írni, vagy fel lehet használni a `json_encode()`-nak átadott adatokban. - -```php -echo $url; -echo json_encode([$url]); -``` - - -URL komponensek .[method] -------------------------- - -Az URL egyes komponenseinek visszaadására vagy megváltoztatására a következő metódusok szolgálnak: - -.[language-php] -| Wither | Getter | Visszaadott érték -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | teljes URL - -A `withoutUserInfo()` metódus eltávolítja a `user`-t és a `password`-öt. - -Az egyes query paraméterekkel is dolgozhatunk a következők segítségével: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Visszaadja a hoszt jobb vagy bal részét. Így működik, ha a hoszt `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} ----------------------------------------------------------------------- -Abszolút URL-t vezet le ugyanúgy, ahogy a böngésző feldolgozza a HTML oldalon lévő linkeket: -- ha a link abszolút URL (sémát tartalmaz), változatlanul használja -- ha a link `//`-vel kezdődik, csak a sémát veszi át az aktuális URL-ből -- ha a link `/`-vel kezdődik, abszolút elérési utat hoz létre a domain gyökerétől -- egyéb esetekben az URL-t relatívan állítja össze az aktuális elérési úthoz képest - -```php -$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' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Ellenőrzi, hogy két URL megegyezik-e. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -A [api:Nette\Http\UrlScript] osztály az [#UrlImmutable] leszármazottja, és további virtuális URL komponensekkel bővíti ki, mint például a projekt gyökérkönyvtára stb. Ugyanúgy, mint a szülő osztálya, immutable (megváltoztathatatlan) objektum. - -A következő diagram azokat a komponenseket ábrázolja, amelyeket az UrlScript felismer: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` az alkalmazás alap URL címe, beleértve a domaint és az alkalmazás gyökérkönyvtárához vezető útvonalrészt -- `basePath` az alkalmazás gyökérkönyvtárához vezető útvonalrész -- `scriptPath` az aktuális szkripthez vezető útvonal -- `relativePath` a szkript neve (esetleg további útvonalszegmensek) a basePath-hoz képest relatívan -- `relativeUrl` az URL teljes része a baseUrl után, beleértve a query stringet és a fragmentet. -- `pathInfo` ma már ritkán használt URL rész a szkript neve után - -Az URL részeinek visszaadására a következő metódusok állnak rendelkezésre: - -.[language-php] -| Getter | Visszaadott érték -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Az `UrlScript` objektumokat általában nem közvetlenül hozzuk létre, hanem a [Nette\Http\Request::getUrl()|request] metódus adja vissza őket már helyesen beállított komponensekkel az aktuális HTTP kéréshez. diff --git a/http/pt/@home.texy b/http/pt/@home.texy deleted file mode 100644 index 6921df5499..0000000000 --- a/http/pt/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -O pacote `nette/http` encapsula a [requisição HTTP|request] & [resposta HTTP|response], trabalho com [sessões|sessions] e [análise e composição de URLs |urls]. - - -Instalação ----------- - -Faça o download e instale a biblioteca usando a ferramenta [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/pt/@left-menu.texy b/http/pt/@left-menu.texy deleted file mode 100644 index f20249494e..0000000000 --- a/http/pt/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Introdução |@home] -- [Requisição HTTP|request] -- [Resposta HTTP|response] -- [Sessões|Sessions] -- [Utilitários de URL |urls] -- [Configuração |configuration] diff --git a/http/pt/@meta.texy b/http/pt/@meta.texy deleted file mode 100644 index 41a853b6aa..0000000000 --- a/http/pt/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentação Nette}} diff --git a/http/pt/configuration.texy b/http/pt/configuration.texy deleted file mode 100644 index e13f7301c1..0000000000 --- a/http/pt/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -Configuração HTTP -***************** - -.[perex] -Visão geral das opções de configuração para Nette HTTP. - -Se você não usa o framework inteiro, mas apenas esta biblioteca, leia [como carregar a configuração|bootstrap:]. - - -Cabeçalhos HTTP -=============== - -```neon -http: - # cabeçalhos que são enviados com cada requisição - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # afeta o cabeçalho X-Frame-Options - frames: ... # (string|bool) padrão é 'SAMEORIGIN' -``` - -O framework, por razões de segurança, envia o cabeçalho `X-Frame-Options: SAMEORIGIN`, que diz que a página só pode ser exibida dentro de outra página (no elemento `<iframe>`) se estiver no mesmo domínio. Isso pode ser indesejável em algumas situações (por exemplo, se você estiver desenvolvendo uma aplicação para o Facebook), o comportamento pode, portanto, ser alterado definindo `frames: http://allowed-host.com` ou `frames: true`. - - -Content Security Policy ------------------------ - -É fácil construir cabeçalhos `Content-Security-Policy` (doravante CSP), sua descrição pode ser encontrada na [descrição do CSP |https://content-security-policy.com]. As diretivas CSP (como `script-src`) podem ser escritas como strings de acordo com a especificação, ou como um array de valores para melhor legibilidade. Então não é necessário colocar aspas em torno de palavras-chave, como `'self'`. Nette também gera automaticamente o valor `nonce`, então no cabeçalho haverá, por exemplo, `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # string no formato de acordo com a especificação CSP - default-src: "'self' https://example.com" - - # array de valores - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool no caso de flags - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -Nos templates, use `<script n:nonce>...</script>` e o valor nonce será preenchido automaticamente. Criar sites seguros em Nette é realmente fácil. - -Da mesma forma, é possível construir os cabeçalhos `Content-Security-Policy-Report-Only` (que podem ser usados ​​simultaneamente com CSP) e [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -Cookie HTTP ------------ - -É possível alterar os valores padrão de alguns parâmetros do método [Nette\Http\Response::setCookie() |response#setCookie] e da sessão. - -```neon -http: - # escopo do cookie pelo caminho - cookiePath: ... # (string) padrão é '/' - - # domínios que aceitam o cookie - cookieDomain: 'example.com' # (string|domain) padrão é não definido - - # enviar cookie apenas via HTTPS? - cookieSecure: ... # (bool|auto) padrão é auto - - # desativa o envio do cookie que o Nette usa como proteção contra CSRF - disableNetteCookie: ... # (bool) padrão é false -``` - -O atributo `cookieDomain` determina quais domínios podem aceitar o cookie. Se não for especificado, o cookie é aceito pelo mesmo (sub)domínio que o definiu, *mas não* por seus subdomínios. Se `cookieDomain` for especificado, os subdomínios também são incluídos. Portanto, especificar `cookieDomain` é menos restritivo do que omiti-lo. - -Por exemplo, com `cookieDomain: nette.org`, os cookies também estão disponíveis em todos os subdomínios como `doc.nette.org`. O mesmo pode ser alcançado usando o valor especial `domain`, ou seja, `cookieDomain: domain`. - -O valor padrão `auto` para o atributo `cookieSecure` significa que, se o site estiver rodando em HTTPS, os cookies serão enviados com o sinalizador `Secure` e, portanto, estarão disponíveis apenas via HTTPS. - - -Proxy HTTP ----------- - -Se o site estiver rodando atrás de um proxy HTTP, forneça seu endereço IP para que a detecção de conexão via HTTPS e também o endereço IP do cliente funcionem corretamente. Ou seja, para que as funções [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] e [isSecured() |request#isSecured] retornem os valores corretos e nos templates sejam gerados links com o protocolo `https:`. - -```neon -http: - # Endereço IP, intervalo (por exemplo, 127.0.0.1/8) ou array desses valores - proxy: 127.0.0.1 # (string|string[]) padrão é não definido -``` - - -Sessão -====== - -Configurações básicas de [sessões|sessions]: - -```neon -session: - # exibir painel de sessão na Tracy Bar? - debugger: ... # (bool) padrão é false - - # tempo de inatividade após o qual a sessão expira - expiration: 14 days # (string) padrão é '3 hours' - - # quando a sessão deve ser iniciada? - autoStart: ... # (smart|always|never) padrão é 'smart' - - # handler, serviço implementando a interface SessionHandlerInterface - handler: @handlerService -``` - -A opção `autoStart` controla quando a sessão deve ser iniciada. O valor `always` significa que a sessão será iniciada sempre com o início da aplicação. O valor `smart` significa que a sessão será iniciada no início da aplicação apenas se já existir, ou no momento em que quisermos ler ou escrever nela. E, finalmente, o valor `never` proíbe o início automático da sessão. - -Além disso, é possível definir todas as [diretivas de sessão |https://www.php.net/manual/en/session.configuration.php] do PHP (no formato camelCase) e também [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Exemplo: - -```neon -session: - # 'session.name' escrevemos como 'name' - name: MYID - - # 'session.save_path' escrevemos como 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Cookie de sessão ----------------- - -O cookie de sessão é enviado com os mesmos parâmetros que [outros cookies |#Cookie HTTP], mas você pode alterá-los para ele: - -```neon -session: - # domínios que aceitam o cookie - cookieDomain: 'example.com' # (string|domain) - - # restrição ao acessar de outro domínio - cookieSamesite: None # (Strict|Lax|None) padrão é Lax -``` - -O atributo `cookieSamesite` afeta se o cookie será enviado durante o [acesso de outro domínio |nette:glossary#SameSite cookie], o que fornece alguma proteção contra ataques [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -Serviços DI -=========== - -Estes serviços são adicionados ao contêiner de DI: - -| Nome | Tipo | Descrição -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [Requisição HTTP| request] -| `http.response` | [api:Nette\Http\Response] | [Resposta HTTP| response] -| `session.session` | [api:Nette\Http\Session] | [gerenciamento de sessão| sessions] diff --git a/http/pt/request.texy b/http/pt/request.texy deleted file mode 100644 index e9b9fe6086..0000000000 --- a/http/pt/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -Requisição HTTP -*************** - -.[perex] -Nette encapsula a requisição HTTP em objetos com uma API compreensível e, ao mesmo tempo, fornece um filtro de sanitização. - -A requisição HTTP é representada pelo objeto [api:Nette\Http\Request]. Se trabalha com Nette, este objeto é criado automaticamente pelo framework e pode recebê-lo por meio de [injeção de dependência |dependency-injection:passing-dependencies]. Nos presenters, basta chamar o método `$this->getHttpRequest()`. Se trabalha fora do Nette Framework, pode criar o objeto usando [#RequestFactory]. - -Uma grande vantagem de Nette é que, ao criar o objeto, ele limpa automaticamente todos os parâmetros de entrada GET, POST, COOKIE e também a URL de caracteres de controlo e sequências UTF-8 inválidas. Com esses dados, pode trabalhar com segurança. Os dados limpos são então usados em presenters e formulários. - -→ [Instalação e requisitos |@home#Instalação] - - -Nette\Http\Request -================== - -Este objeto é imutável. Não possui setters, tem apenas um chamado wither `withUrl()`, que não altera o objeto, mas retorna uma nova instância com o valor alterado. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Retorna um clone com uma URL diferente. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Retorna a URL da requisição como um objeto [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Aviso: os navegadores não enviam o fragmento para o servidor, então `$url->getFragment()` retornará uma string vazia. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Retorna os parâmetros GET da requisição. - -```php -$all = $httpRequest->getQuery(); // retorna um array de todos os parâmetros da URL -$id = $httpRequest->getQuery('id'); // retorna o parâmetro GET 'id' (ou null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Retorna os parâmetros POST da requisição. - -```php -$all = $httpRequest->getPost(); // retorna um array de todos os parâmetros do POST -$id = $httpRequest->getPost('id'); // retorna o parâmetro POST 'id' (ou null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Retorna o [upload |#Upload de ficheiros] como um objeto [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // algum ficheiro foi enviado? - $file->getUntrustedName(); // nome do ficheiro enviado pelo utilizador - $file->getSanitizedName(); // nome sem caracteres perigosos -} -``` - -Para aceder à estrutura aninhada, forneça um array de chaves. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Como não se pode confiar nos dados externos e, portanto, nem na forma da estrutura dos ficheiros, este método é mais seguro do que, por exemplo, `$request->getFiles()['my-form']['details']['avatar']`, que pode falhar. - - -getFiles(): array .[method] ---------------------------- -Retorna a árvore de [todos os uploads |#Upload de ficheiros] numa estrutura normalizada, cujas folhas são objetos [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Retorna um cookie ou `null` se não existir. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Retorna todos os cookies. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Retorna o método HTTP com o qual a requisição foi feita. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Testa o método HTTP com o qual a requisição foi feita. O parâmetro é insensível a maiúsculas/minúsculas. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Retorna um cabeçalho HTTP ou `null` se não existir. O parâmetro é insensível a maiúsculas/minúsculas. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Retorna todos os cabeçalhos HTTP como um array associativo. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -A conexão é criptografada (HTTPS)? Para o funcionamento correto, pode ser necessário [configurar o proxy |configuration#Proxy HTTP]. - - -isSameSite(): bool .[method] ----------------------------- -A requisição vem do mesmo (sub)domínio e é iniciada clicando num link? Nette usa o cookie `_nss` (anteriormente `nette-samesite`) para deteção. - - -isAjax(): bool .[method] ------------------------- -É uma requisição AJAX? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Retorna o endereço IP do utilizador. Para o funcionamento correto, pode ser necessário [configurar o proxy |configuration#Proxy HTTP]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Retorna a resolução DNS do endereço IP do utilizador. Para o funcionamento correto, pode ser necessário [configurar o proxy |configuration#Proxy HTTP]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Retorna as credenciais de autenticação para [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Retorna o corpo da requisição HTTP. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Deteta o idioma. Como parâmetro `$lang`, passamos um array com os idiomas que a aplicação suporta, e ela retorna aquele que o navegador do visitante preferiria ver. Não há mágica, apenas o cabeçalho `Accept-Language` é usado. Se não houver correspondência, retorna `null`. - -```php -// o navegador envia, por exemplo, Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'en']; // idiomas suportados pela aplicação -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -A classe [api:Nette\Http\RequestFactory] serve para criar uma instância de `Nette\Http\Request`, que representa a requisição HTTP atual. (Se trabalha com Nette, o objeto da requisição HTTP é criado automaticamente pelo framework.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -O método `fromGlobals()` cria o objeto da requisição com base nas variáveis globais atuais do PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` e `$_SERVER`). Ao criar o objeto, ele limpa automaticamente todos os parâmetros de entrada GET, POST, COOKIE e também a URL de caracteres de controlo e sequências UTF-8 inválidas, o que garante a segurança ao trabalhar posteriormente com esses dados. - -A RequestFactory pode ser configurada antes de chamar `fromGlobals()`: - -- com o método `$factory->setBinary()`, desativa a limpeza automática dos parâmetros de entrada de caracteres de controlo e sequências UTF-8 inválidas. -- com o método `$factory->setProxy(...)`, indica o endereço IP do [servidor proxy |configuration#Proxy HTTP], o que é necessário para a deteção correta do endereço IP do utilizador. - -A RequestFactory permite definir filtros que transformam automaticamente partes da URL da requisição. Esses filtros removem caracteres indesejados da URL, que podem ser inseridos lá, por exemplo, por implementações incorretas de sistemas de comentários em vários sites: - -```php -// remoção de espaços do caminho -$requestFactory->urlFilters['path']['%20'] = ''; - -// remoção de ponto, vírgula ou parêntese direito do final da URI -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// limpeza do caminho de barras duplicadas (filtro padrão) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -A primeira chave `'path'` ou `'url'` determina a qual parte da URL o filtro se aplica. A segunda chave é a expressão regular a ser pesquisada, e o valor é a substituição a ser usada no lugar do texto encontrado. - - -Upload de ficheiros -=================== - -O método `Nette\Http\Request::getFiles()` retorna um array de todos os uploads numa estrutura normalizada, cujas folhas são objetos [api:Nette\Http\FileUpload]. Eles encapsulam os dados enviados pelo controlo de formulário `<input type=file>`. - -A estrutura reflete a nomenclatura dos controlos em HTML. No caso mais simples, pode ser um único elemento de formulário nomeado enviado como: - -```latte -<input type="file" name="avatar"> -``` - -Neste caso, `$request->getFiles()` retorna um array: - -```php -[ - 'avatar' => /* Instância FileUpload */ -] -``` - -O objeto `FileUpload` é criado mesmo que o utilizador não tenha enviado nenhum ficheiro ou o envio tenha falhado. Se o ficheiro foi enviado é retornado pelo método `hasFile()`: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -No caso do nome do elemento usando a notação de array: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -a árvore retornada parece-se com isto: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* Instância FileUpload */ - ], - ], -] -``` - -Também é possível criar um array de ficheiros: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -Nesse caso, a estrutura parece-se com isto: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* Instância FileUpload */, - 1 => /* Instância FileUpload */, - 2 => /* Instância FileUpload */, - ], - ], - ], -] -``` - -A melhor maneira de aceder ao índice 1 do array aninhado é assim: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Como não se pode confiar nos dados externos e, portanto, nem na forma da estrutura dos ficheiros, este método é mais seguro do que, por exemplo, `$request->getFiles()['my-form']['details']['avatars'][1]`, que pode falhar. - - -Visão geral dos métodos `FileUpload` .{toc: FileUpload} -------------------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Retorna `true` se o utilizador enviou algum ficheiro. - - -isOk(): bool .[method] ----------------------- -Retorna `true` se o ficheiro foi carregado com sucesso. - - -getError(): int .[method] -------------------------- -Retorna o código de erro durante o upload do ficheiro. É uma das constantes [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. Caso o upload tenha ocorrido corretamente, retorna `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Move o ficheiro carregado para um novo local. Se o ficheiro de destino já existir, ele será sobrescrito. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Retorna o conteúdo do ficheiro carregado. Caso o upload não tenha sido bem-sucedido, retorna `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Deteta o tipo de conteúdo MIME do ficheiro carregado com base na sua assinatura. Caso o upload não tenha sido bem-sucedido ou a deteção falhe, retorna `null`. - -.[caution] -Requer a extensão PHP `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Retorna o nome original do ficheiro, como enviado pelo navegador. - -.[caution] -Não confie no valor retornado por este método. O cliente pode ter enviado um nome de ficheiro malicioso com a intenção de danificar ou hackear a sua aplicação. - - -getSanitizedName(): string .[method] ------------------------------------- -Retorna o nome do ficheiro sanitizado. Contém apenas caracteres ASCII `[a-zA-Z0-9.-]`. Se o nome não contiver tais caracteres, retorna `'unknown'`. Se o ficheiro for uma imagem no formato JPEG, PNG, GIF, WebP ou AVIF, retorna também a extensão correta. - -.[caution] -Requer a extensão PHP `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Retorna a extensão de ficheiro apropriada (sem o ponto) correspondente ao tipo MIME detetado. - -.[caution] -Requer a extensão PHP `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Retorna o caminho original do ficheiro, como enviado pelo navegador ao fazer upload de uma pasta. O caminho completo está disponível apenas no PHP 8.1 e superior. Em versões anteriores, este método retorna o nome original do ficheiro. - -.[caution] -Não confie no valor retornado por este método. O cliente pode ter enviado um nome de ficheiro malicioso com a intenção de danificar ou hackear a sua aplicação. - - -getSize(): int .[method] ------------------------- -Retorna o tamanho do ficheiro carregado. Caso o upload não tenha sido bem-sucedido, retorna `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Retorna o caminho para o local temporário do ficheiro carregado. Caso o upload não tenha sido bem-sucedido, retorna `''`. - - -isImage(): bool .[method] -------------------------- -Retorna `true` se o ficheiro carregado for uma imagem no formato JPEG, PNG, GIF, WebP ou AVIF. A deteção ocorre com base na sua assinatura e não verifica a integridade de todo o ficheiro. Se a imagem não está danificada pode ser verificado, por exemplo, tentando [carregá-la |#toImage]. - -.[caution] -Requer a extensão PHP `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Retorna o par `[largura, altura]` com as dimensões da imagem carregada. Caso o upload não tenha sido bem-sucedido ou não seja uma imagem válida, retorna `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Carrega a imagem como um objeto [Image|utils:images]. Caso o upload não tenha sido bem-sucedido ou não seja uma imagem válida, lança a exceção `Nette\Utils\ImageException`. diff --git a/http/pt/response.texy b/http/pt/response.texy deleted file mode 100644 index 2e9692d2be..0000000000 --- a/http/pt/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -Resposta HTTP -************* - -.[perex] -Nette encapsula a resposta HTTP em objetos com uma API compreensível. - -A resposta HTTP é representada pelo objeto [api:Nette\Http\Response]. Se trabalha com Nette, este objeto é criado automaticamente pelo framework e pode recebê-lo por meio de [injeção de dependência |dependency-injection:passing-dependencies]. Nos presenters, basta chamar o método `$this->getHttpResponse()`. - -→ [Instalação e requisitos |@home#Instalação] - - -Nette\Http\Response -=================== - -O objeto, ao contrário de [Nette\Http\Request|request], é mutável, ou seja, usando setters pode alterar o estado, por exemplo, enviar cabeçalhos. Lembre-se de que todos os setters devem ser chamados **antes de enviar qualquer saída.** Se a saída já foi enviada é indicado pelo método `isSent()`. Se retornar `true`, qualquer tentativa de enviar um cabeçalho lançará a exceção `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Altera o [código de status da resposta |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Para melhor clareza do código-fonte, recomendamos usar [constantes predefinidas |api:Nette\Http\IResponse] em vez de números para o código. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Retorna o código de status da resposta. - - -isSent(): bool .[method] ------------------------- -Retorna se os cabeçalhos já foram enviados do servidor para o navegador e, portanto, não é mais possível enviar cabeçalhos ou alterar o código de status. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Envia um cabeçalho HTTP e **sobrescreve** um cabeçalho enviado anteriormente com o mesmo nome. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Envia um cabeçalho HTTP e **não sobrescreve** um cabeçalho enviado anteriormente com o mesmo nome. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Exclui um cabeçalho HTTP enviado anteriormente. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Retorna o cabeçalho HTTP enviado ou `null` se não existir. O parâmetro é insensível a maiúsculas/minúsculas. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Retorna todos os cabeçalhos HTTP enviados como um array associativo. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Altera o cabeçalho `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Redireciona para outra URL. Lembre-se de encerrar o script depois. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Define a expiração do documento HTTP usando os cabeçalhos `Cache-Control` e `Expires`. O parâmetro é um intervalo de tempo (como texto) ou `null`, que desativa o cache. - -```php -// o cache no navegador expirará em uma hora -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -A resposta será baixada usando a caixa de diálogo *Salvar como* com o nome fornecido. O ficheiro em si não é enviado. - -```php -$httpResponse->sendAsFile('fatura.pdf'); -``` - - -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Envia um cookie. Os valores padrão dos parâmetros são: - -| `$path` | `'/'` | o cookie tem alcance para todos os caminhos no (sub)domínio *(configurável)* -| `$domain` | `null` | o que significa com alcance para o (sub)domínio atual, mas não seus subdomínios *(configurável)* -| `$secure` | `true` | se o site estiver rodando em HTTPS, caso contrário `false` *(configurável)* -| `$httpOnly` | `true` | o cookie é inacessível para JavaScript -| `$sameSite` | `'Lax'` | o cookie pode não ser enviado durante o [acesso de outro domínio |nette:glossary#SameSite cookie] - -Os valores padrão dos parâmetros `$path`, `$domain` e `$secure` podem ser alterados na [configuração |configuration#Cookie HTTP]. - -O tempo pode ser especificado como um número de segundos ou uma string: - -```php -$httpResponse->setCookie('lang', 'pt', '100 days'); // Traduzido 'cs' para 'pt' como exemplo -``` - -O parâmetro `$domain` determina quais domínios podem aceitar o cookie. Se não for especificado, o cookie é aceito pelo mesmo (sub)domínio que o definiu, mas não por seus subdomínios. Se `$domain` for especificado, os subdomínios também são incluídos. Portanto, especificar `$domain` é menos restritivo do que omiti-lo. Por exemplo, com `$domain = 'nette.org'`, os cookies também estão disponíveis em todos os subdomínios como `doc.nette.org`. - -Para o valor `$sameSite`, pode usar as constantes `Response::SameSiteLax`, `SameSiteStrict` e `SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Exclui um cookie. Os valores padrão dos parâmetros são: -- `$path` com alcance para todos os diretórios (`'/'`) -- `$domain` com alcance para o (sub)domínio atual, mas não seus subdomínios -- `$secure` é regido pelas configurações na [configuração |configuration#Cookie HTTP] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/pt/sessions.texy b/http/pt/sessions.texy deleted file mode 100644 index 96c4aeb392..0000000000 --- a/http/pt/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Sessões -******* - -<div class=perex> - -HTTP é um protocolo sem estado, no entanto, quase toda aplicação precisa manter o estado entre as requisições, por exemplo, o conteúdo de um carrinho de compras. É exatamente para isso que servem as sessões. Vamos mostrar: - -- como usar sessões -- como evitar conflitos de nomes -- como definir a expiração - -</div> - -Ao usar sessões, cada utilizador recebe um identificador único chamado ID de sessão, que é passado num cookie. Ele serve como chave para os dados da sessão. Ao contrário dos cookies, que são armazenados no lado do navegador, os dados da sessão são armazenados no lado do servidor. - -Configuramos a sessão na [configuração |configuration#Sessão], a escolha do tempo de expiração é especialmente importante. - -O gerenciamento da sessão é feito pelo objeto [api:Nette\Http\Session], ao qual pode aceder solicitando-o por meio de [injeção de dependência |dependency-injection:passing-dependencies]. Nos presenters, basta chamar `$session = $this->getSession()`. - -→ [Instalação e requisitos |@home#Instalação] - - -Iniciar sessão -============== - -Nette, por padrão, inicia automaticamente a sessão no momento em que começamos a ler ou escrever dados nela. Manualmente, a sessão é iniciada usando `$session->start()`. - -O PHP envia cabeçalhos HTTP que afetam o cache ao iniciar a sessão, veja [php:session_cache_limiter], e possivelmente também um cookie com o ID da sessão. Portanto, é sempre necessário iniciar a sessão antes de enviar qualquer saída para o navegador, caso contrário, uma exceção será lançada. Se sabe que a sessão será usada durante a renderização da página, inicie-a manualmente antes, por exemplo, no presenter. - -No modo de desenvolvimento, o Tracy inicia a sessão porque a usa para exibir barras com redirecionamentos e requisições AJAX na Tracy Bar. - - -Seções -====== - -Em PHP puro, o armazenamento de dados da sessão é implementado como um array acessível através da variável global `$_SESSION`. O problema é que as aplicações geralmente consistem em várias partes independentes e, se todas tiverem acesso a apenas um array, mais cedo ou mais tarde ocorrerá uma colisão de nomes. - -O Nette Framework resolve o problema dividindo todo o espaço em seções (objetos [api:Nette\Http\SessionSection]). Cada unidade então usa a sua própria seção com um nome exclusivo e nenhuma colisão pode mais ocorrer. - -Obtemos a seção da sessão: - -```php -$section = $session->getSection('nome unico'); -``` - -No presenter, basta usar `getSession()` com um parâmetro: - -```php -// $this é Presenter -$section = $this->getSession('nome unico'); -``` - -A existência da seção pode ser verificada com o método `$session->hasSection('nomeUnico')`. - -Trabalhar com a própria seção é então muito fácil usando os métodos `set()`, `get()` e `remove()`: - -```php -// escrever variável -$section->set('userName', 'franta'); - -// ler variável, retorna null se não existir -echo $section->get('userName'); - -// cancelar variável -$section->remove('userName'); -``` - -Para obter todas as variáveis da seção, é possível usar o ciclo `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Definir expiração ------------------ - -É possível definir a expiração para seções individuais ou até mesmo variáveis individuais. Podemos, assim, deixar a sessão do utilizador expirar em 20 minutos, mas ainda lembrar o conteúdo do carrinho. - -```php -// a seção expirará após 20 minutos -$section->setExpiration('20 minutes'); -``` - -Para definir a expiração de variáveis individuais, serve o terceiro parâmetro do método `set()`: - -```php -// a variável 'flash' expirará em 30 segundos -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Não se esqueça que o tempo de expiração de toda a sessão (veja [configuração da sessão |configuration#Sessão]) deve ser igual ou maior que o tempo definido para seções ou variáveis individuais. - -A remoção da expiração definida anteriormente é feita pelo método `removeExpiration()`. A remoção imediata de toda a seção é garantida pelo método `remove()`. - - -Eventos $onStart, $onBeforeWrite --------------------------------- - -O objeto `Nette\Http\Session` possui os [eventos |nette:glossary#Eventos] `$onStart` e `$onBeforeWrite`, então pode adicionar callbacks que serão chamados após o início da sessão ou antes da sua escrita no disco e subsequente encerramento. - -```php -$session->onBeforeWrite[] = function () { - // escrevemos dados na sessão - $this->section->set('basket', $this->basket); -}; -``` - - -Gerenciamento de sessão -======================= - -Visão geral dos métodos da classe `Nette\Http\Session` para gerenciamento de sessão: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Inicia a sessão. - - -isStarted(): bool .[method] ---------------------------- -A sessão está iniciada? - - -close(): void .[method] ------------------------ -Encerra a sessão. A sessão é encerrada automaticamente no final da execução do script. - - -destroy(): void .[method] -------------------------- -Encerra e exclui a sessão. - - -exists(): bool .[method] ------------------------- -A requisição HTTP contém um cookie com o ID da sessão? - - -regenerateId(): void .[method] ------------------------------- -Gera um novo ID de sessão aleatório. Os dados permanecem preservados. - - -getId(): string .[method] -------------------------- -Retorna o ID da sessão. - -</div> - - -Configuração ------------- - -Configuramos a sessão na [configuração |configuration#Sessão]. Se está a escrever uma aplicação que não usa um contêiner DI, estes métodos são usados para configuração. Devem ser chamados antes de iniciar a sessão. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Define o nome do cookie no qual o ID da sessão é transmitido. O nome padrão é `PHPSESSID`. É útil caso execute várias aplicações diferentes no mesmo site. - - -getName(): string .[method] ---------------------------- -Retorna o nome do cookie no qual o ID da sessão é transmitido. - - -setOptions(array $options): static .[method] --------------------------------------------- -Configura a sessão. É possível definir todas as [diretivas de sessão |https://www.php.net/manual/en/session.configuration.php] do PHP (no formato camelCase, por exemplo, em vez de `session.save_path` escrevemos `savePath`) e também [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Define o tempo de inatividade após o qual a sessão expira. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Define os parâmetros para o cookie. Os valores padrão dos parâmetros podem ser alterados na [configuração |configuration#Cookie de sessão]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Define o diretório onde os ficheiros de sessão são armazenados. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Define um manipulador personalizado, veja a [documentação do PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Segurança em primeiro lugar -=========================== - -O servidor assume que está a comunicar sempre com o mesmo utilizador, desde que as requisições sejam acompanhadas pelo mesmo ID de sessão. A tarefa dos mecanismos de segurança é garantir que isso realmente aconteça e que não seja possível roubar ou falsificar o identificador. - -O Nette Framework, portanto, configura corretamente as diretivas PHP para que o ID da sessão seja transmitido apenas em cookies, o torne inacessível ao JavaScript e ignore quaisquer identificadores na URL. Além disso, em momentos críticos, como o login do utilizador, ele gera um novo ID de sessão. - -.[note] -Para configurar o PHP, usa-se a função ini_set, que infelizmente alguns provedores de hospedagem proíbem. Se este for o caso do seu provedor, tente negociar com ele para permitir a função ou pelo menos configurar o servidor. diff --git a/http/pt/urls.texy b/http/pt/urls.texy deleted file mode 100644 index ba39376048..0000000000 --- a/http/pt/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Trabalhando com URLs -******************** - -.[perex] -As classes [#Url], [#UrlImmutable] e [#UrlScript] permitem gerar, analisar e manipular URLs facilmente. - -→ [Instalação e requisitos |@home#Instalação] - - -Url -=== - -A classe [api:Nette\Http\Url] permite trabalhar facilmente com URLs e os seus componentes individuais, que são capturados neste diagrama: - -/--pre - schema user password host port path query fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -A geração de URLs é intuitiva: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Também é possível analisar uma URL e manipulá-la posteriormente: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -A classe `Url` implementa a interface `JsonSerializable` e possui o método `__toString()`, então o objeto pode ser impresso ou usado em dados passados para `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Componentes da URL .[method] ----------------------------- - -Para retornar ou alterar os componentes individuais da URL, estes métodos estão disponíveis: - -.[language-php] -| Setter | Getter | Valor retornado -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL completa - -Aviso: Ao trabalhar com uma URL obtida de uma [requisição HTTP|request], lembre-se de que ela não conterá o fragmento, pois o navegador não o envia para o servidor. - -Também podemos trabalhar com parâmetros de consulta individuais usando: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Retorna a parte direita ou esquerda do host. Funciona assim se o host for `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)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Verifica se duas URLs são idênticas. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Verifica se a URL é absoluta. Uma URL é considerada absoluta se começa com um esquema (por exemplo, http, https, ftp) seguido por dois pontos. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Normaliza o caminho na URL removendo os segmentos especiais `.` e `..`. O método remove elementos de caminho redundantes da mesma forma que os navegadores web fazem. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -A classe [api:Nette\Http\UrlImmutable] é uma alternativa imutável à classe [#Url] (semelhante a como `DateTimeImmutable` do PHP é a alternativa imutável a `DateTime`). Em vez de setters, ela possui os chamados withers, que não alteram o objeto, mas retornam novas instâncias com o valor modificado: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -A classe `UrlImmutable` implementa a interface `JsonSerializable` e possui o método `__toString()`, então o objeto pode ser impresso ou usado em dados passados para `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Componentes da URL .[method] ----------------------------- - -Para retornar ou alterar os componentes individuais da URL, servem os métodos: - -.[language-php] -| Wither | Getter | Valor retornado -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL completa - -O método `withoutUserInfo()` remove `user` e `password`. - -Também podemos trabalhar com parâmetros de consulta individuais usando: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Retorna a parte direita ou esquerda do host. Funciona assim se o host for `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} ----------------------------------------------------------------------- -Deriva uma URL absoluta da mesma forma que um navegador processa links numa página HTML: -- se o link for uma URL absoluta (contém esquema), ele é usado sem alterações -- se o link começar com `//`, apenas o esquema da URL atual é adotado -- se o link começar com `/`, um caminho absoluto da raiz do domínio é criado -- em outros casos, a URL é construída relativamente ao caminho atual - -```php -$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' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Verifica se duas URLs são idênticas. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -A classe [api:Nette\Http\UrlScript] é descendente de [#UrlImmutable] e a estende com outros componentes virtuais da URL, como o diretório raiz do projeto, etc. Assim como a classe pai, é um objeto imutável. - -O diagrama a seguir mostra os componentes que UrlScript reconhece: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` é o endereço URL base da aplicação, incluindo o domínio e a parte do caminho para o diretório raiz da aplicação -- `basePath` é a parte do caminho para o diretório raiz da aplicação -- `scriptPath` é o caminho para o script atual -- `relativePath` é o nome do script (eventualmente outros segmentos do caminho) relativo a basePath -- `relativeUrl` é toda a parte da URL após baseUrl, incluindo a query string e o fragmento. -- `pathInfo` hoje em dia é uma parte da URL pouco utilizada após o nome do script - -Para retornar partes da URL, estão disponíveis os métodos: - -.[language-php] -| Getter | Valor retornado -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Objetos `UrlScript` geralmente não são criados diretamente, mas são retornados pelo método [Nette\Http\Request::getUrl()|request] com os componentes já configurados corretamente para a requisição HTTP atual. diff --git a/http/ro/@home.texy b/http/ro/@home.texy deleted file mode 100644 index 77caa66684..0000000000 --- a/http/ro/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -Pachetul `nette/http` încapsulează [cererea HTTP|request] & [răspunsul|response], lucrul cu [sesiunile|sessions] și [parsarea și compunerea URL-urilor |urls]. - - -Instalare ---------- - -Descărcați și instalați biblioteca folosind [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/ro/@left-menu.texy b/http/ro/@left-menu.texy deleted file mode 100644 index df87435c9c..0000000000 --- a/http/ro/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Introducere |@home] -- [Cerere HTTP|request] -- [Răspuns HTTP|response] -- [Sesiuni |Sessions] -- [Utilități URL |urls] -- [Configurație |configuration] diff --git a/http/ro/@meta.texy b/http/ro/@meta.texy deleted file mode 100644 index 9c744b37d6..0000000000 --- a/http/ro/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Documentație Nette}} diff --git a/http/ro/configuration.texy b/http/ro/configuration.texy deleted file mode 100644 index 74ded7164e..0000000000 --- a/http/ro/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -Configurare HTTP -**************** - -.[perex] -Prezentare generală a opțiunilor de configurare pentru Nette HTTP. - -Dacă nu utilizați întregul framework, ci doar această bibliotecă, citiți [cum se încarcă configurația|bootstrap:]. - - -Antete HTTP -=========== - -```neon -http: - # antete care sunt trimise cu fiecare cerere - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # afectează antetul X-Frame-Options - frames: ... # (string|bool) implicit este 'SAMEORIGIN' -``` - -Framework-ul, din motive de securitate, trimite antetul `X-Frame-Options: SAMEORIGIN`, care specifică faptul că pagina poate fi afișată în interiorul altei pagini (în elementul `<iframe>`) doar dacă se află pe același domeniu. Acest lucru poate fi nedorit în anumite situații (de exemplu, dacă dezvoltați o aplicație pentru Facebook), comportamentul putând fi modificat prin setarea `frames: http://allowed-host.com` sau `frames: true`. - - -Content Security Policy ------------------------ - -Se pot construi ușor antetele `Content-Security-Policy` (în continuare CSP), descrierea lor o găsiți în [descrierea CSP |https://content-security-policy.com]. Directivele CSP (cum ar fi `script-src`) pot fi scrise fie ca șiruri conform specificației, fie ca array-uri de valori pentru o mai bună lizibilitate. Atunci nu este nevoie să puneți ghilimele în jurul cuvintelor cheie, cum ar fi `'self'`. Nette generează, de asemenea, automat valoarea `nonce`, astfel încât antetul va conține, de exemplu, `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # șir în format conform specificației CSP - default-src: "'self' https://example.com" - - # array de valori - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool în cazul comutatoarelor - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -În șabloane utilizați `<script n:nonce>...</script>` și valoarea nonce se va completa automat. Crearea site-urilor web sigure în Nette este într-adevăr ușoară. - -Similar se pot construi și antetele `Content-Security-Policy-Report-Only` (care pot fi utilizate concomitent cu CSP) și [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -Cookie HTTP ------------ - -Se pot modifica valorile implicite ale unor parametri ai metodei [Nette\Http\Response::setCookie() |response#setCookie] și ale sesiunii. - -```neon -http: - # domeniul cookie-ului în funcție de cale - cookiePath: ... # (string) implicit este '/' - - # domenii care acceptă cookie-uri - cookieDomain: 'example.com' # (string|domain) implicit este nesetat - - # trimite cookie-uri doar prin HTTPS? - cookieSecure: ... # (bool|auto) implicit este auto - - # dezactivează trimiterea cookie-ului utilizat de Nette pentru protecția CSRF - disableNetteCookie: ... # (bool) implicit este false -``` - -Atributul `cookieDomain` specifică ce domenii pot accepta cookie-uri. Dacă nu este specificat, cookie-ul este acceptat de același (sub)domeniu care l-a setat, *dar nu* și de subdomeniile sale. Dacă `cookieDomain` este specificat, sunt incluse și subdomeniile. Prin urmare, specificarea `cookieDomain` este mai puțin restrictivă decât omiterea sa. - -De exemplu, cu `cookieDomain: nette.org`, cookie-urile sunt disponibile și pe toate subdomeniile precum `doc.nette.org`. Același lucru se poate realiza și cu valoarea specială `domain`, adică `cookieDomain: domain`. - -Valoarea implicită `auto` pentru atributul `cookieSecure` înseamnă că, dacă site-ul rulează pe HTTPS, cookie-urile vor fi trimise cu flag-ul `Secure` și, prin urmare, vor fi disponibile doar prin HTTPS. - - -Proxy HTTP ----------- - -Dacă site-ul rulează în spatele unui proxy HTTP, specificați adresa sa IP pentru ca detectarea conexiunii prin HTTPS și a adresei IP a clientului să funcționeze corect. Adică, pentru ca funcțiile [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] și [isSecured() |request#isSecured] să returneze valorile corecte și în șabloane să se genereze linkuri cu protocolul `https:`. - -```neon -http: - # Adresă IP, interval (ex. 127.0.0.1/8) sau array cu aceste valori - proxy: 127.0.0.1 # (string|string[]) implicit este nesetat -``` - - -Sesiune -======= - -Setări de bază pentru [sesiuni|sessions]: - -```neon -session: - # afișează panoul de sesiune în Tracy Bar? - debugger: ... # (bool) implicit este false - - # perioada de inactivitate după care sesiunea expiră - expiration: 14 days # (string) implicit este '3 hours' - - # când ar trebui să pornească sesiunea? - autoStart: ... # (smart|always|never) implicit este 'smart' - - # handler, serviciu care implementează interfața SessionHandlerInterface - handler: @handlerService -``` - -Opțiunea `autoStart` controlează când trebuie să pornească sesiunea. Valoarea `always` înseamnă că sesiunea va porni întotdeauna la pornirea aplicației. Valoarea `smart` înseamnă că sesiunea va porni la începutul aplicației doar dacă există deja, sau în momentul în care dorim să citim sau să scriem în ea. Și, în final, valoarea `never` interzice pornirea automată a sesiunii. - -În plus, se pot seta toate [directivele de sesiune |https://www.php.net/manual/en/session.configuration.php] PHP (în format camelCase) și, de asemenea, [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Exemplu: - -```neon -session: - # 'session.name' se scrie ca 'name' - name: MYID - - # 'session.save_path' se scrie ca 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Cookie de sesiune ------------------ - -Cookie-ul de sesiune este trimis cu aceiași parametri ca [alte cookie-uri |#Cookie HTTP], dar îi puteți modifica pentru acesta: - -```neon -session: - # domenii care acceptă cookie-uri - cookieDomain: 'example.com' # (string|domain) - - # restricții la accesul de pe alt domeniu - cookieSamesite: None # (Strict|Lax|None) implicit este Lax -``` - -Atributul `cookieSamesite` afectează dacă cookie-ul va fi trimis la [accesul de pe alt domeniu |nette:glossary#Cookie SameSite], ceea ce oferă o anumită protecție împotriva atacurilor [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -Servicii DI -=========== - -Aceste servicii sunt adăugate în containerul DI: - -| Nume | Tip | Descriere -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [Cerere HTTP| request] -| `http.response` | [api:Nette\Http\Response] | [Răspuns HTTP| response] -| `session.session` | [api:Nette\Http\Session] | [Gestionarea sesiunii| sessions] diff --git a/http/ro/request.texy b/http/ro/request.texy deleted file mode 100644 index 87c68f188b..0000000000 --- a/http/ro/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -Cerere HTTP -*********** - -.[perex] -Nette încapsulează cererea HTTP în obiecte cu o API inteligibilă și, în același timp, oferă un filtru de igienizare. - -Cererea HTTP este reprezentată de obiectul [api:Nette\Http\Request]. Dacă lucrați cu Nette, acest obiect este creat automat de framework și îl puteți primi prin [injecție de dependențe |dependency-injection:passing-dependencies]. În presentere, este suficient să apelați metoda `$this->getHttpRequest()`. Dacă lucrați în afara Nette Framework, puteți crea obiectul folosind [#RequestFactory]. - -Un mare avantaj al Nette este că, la crearea obiectului, curăță automat toți parametrii de intrare GET, POST, COOKIE și, de asemenea, URL-ul de caractere de control și secvențe UTF-8 invalide. Cu aceste date puteți lucra în siguranță în continuare. Datele curățate sunt apoi utilizate în presentere și formulare. - -→ [Instalare și cerințe |@home#Instalare] - - -Nette\Http\Request -================== - -Acest obiect este imuabil (nu poate fi modificat). Nu are setteri, are doar un așa-numit wither `withUrl()`, care nu modifică obiectul, ci returnează o nouă instanță cu valoarea modificată. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Returnează o clonă cu o altă adresă URL. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Returnează URL-ul cererii ca obiect [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Atenție: browserele nu trimit fragmentul către server, așa că `$url->getFragment()` va returna un șir gol. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Returnează parametrii GET ai cererii. - -```php -$all = $httpRequest->getQuery(); // returnează un array cu toți parametrii din URL -$id = $httpRequest->getQuery('id'); // returnează parametrul GET 'id' (sau null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Returnează parametrii POST ai cererii. - -```php -$all = $httpRequest->getPost(); // returnează un array cu toți parametrii din POST -$id = $httpRequest->getPost('id'); // returnează parametrul POST 'id' (sau null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Returnează [încărcarea |#Fișiere încărcate] ca obiect [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // a fost încărcat vreun fișier? - $file->getUntrustedName(); // numele fișierului trimis de utilizator - $file->getSanitizedName(); // nume fără caractere periculoase -} -``` - -Pentru a accesa structura imbricată, specificați un array de chei. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Deoarece nu se poate avea încredere în datele din exterior și, prin urmare, nici în structura fișierelor, această metodă este mai sigură decât, de exemplu, `$request->getFiles()['my-form']['details']['avatar']`, care poate eșua. - - -getFiles(): array .[method] ---------------------------- -Returnează arborele [tuturor încărcărilor |#Fișiere încărcate] într-o structură normalizată, ale cărei frunze sunt obiecte [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Returnează cookie-ul sau `null` dacă nu există. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Returnează toate cookie-urile. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Returnează metoda HTTP cu care a fost făcută cererea. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Testează metoda HTTP cu care a fost făcută cererea. Parametrul este case-insensitive. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Returnează antetul HTTP sau `null` dacă nu există. Parametrul este case-insensitive. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Returnează toate antetele HTTP ca un array asociativ. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Este conexiunea criptată (HTTPS)? Pentru o funcționare corectă, poate fi necesar să [configurați proxy-ul |configuration#Proxy HTTP]. - - -isSameSite(): bool .[method] ----------------------------- -Cererea provine de pe același (sub)domeniu și este inițiată printr-un clic pe un link? Nette utilizează cookie-ul `_nss` (anterior `nette-samesite`) pentru detectare. - - -isAjax(): bool .[method] ------------------------- -Este o cerere AJAX? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Returnează adresa IP a utilizatorului. Pentru o funcționare corectă, poate fi necesar să [configurați proxy-ul |configuration#Proxy HTTP]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Returnează rezoluția DNS a adresei IP a utilizatorului. Pentru o funcționare corectă, poate fi necesar să [configurați proxy-ul |configuration#Proxy HTTP]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Returnează datele de autentificare pentru [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Returnează corpul cererii HTTP. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Detectează limba. Ca parametru `$lang`, transmitem un array cu limbile suportate de aplicație, iar aceasta va returna limba preferată de browserul vizitatorului. Nu este magie, ci doar utilizează antetul `Accept-Language`. Dacă nu există nicio potrivire, returnează `null`. - -```php -// browserul trimite, de ex., Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'en']; // limbi suportate de aplicație -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -Clasa [api:Nette\Http\RequestFactory] servește la crearea unei instanțe `Nette\Http\Request`, care reprezintă cererea HTTP curentă. (Dacă lucrați cu Nette, obiectul cererii HTTP este creat automat de framework.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -Metoda `fromGlobals()` creează obiectul cererii pe baza variabilelor globale PHP curente (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` și `$_SERVER`). La crearea obiectului, curăță automat toți parametrii de intrare GET, POST, COOKIE și, de asemenea, URL-ul de caractere de control și secvențe UTF-8 invalide, ceea ce asigură siguranța în lucrul ulterior cu aceste date. - -RequestFactory poate fi configurat înainte de a apela `fromGlobals()`: - -- prin metoda `$factory->setBinary()` dezactivați curățarea automată a parametrilor de intrare de caractere de control și secvențe UTF-8 invalide. -- prin metoda `$factory->setProxy(...)` specificați adresa IP a [serverului proxy |configuration#Proxy HTTP], ceea ce este necesar pentru detectarea corectă a adresei IP a utilizatorului. - -RequestFactory permite definirea filtrelor care transformă automat părți ale URL-ului cererii. Aceste filtre elimină caracterele nedorite din URL, care pot fi introduse acolo, de exemplu, printr-o implementare incorectă a sistemelor de comentarii pe diverse site-uri web: - -```php -// eliminarea spațiilor din cale -$requestFactory->urlFilters['path']['%20'] = ''; - -// eliminarea punctului, virgulei sau parantezei drepte de la sfârșitul URI-ului -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// curățarea căii de slash-uri duplicate (filtru implicit) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Prima cheie `'path'` sau `'url'` specifică la ce parte a URL-ului se aplică filtrul. A doua cheie este expresia regulată care trebuie căutată, iar valoarea este înlocuirea care se utilizează în locul textului găsit. - - -Fișiere încărcate -================= - -Metoda `Nette\Http\Request::getFiles()` returnează un array cu toate încărcările într-o structură normalizată, ale cărei frunze sunt obiecte [api:Nette\Http\FileUpload]. Acestea încapsulează datele trimise de elementul de formular `<input type=file>`. - -Structura reflectă denumirea elementelor în HTML. În cel mai simplu caz, poate fi un singur element de formular numit, trimis ca: - -```latte -<input type="file" name="avatar"> -``` - -În acest caz, `$request->getFiles()` returnează un array: - -```php -[ - 'avatar' => /* Instanță FileUpload */ -] -``` - -Obiectul `FileUpload` este creat chiar și în cazul în care utilizatorul nu a trimis niciun fișier sau trimiterea a eșuat. Dacă fișierul a fost trimis, returnează metoda `hasFile()`: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -În cazul numelui elementului care utilizează notația pentru array-uri: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -arborele returnat arată astfel: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* Instanță FileUpload */ - ], - ], -] -``` - -Se poate crea și un array de fișiere: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -În acest caz, structura arată astfel: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* Instanță FileUpload */, - 1 => /* Instanță FileUpload */, - 2 => /* Instanță FileUpload */, - ], - ], - ], -] -``` - -Accesarea indexului 1 al array-ului imbricat se face cel mai bine astfel: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Deoarece nu se poate avea încredere în datele din exterior și, prin urmare, nici în structura fișierelor, această metodă este mai sigură decât, de exemplu, `$request->getFiles()['my-form']['details']['avatars'][1]`, care poate eșua. - - -Prezentare generală a metodelor `FileUpload` .{toc: FileUpload} ---------------------------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Returnează `true` dacă utilizatorul a încărcat un fișier. - - -isOk(): bool .[method] ----------------------- -Returnează `true` dacă fișierul a fost încărcat cu succes. - - -getError(): int .[method] -------------------------- -Returnează codul de eroare la încărcarea fișierului. Este una dintre constantele [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. Dacă încărcarea a avut succes, returnează `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Mută fișierul încărcat într-o nouă locație. Dacă fișierul țintă există deja, acesta va fi suprascris. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Returnează conținutul fișierului încărcat. Dacă încărcarea nu a avut succes, returnează `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Detectează tipul de conținut MIME al fișierului încărcat pe baza semnăturii sale. Dacă încărcarea nu a avut succes sau detectarea a eșuat, returnează `null`. - -.[caution] -Necesită extensia PHP `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Returnează numele original al fișierului, așa cum a fost trimis de browser. - -.[caution] -Nu aveți încredere în valoarea returnată de această metodă. Clientul ar fi putut trimite un nume de fișier dăunător cu intenția de a deteriora sau de a pirata aplicația dvs. - - -getSanitizedName(): string .[method] ------------------------------------- -Returnează numele de fișier igienizat. Conține doar caractere ASCII `[a-zA-Z0-9.-]`. Dacă numele nu conține astfel de caractere, returnează `'unknown'`. Dacă fișierul este o imagine în format JPEG, PNG, GIF, WebP sau AVIF, returnează și extensia corectă. - -.[caution] -Necesită extensia PHP `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Returnează extensia de fișier potrivită (fără punct) corespunzătoare tipului MIME detectat. - -.[caution] -Necesită extensia PHP `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Returnează calea originală a fișierului, așa cum a fost trimisă de browser la încărcarea unui folder. Calea completă este disponibilă numai în PHP 8.1 și versiunile ulterioare. În versiunile anterioare, această metodă returnează numele original al fișierului. - -.[caution] -Nu aveți încredere în valoarea returnată de această metodă. Clientul ar fi putut trimite un nume de fișier dăunător cu intenția de a deteriora sau de a pirata aplicația dvs. - - -getSize(): int .[method] ------------------------- -Returnează dimensiunea fișierului încărcat. Dacă încărcarea nu a avut succes, returnează `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Returnează calea către locația temporară a fișierului încărcat. Dacă încărcarea nu a avut succes, returnează `''`. - - -isImage(): bool .[method] -------------------------- -Returnează `true` dacă fișierul încărcat este o imagine în format JPEG, PNG, GIF, WebP sau AVIF. Detectarea se bazează pe semnătura sa și nu verifică integritatea întregului fișier. Dacă imaginea este deteriorată poate fi determinat, de exemplu, încercând să o [încărcați |#toImage]. - -.[caution] -Necesită extensia PHP `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Returnează o pereche `[lățime, înălțime]` cu dimensiunile imaginii încărcate. Dacă încărcarea nu a avut succes sau nu este o imagine validă, returnează `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Încarcă imaginea ca obiect [Image|utils:images]. Dacă încărcarea nu a avut succes sau nu este o imagine validă, aruncă o excepție `Nette\Utils\ImageException`. diff --git a/http/ro/response.texy b/http/ro/response.texy deleted file mode 100644 index e5dcb254f3..0000000000 --- a/http/ro/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -Răspuns HTTP -************ - -.[perex] -Nette încapsulează răspunsul HTTP în obiecte cu o API inteligibilă. - -Răspunsul HTTP este reprezentat de obiectul [api:Nette\Http\Response]. Dacă lucrați cu Nette, acest obiect este creat automat de framework și îl puteți primi prin [injecție de dependențe |dependency-injection:passing-dependencies]. În presentere, este suficient să apelați metoda `$this->getHttpResponse()`. - -→ [Instalare și cerințe |@home#Instalare] - - -Nette\Http\Response -=================== - -Obiectul, spre deosebire de [Nette\Http\Request|request], este mutabil, adică puteți modifica starea folosind setteri, de exemplu, trimițând antete. Nu uitați că toți setterii trebuie apelați **înainte de a trimite orice ieșire.** Dacă ieșirea a fost deja trimisă, indică metoda `isSent()`. Dacă returnează `true`, orice încercare de a trimite un antet va arunca o excepție `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Modifică [codul de stare al răspunsului |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Pentru o mai bună lizibilitate a codului sursă, se recomandă utilizarea [constantelor predefinite |api:Nette\Http\IResponse] în loc de numere pentru cod. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Returnează codul de stare al răspunsului. - - -isSent(): bool .[method] ------------------------- -Returnează dacă antetele au fost deja trimise de la server la browser și, prin urmare, nu mai este posibil să se trimită antete sau să se modifice codul de stare. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Trimite un antet HTTP și **suprascrie** antetul trimis anterior cu același nume. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Trimite un antet HTTP și **nu suprascrie** antetul trimis anterior cu același nume. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Șterge un antet HTTP trimis anterior. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Returnează antetul HTTP trimis sau `null` dacă nu există. Parametrul este case-insensitive. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Returnează toate antetele HTTP trimise ca un array asociativ. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Modifică antetul `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Redirecționează către o altă adresă URL. Nu uitați să terminați scriptul după aceea. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Setează expirarea documentului HTTP folosind antetele `Cache-Control` și `Expires`. Parametrul este fie un interval de timp (ca text), fie `null`, ceea ce dezactivează stocarea în cache. - -```php -// cache-ul din browser va expira într-o oră -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -Răspunsul va fi descărcat folosind caseta de dialog *Salvare ca* sub numele specificat. Fișierul în sine nu este trimis. - -```php -$httpResponse->sendAsFile('factura.pdf'); -``` - - -setCookie(string $name, string $value, $time, ?string $path=null, ?string $domain=null, ?bool $secure=null, ?bool $httpOnly=null, ?string $sameSite=null) .[method] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Trimite un cookie. Valorile implicite ale parametrilor: - -| `$path` | `'/'` | cookie-ul are acoperire pentru toate căile din (sub)domeniu *(configurabil)* -| `$domain` | `null` | ceea ce înseamnă cu acoperire pentru (sub)domeniul curent, dar nu și subdomeniile sale *(configurabil)* -| `$secure` | `true` | dacă site-ul rulează pe HTTPS, altfel `false` *(configurabil)* -| `$httpOnly` | `true` | cookie-ul este inaccesibil pentru JavaScript -| `$sameSite` | `'Lax'` | cookie-ul poate să nu fie trimis la [accesul de pe alt domeniu |nette:glossary#Cookie SameSite] - -Valorile implicite ale parametrilor `$path`, `$domain` și `$secure` le puteți modifica în [configurație |configuration#Cookie HTTP]. - -Timpul poate fi specificat ca număr de secunde sau șir: - -```php -$httpResponse->setCookie('lang', 'ro', '100 days'); -``` - -Parametrul `$domain` specifică ce domenii pot accepta cookie-uri. Dacă nu este specificat, cookie-ul este acceptat de același (sub)domeniu care l-a setat, dar nu și de subdomeniile sale. Dacă `$domain` este specificat, sunt incluse și subdomeniile. Prin urmare, specificarea `$domain` este mai puțin restrictivă decât omiterea sa. De exemplu, cu `$domain = 'nette.org'`, cookie-urile sunt disponibile și pe toate subdomeniile precum `doc.nette.org`. - -Pentru valoarea `$sameSite` puteți utiliza constantele `Response::SameSiteLax`, `SameSiteStrict` și `SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Șterge un cookie. Valorile implicite ale parametrilor sunt: -- `$path` cu acoperire pentru toate directoarele (`'/'`) -- `$domain` cu acoperire pentru (sub)domeniul curent, dar nu și subdomeniile sale -- `$secure` se ghidează după setările din [configurație |configuration#Cookie HTTP] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/ro/sessions.texy b/http/ro/sessions.texy deleted file mode 100644 index 02af57148e..0000000000 --- a/http/ro/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Sesiuni -******* - -<div class=perex> - -HTTP este un protocol fără stare, însă aproape orice aplicație are nevoie să păstreze starea între cereri, de exemplu conținutul coșului de cumpărături. Tocmai pentru aceasta servesc sesiunile. Vom arăta, - -- cum să utilizați sesiunile -- cum să preveniți conflictele de nume -- cum să setați expirarea - -</div> - -La utilizarea sesiunilor, fiecare utilizator primește un identificator unic numit ID de sesiune, care este transmis într-un cookie. Acesta servește drept cheie pentru datele sesiunii. Spre deosebire de cookie-uri, care sunt stocate pe partea browserului, datele din sesiune sunt stocate pe partea serverului. - -Sesiunea o setăm în [configurație |configuration#Sesiune], importantă fiind în special alegerea timpului de expirare. - -Gestionarea sesiunii este responsabilitatea obiectului [api:Nette\Http\Session], la care ajungeți solicitându-l prin [injecție de dependențe |dependency-injection:passing-dependencies]. În presentere, este suficient să apelați `$session = $this->getSession()`. - -→ [Instalare și cerințe |@home#Instalare] - - -Pornirea sesiunii -================= - -Nette, în setarea implicită, pornește automat sesiunea în momentul în care începem să citim sau să scriem date în ea. Manual, sesiunea se pornește folosind `$session->start()`. - -PHP trimite la pornirea sesiunii antete HTTP care afectează stocarea în cache, vezi [php:session_cache_limiter], și eventual și un cookie cu ID-ul sesiunii. De aceea, este necesar să porniți întotdeauna sesiunea înainte de a trimite orice ieșire către browser, altfel se va arunca o excepție. Deci, dacă știți că în timpul randării paginii se va utiliza sesiunea, porniți-o manual înainte, de exemplu în presenter. - -În modul de dezvoltare, Tracy pornește sesiunea, deoarece o utilizează pentru afișarea barelor cu redirecționări și cereri AJAX în Tracy Bar. - - -Secțiuni -======== - -În PHP pur, stocarea datelor sesiunii este realizată ca un array accesibil prin variabila globală `$_SESSION`. Problema este că aplicațiile sunt compuse în mod obișnuit dintr-o serie de părți independente reciproc și dacă toate au la dispoziție doar un singur array, mai devreme sau mai târziu va apărea o coliziune de nume. - -Nette Framework rezolvă problema împărțind întregul spațiu în secțiuni (obiecte [api:Nette\Http\SessionSection]). Fiecare unitate utilizează apoi propria secțiune cu un nume unic și nicio coliziune nu mai poate avea loc. - -Obținem secțiunea din sesiune: - -```php -$section = $session->getSection('nume-unic'); -``` - -În presenter este suficient să folosim `getSession()` cu parametru: - -```php -// $this este Presenter -$section = $this->getSession('nume-unic'); -``` - -Existența secțiunii poate fi verificată cu metoda `$session->hasSection('nume-unic')`. - -Cu secțiunea însăși se lucrează apoi foarte ușor folosind metodele `set()`, `get()` și `remove()`: - -```php -// scriere variabilă -$section->set('userName', 'franta'); - -// citire variabilă, returnează null dacă nu există -echo $section->get('userName'); - -// anulare variabilă -$section->remove('userName'); -``` - -Pentru a obține toate variabilele dintr-o secțiune, se poate utiliza bucla `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Setarea expirării ------------------ - -Pentru secțiuni individuale sau chiar variabile individuale se poate seta expirarea. Putem astfel lăsa autentificarea utilizatorului să expire după 20 de minute, dar în același timp să păstrăm conținutul coșului. - -```php -// secțiunea expiră după 20 de minute -$section->setExpiration('20 minutes'); -``` - -Pentru setarea expirării variabilelor individuale servește al treilea parametru al metodei `set()`: - -```php -// variabila 'flash' va expira după 30 de secunde -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Nu uitați că timpul de expirare al întregii sesiuni (vezi [configurarea sesiunii |configuration#Sesiune]) trebuie să fie egal sau mai mare decât timpul setat pentru secțiunile sau variabilele individuale. - -Anularea expirării setate anterior se realizează cu metoda `removeExpiration()`. Anularea imediată a întregii secțiuni este asigurată de metoda `remove()`. - - -Evenimentele $onStart, $onBeforeWrite -------------------------------------- - -Obiectul `Nette\Http\Session` are [evenimente |nette:glossary#Evenimente] `$onStart` și `$onBeforeWrite`, deci puteți adăuga callback-uri care se declanșează după pornirea sesiunii sau înainte de scrierea ei pe disc și închiderea ulterioară. - -```php -$session->onBeforeWrite[] = function () { - // scriem datele în sesiune - $this->section->set('basket', $this->basket); -}; -``` - - -Gestionarea sesiunii -==================== - -Prezentare generală a metodelor clasei `Nette\Http\Session` pentru gestionarea sesiunii: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Pornește sesiunea. - - -isStarted(): bool .[method] ---------------------------- -Sesiunea este pornită? - - -close(): void .[method] ------------------------ -Închide sesiunea. Sesiunea se închide automat la sfârșitul rulării scriptului. - - -destroy(): void .[method] -------------------------- -Închide și șterge sesiunea. - - -exists(): bool .[method] ------------------------- -Cererea HTTP conține un cookie cu ID-ul sesiunii? - - -regenerateId(): void .[method] ------------------------------- -Generează un nou ID de sesiune aleatoriu. Datele rămân păstrate. - - -getId(): string .[method] -------------------------- -Returnează ID-ul sesiunii. - -</div> - - -Configurație ------------- - -Sesiunea o setăm în [configurație |configuration#Sesiune]. Dacă scrieți o aplicație care nu utilizează containerul DI, pentru configurare servesc aceste metode. Trebuie apelate înainte de pornirea sesiunii. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Setează numele cookie-ului în care se transmite ID-ul sesiunii. Numele standard este `PHPSESSID`. Este util în cazul în care pe același site web rulați mai multe aplicații diferite. - - -getName(): string .[method] ---------------------------- -Returnează numele cookie-ului în care se transmite ID-ul sesiunii. - - -setOptions(array $options): static .[method] --------------------------------------------- -Configurează sesiunea. Se pot seta toate [directivele de sesiune |https://www.php.net/manual/en/session.configuration.php] PHP (în format camelCase, de ex. în loc de `session.save_path` scriem `savePath`) și, de asemenea, [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Setează perioada de inactivitate după care sesiunea expiră. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Setarea parametrilor pentru cookie. Valorile implicite ale parametrilor le puteți modifica în [configurație |configuration#Cookie de sesiune]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Setează directorul unde se salvează fișierele cu sesiuni. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Setarea unui handler personalizat, vezi [documentația PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Securitatea înainte de toate -============================ - -Serverul presupune că comunică în continuare cu același utilizator, atâta timp cât cererile sunt însoțite de același ID de sesiune. Sarcina mecanismelor de securitate este să asigure că acest lucru se întâmplă într-adevăr și că nu este posibilă furtul sau substituirea identificatorului. - -Nette Framework configurează, prin urmare, corect directivele PHP, astfel încât ID-ul sesiunii să fie transmis doar în cookie, să fie inaccesibil pentru JavaScript și să ignore eventualii identificatori din URL. În plus, în momente critice, cum ar fi autentificarea utilizatorului, generează un nou ID de sesiune. - -.[note] -Pentru configurarea PHP se utilizează funcția ini_set, pe care, din păcate, unele hostinguri o interzic. Dacă este și cazul hosterului dvs., încercați să discutați cu el pentru a vă permite funcția sau cel puțin pentru a configura serverul. diff --git a/http/ro/urls.texy b/http/ro/urls.texy deleted file mode 100644 index a15fa0903b..0000000000 --- a/http/ro/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Lucrul cu URL-uri -***************** - -.[perex] -Clasele [#Url], [#UrlImmutable] și [#UrlScript] permit generarea, parsarea și manipularea ușoară a URL-urilor. - -→ [Instalare și cerințe |@home#Instalare] - - -Url -=== - -Clasa [api:Nette\Http\Url] permite lucrul ușor cu URL-uri și componentele sale individuale, pe care le surprinde această schiță: - -/--pre - scheme user password host port path query fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -Generarea URL-urilor este intuitivă: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Se poate, de asemenea, parsa un URL și manipula ulterior: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -Clasa `Url` implementează interfața `JsonSerializable` și are metoda `__toString()`, astfel încât obiectul poate fi afișat sau utilizat în datele transmise către `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Componentele URL .[method] --------------------------- - -Pentru returnarea sau modificarea componentelor individuale ale URL-ului, aveți la dispoziție aceste metode: - -.[language-php] -| Setter | Getter | Valoare returnată -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz*12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL complet - -Atenție: Când lucrați cu un URL obținut dintr-o [cerere HTTP|request], rețineți că nu va conține fragmentul, deoarece browserul nu îl trimite către server. - -Putem lucra și cu parametrii query individuali folosind: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name): ?string` - - -getDomain(int $level = 2): ?string .[method] --------------------------------------------- -Returnează partea dreaptă sau stângă a gazdei. Funcționează astfel dacă gazda este `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)` | `null` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Verifică dacă două URL-uri sunt identice. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Verifică dacă URL-ul este absolut. Un URL este considerat absolut dacă începe cu o schemă (de ex., http, https, ftp) urmată de două puncte. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Normalizează calea în URL prin eliminarea segmentelor speciale `.` și `..`. Metoda elimină elementele redundante ale căii în același mod în care o fac browserele web. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -Clasa [api:Nette\Http\UrlImmutable] este o alternativă imuabilă (nu poate fi modificată) a clasei [#Url] (similar cu modul în care în PHP `DateTimeImmutable` este alternativa imuabilă a `DateTime`). În loc de setteri, are așa-numiți witheri, care nu modifică obiectul, ci returnează noi instanțe cu valoarea modificată: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -Clasa `UrlImmutable` implementează interfața `JsonSerializable` și are metoda `__toString()`, astfel încât obiectul poate fi afișat sau utilizat în datele transmise către `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Componentele URL .[method] --------------------------- - -Pentru returnarea sau modificarea componentelor individuale ale URL-ului servesc metodele: - -.[language-php] -| Wither | Getter | Valoare returnată -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz*12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | URL complet - -Metoda `withoutUserInfo()` elimină `user` și `password`. - -Putem lucra și cu parametrii query individuali folosind: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name): ?string` - - -getDomain(int $level = 2): ?string .[method] --------------------------------------------- -Returnează partea dreaptă sau stângă a gazdei. Funcționează astfel dacă gazda este `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)` | `null` - - -resolve(string $reference): UrlImmutable .[method]{data-version:3.3.2} ----------------------------------------------------------------------- -Derivă un URL absolut în același mod în care un browser procesează linkurile pe o pagină HTML: -- dacă linkul este un URL absolut (conține o schemă), este utilizat neschimbat -- dacă linkul începe cu `//`, se preia doar schema din URL-ul curent -- dacă linkul începe cu `/`, se creează o cale absolută de la rădăcina domeniului -- în celelalte cazuri, URL-ul este construit relativ la calea curentă - -```php -$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' -``` - - -isEqual(string|UrlImmutable $anotherUrl): bool .[method] --------------------------------------------------------- -Verifică dacă două URL-uri sunt identice. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -Clasa [api:Nette\Http\UrlScript] este un descendent al [#UrlImmutable] și îl extinde cu alte componente virtuale ale URL-ului, cum ar fi directorul rădăcină al proiectului etc. La fel ca clasa părinte, este un obiect imuabil (nu poate fi modificat). - -Următoarea diagramă afișează componentele pe care UrlScript le recunoaște: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` este adresa URL de bază a aplicației, inclusiv domeniul și partea căii către directorul rădăcină al aplicației -- `basePath` este partea căii către directorul rădăcină al aplicației -- `scriptPath` este calea către scriptul curent -- `relativePath` este numele scriptului (eventual alte segmente ale căii) relativ la basePath -- `relativeUrl` este întreaga parte a URL-ului după baseUrl, inclusiv query string și fragment. -- `pathInfo` este o parte a URL-ului, astăzi puțin utilizată, după numele scriptului - -Pentru returnarea părților URL-ului sunt disponibile metodele: - -.[language-php] -| Getter | Valoare returnată -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Obiectele `UrlScript` de obicei nu le creăm direct, ci le returnează metoda [Nette\Http\Request::getUrl()|request] cu componentele deja setate corect pentru cererea HTTP curentă. diff --git a/http/sl/@home.texy b/http/sl/@home.texy deleted file mode 100644 index aa5ad2508b..0000000000 --- a/http/sl/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -Paket `nette/http` zaobjema [HTTP zahtevo|request] & [odgovor|response], delo s [sejami|sessions] ter [razčlenjevanje in sestavljanje URL-jev |urls]. - - -Namestitev ----------- - -Knjižnico prenesete in namestite z orodjem [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/sl/@left-menu.texy b/http/sl/@left-menu.texy deleted file mode 100644 index e024b050e0..0000000000 --- a/http/sl/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Uvod |@home] -- [HTTP zahteva|request] -- [HTTP odgovor|response] -- [Seje |Sessions] -- [Pripomočki za URL |urls] -- [Konfiguracija |configuration] diff --git a/http/sl/@meta.texy b/http/sl/@meta.texy deleted file mode 100644 index 724324bee5..0000000000 --- a/http/sl/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Nette Dokumentacija}} diff --git a/http/sl/configuration.texy b/http/sl/configuration.texy deleted file mode 100644 index 1e57c793c9..0000000000 --- a/http/sl/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -Konfiguracija HTTP -****************** - -.[perex] -Pregled konfiguracijskih možnosti za Nette HTTP. - -Če ne uporabljate celotnega ogrodja, ampak samo to knjižnico, preberite, [kako naložiti konfiguracijo|bootstrap:]. - - -Glave HTTP -========== - -```neon -http: - # glave, ki se pošljejo z vsako zahtevo - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # vpliva na glavo X-Frame-Options - frames: ... # (string|bool) privzeto je 'SAMEORIGIN' -``` - -Ogrodje iz varnostnih razlogov pošilja glavo `X-Frame-Options: SAMEORIGIN`, ki pravi, da se stran lahko prikaže znotraj druge strani (v elementu `<iframe>`) samo, če se nahaja na isti domeni. To je lahko v nekaterih situacijah nezaželeno (na primer, če razvijate aplikacijo za Facebook), vedenje lahko zato spremenite z nastavitvijo `frames: http://allowed-host.com` ali `frames: true`. - - -Content Security Policy ------------------------ - -Enostavno je mogoče sestaviti glave `Content-Security-Policy` (v nadaljevanju CSP), njihov opis najdete v [opisu CSP |https://content-security-policy.com]. CSP direktive (kot npr. `script-src`) so lahko zapisane bodisi kot nizi po specifikaciji ali kot polja vrednosti zaradi boljše čitljivosti. Potem ni treba okoli ključnih besed, kot na primer `'self'`, pisati narekovajev. Nette tudi samodejno generira vrednost `nonce`, tako da bo v glavi na primer `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # niz v obliki po specifikaciji CSP - default-src: "'self' https://example.com" - - # polje vrednosti - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool v primeru stikal - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -V predlogah uporabljajte `<script n:nonce>...</script>` in vrednost nonce se dopolni samodejno. Delati varne spletne strani v Nette je res enostavno. - -Podobno je mogoče sestaviti tudi glave `Content-Security-Policy-Report-Only` (ki jih je mogoče uporabljati sočasno s CSP) in [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -Piškotki HTTP -------------- - -Lahko spremenite privzete vrednosti nekaterih parametrov metode [Nette\Http\Response::setCookie() |response#setCookie] in seje. - -```neon -http: - # doseg piškotka glede na pot - cookiePath: ... # (string) privzeto je '/' - - # domene, ki sprejemajo piškotek - cookieDomain: 'example.com' # (string|domain) privzeto je nenastavljeno - - # pošiljati piškotek samo preko HTTPS? - cookieSecure: ... # (bool|auto) privzeto je auto - - # izklopi pošiljanje piškotka, ki ga uporablja Nette kot zaščito pred CSRF - disableNetteCookie: ... # (bool) privzeto je false -``` - -Atribut `cookieDomain` določa, katere domene lahko sprejemajo piškotek. Če ni naveden, piškotek sprejema ista (pod)domena, kot ga je nastavila, *vendar ne* njenih poddomen. Če je `cookieDomain` določen, so vključene tudi poddomene. Zato je navedba `cookieDomain` manj omejujoča kot izpustitev. - -Na primer, pri `cookieDomain: nette.org` so piškotki dostopni tudi na vseh poddomenah kot `doc.nette.org`. Istega lahko dosežemo tudi s pomočjo posebne vrednosti `domain`, torej `cookieDomain: domain`. - -Privzeta vrednost `auto` pri atributu `cookieSecure` pomeni, da če spletno mesto teče na HTTPS, se bodo piškotki pošiljali z zastavico `Secure` in bodo torej dostopni samo preko HTTPS. - - -HTTP proxy ----------- - -Če spletno mesto teče za HTTP proxyjem, vnesite njegov IP naslov, da bo pravilno delovalo zaznavanje povezave preko HTTPS in tudi IP naslova odjemalca. Torej, da bosta funkciji [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] in [isSecured() |request#isSecured] vračali pravilne vrednosti in se bodo v predlogah generirale povezave s `https:` protokolom. - -```neon -http: - # IP naslov, obseg (npr. 127.0.0.1/8) ali polje teh vrednosti - proxy: 127.0.0.1 # (string|string[]) privzeto je nenastavljeno -``` - - -Seja -==== - -Osnovne nastavitve [sej|sessions]: - -```neon -session: - # prikazati ploščo seje v Tracy Bar? - debugger: ... # (bool) privzeto je false - - # čas neaktivnosti, po katerem seja poteče - expiration: 14 days # (string) privzeto je '3 hours' - - # kdaj naj se zažene seja? - autoStart: ... # (smart|always|never) privzeto je 'smart' - - # handler, storitev, ki implementira vmesnik SessionHandlerInterface - handler: @handlerService -``` - -Možnost `autoStart` nadzoruje, kdaj naj se zažene seja. Vrednost `always` pomeni, da se seja zažene vedno ob zagonu aplikacije. Vrednost `smart` pomeni, da se seja zažene ob zagonu aplikacije samo takrat, ko že obstaja, ali v trenutku, ko želimo iz nje brati ali vanjo pisati. In končno vrednost `never` prepoveduje samodejni zagon seje. - -Nadalje je mogoče nastavljati vse PHP [direktive seje |https://www.php.net/manual/en/session.configuration.php] (v formatu camelCase) in tudi [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Primer: - -```neon -session: - # 'session.name' zapišemo kot 'name' - name: MYID - - # 'session.save_path' zapišemo kot 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Piškotek seje -------------- - -Piškotek seje se pošilja z enakimi parametri kot [drugi piškotki |#Piškotki HTTP], vendar te lahko zanj spremenite: - -```neon -session: - # domene, ki sprejemajo piškotek - cookieDomain: 'example.com' # (string|domain) - - # omejitve pri dostopu iz druge domene - cookieSamesite: None # (Strict|Lax|None) privzeto je Lax -``` - -Atribut `cookieSamesite` vpliva na to, ali bo piškotek poslan pri [dostopu iz druge domene |nette:glossary#SameSite cookie], kar zagotavlja določeno zaščito pred napadi [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -Storitve DI -=========== - -Te storitve se dodajo v DI vsebnik: - -| Ime | Tip | Opis -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [zahteva HTTP| request] -| `http.response` | [api:Nette\Http\Response] | [odgovor HTTP| response] -| `session.session` | [api:Nette\Http\Session] | [upravljanje sej| sessions] diff --git a/http/sl/request.texy b/http/sl/request.texy deleted file mode 100644 index 8c991d201f..0000000000 --- a/http/sl/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -Zahteva HTTP -************ - -.[perex] -Nette inkapsulira HTTP zahtevo v objekte z razumljivim API-jem in hkrati zagotavlja sanacijski filter. - -HTTP zahtevo predstavlja objekt [api:Nette\Http\Request]. Če delate z Nette, ta objekt samodejno ustvari ogrodje in si ga lahko pustite predati s pomočjo [dependency injection |dependency-injection:passing-dependencies]. V presenterjih je dovolj le poklicati metodo `$this->getHttpRequest()`. Če delate izven Nette Frameworka, si lahko ustvarite objekt s pomočjo [#RequestFactory]. - -Velika prednost Nette je, da pri ustvarjanju objekta samodejno očisti vse vhodne parametre GET, POST, COOKIE in tudi URL kontrolnih znakov in neveljavnih UTF-8 sekvenc. S temi podatki lahko nato varno nadalje delate. Očiščeni podatki se nato uporabljajo v presenterjih in obrazcih. - -→ [Namestitev in zahteve |@home#Namestitev] - - -Nette\Http\Request -================== - -Ta objekt je nespremenljiv (immutable). Nima nobenih setterjev, ima le en t.i. wither `withUrl()`, ki objekta ne spreminja, ampak vrača novo instanco s spremenjeno vrednostjo. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Vrača klon z drugim URL-jem. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Vrača URL zahteve kot objekt [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/cs/?action=edit -echo $url->getHost(); // nette.org -``` - -Opozorilo: brskalniki ne pošiljajo fragmenta na strežnik, zato bo `$url->getFragment()` vračal prazen niz. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Vrača parametre GET zahteve. - -```php -$all = $httpRequest->getQuery(); // vrača polje vseh parametrov iz URL-ja -$id = $httpRequest->getQuery('id'); // vrača GET parameter 'id' (ali null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Vrača parametre POST zahteve. - -```php -$all = $httpRequest->getPost(); // vrača polje vseh parametrov iz POST-a -$id = $httpRequest->getPost('id'); // vrača POST parameter 'id' (ali null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Vrača [naloženo datoteko |#Naložene datoteke] kot objekt [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // je bila kakšna datoteka naložena? - $file->getUntrustedName(); // ime datoteke, ki ga je poslal uporabnik - $file->getSanitizedName(); // ime brez nevarnih znakov -} -``` - -Za dostop do ugnezdene strukture navedite polje ključev. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Ker ni mogoče zaupati podatkom od zunaj in se torej tudi ne zanašati na obliko strukture datotek, je ta način varnejši kot na primer `$request->getFiles()['my-form']['details']['avatar']`, ki lahko odpove. - - -getFiles(): array .[method] ---------------------------- -Vrne drevo [vseh naloženih datotek |#Naložene datoteke] v normalizirani strukturi, katere listi so objekti [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Vrača piškotek ali `null`, če ne obstaja. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Vrača vse piškotke. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Vrača HTTP metodo, s katero je bila narejena zahteva. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Testira HTTP metodo, s katero je bila narejena zahteva. Parameter je neobčutljiv na velikost črk. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Vrača HTTP glavo ali `null`, če ne obstaja. Parameter je neobčutljiv na velikost črk. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Vrača vse HTTP glave kot asociativno polje. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Je povezava šifrirana (HTTPS)? Za pravilno delovanje je morda treba [nastaviti proxy |configuration#HTTP proxy]. - - -isSameSite(): bool .[method] ----------------------------- -Ali zahteva prihaja iz iste (pod)domene in je sprožena s klikom na povezavo? Nette za zaznavanje uporablja piškotek `_nss` (prej `nette-samesite`). - - -isAjax(): bool .[method] ------------------------- -Gre za AJAX zahtevo? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Vrača IP naslov uporabnika. Za pravilno delovanje je morda treba [nastaviti proxy |configuration#HTTP proxy]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Vrača DNS prevod IP naslova uporabnika. Za pravilno delovanje je morda treba [nastaviti proxy |configuration#HTTP proxy]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Vrača podatke za preverjanje pristnosti za [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Vrača telo HTTP zahteve. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Zazna jezik. Kot parameter `$lang` predamo polje z jeziki, ki jih aplikacija podpira, in ona vrne tistega, ki bi ga brskalnik obiskovalca najraje videl. To niso nobene čarovnije, le uporablja se glava `Accept-Language`. Če ne pride do nobenega ujemanja, vrača `null`. - -```php -// brskalnik pošilja npr. Accept-Language: cs,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'en']; // jeziki, ki jih podpira aplikacija -echo $httpRequest->detectLanguage($langs); // en -``` - - -RequestFactory -============== - -Razred [api:Nette\Http\RequestFactory] služi za ustvarjanje instance `Nette\Http\Request`, ki predstavlja trenutno HTTP zahtevo. (Če delate z Nette, objekt HTTP zahteve samodejno ustvari ogrodje.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -Metoda `fromGlobals()` ustvari objekt zahteve na podlagi trenutnih globalnih spremenljivk PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` in `$_SERVER`). Pri ustvarjanju objekta samodejno očisti vse vhodne parametre GET, POST, COOKIE in tudi URL kontrolnih znakov in neveljavnih UTF-8 sekvenc, kar zagotavlja varnost pri nadaljnjem delu s temi podatki. - -RequestFactory lahko pred klicem `fromGlobals()` konfigurirate: - -- z metodo `$factory->setBinary()` izklopite samodejno čiščenje vhodnih parametrov kontrolnih znakov in neveljavnih UTF-8 sekvenc. -- z metodo `$factory->setProxy(...)` navedete IP naslov [proxy strežniku |configuration#HTTP proxy], kar je nujno za pravilno zaznavanje IP naslova uporabnika. - -RequestFactory omogoča definiranje filtrov, ki samodejno transformirajo dele URL zahteve. Ti filtri odstranjujejo nezaželene znake iz URL-ja, ki so tja lahko vstavljeni na primer z nepravilno implementacijo sistemov za komentarje na različnih spletnih mestih: - -```php -// odstranitev presledkov iz poti -$requestFactory->urlFilters['path']['%20'] = ''; - -// odstranitev pike, vejice ali desnega oklepaja s konca URI -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// čiščenje poti od podvojenih poševnic (privzeti filter) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Prvi ključ `'path'` ali `'url'` določa, na kateri del URL-ja se filter uporabi. Drugi ključ je regularni izraz, ki se najde, in vrednost je nadomestilo, ki se uporabi namesto najdenega besedila. - - -Naložene datoteke -================= - -Metoda `Nette\Http\Request::getFiles()` vrača polje vseh naloženih datotek v normalizirani strukturi, katere listi so objekti [api:Nette\Http\FileUpload]. Ti inkapsulirajo podatke, poslane z elementom obrazca `<input type=file>`. - -Struktura odraža poimenovanje elementov v HTML. V najpreprostejšem primeru je to lahko en sam poimenovan element obrazca, poslan kot: - -```latte -<input type="file" name="avatar"> -``` - -V tem primeru `$request->getFiles()` vrača polje: - -```php -[ - 'avatar' => /* FileUpload instance */ -] -``` - -Objekt `FileUpload` se ustvari tudi v primeru, da uporabnik ni poslal nobene datoteke ali je pošiljanje spodletelo. Ali je bila datoteka poslana, vrača metoda `hasFile()`: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -V primeru imena elementa, ki uporablja notacijo za polja: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -izgleda vrnjeno drevo takole: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* FileUpload instance */ - ], - ], -] -``` - -Lahko ustvarite tudi polje datotek: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -V takem primeru izgleda struktura takole: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, - ], - ], - ], -] -``` - -Dostop do indeksa 1 ugnezdenega polja je najbolje izvesti tako: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Ker ni mogoče zaupati podatkom od zunaj in se torej tudi ne zanašati na obliko strukture datotek, je ta način varnejši kot na primer `$request->getFiles()['my-form']['details']['avatars'][1]`, ki lahko odpove. - - -Pregled metod `FileUpload` .{toc: FileUpload} ---------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Vrača `true`, če je uporabnik naložil kakšno datoteko. - - -isOk(): bool .[method] ----------------------- -Vrača `true`, če je bila datoteka uspešno naložena. - - -getError(): int .[method] -------------------------- -Vrača kodo napake pri nalaganju datoteke. Gre za eno od konstant [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. V primeru, da je nalaganje potekalo v redu, vrača `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Premakne naloženo datoteko na novo lokacijo. Če ciljna datoteka že obstaja, bo prepisana. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Vrača vsebino naložene datoteke. V primeru, da nalaganje ni bilo uspešno, vrača `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Zazna MIME content type naložene datoteke na podlagi njene signature. V primeru, da nalaganje ni bilo uspešno ali zaznavanje ni uspelo, vrača `null`. - -.[caution] -Zahteva PHP razširitev `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Vrača originalno ime datoteke, kot ga je poslal brskalnik. - -.[caution] -Ne zaupajte vrednosti, ki jo vrne ta metoda. Odjemalec je lahko poslal škodljivo ime datoteke z namenom poškodovati ali vdreti v vašo aplikacijo. - - -getSanitizedName(): string .[method] ------------------------------------- -Vrača sanirano ime datoteke. Vsebuje samo ASCII znake `[a-zA-Z0-9.-]`. Če ime takih znakov ne vsebuje, vrne `'unknown'`. Če je datoteka slika v formatu JPEG, PNG, GIF, WebP ali AVIF, vrne tudi pravilno končnico. - -.[caution] -Zahteva PHP razširitev `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Vrača primerno končnico datoteke (brez pike), ki ustreza zaznanemu MIME tipu. - -.[caution] -Zahteva PHP razširitev `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Vrača originalno pot do datoteke, kot jo je poslal brskalnik pri nalaganju mape. Celotna pot je na voljo samo v PHP 8.1 in višjih. V prejšnjih različicah ta metoda vrača originalno ime datoteke. - -.[caution] -Ne zaupajte vrednosti, ki jo vrne ta metoda. Odjemalec je lahko poslal škodljivo ime datoteke z namenom poškodovati ali vdreti v vašo aplikacijo. - - -getSize(): int .[method] ------------------------- -Vrača velikost naložene datoteke. V primeru, da nalaganje ni bilo uspešno, vrača `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Vrača pot do začasne lokacije naložene datoteke. V primeru, da nalaganje ni bilo uspešno, vrača `''`. - - -isImage(): bool .[method] -------------------------- -Vrača `true`, če je naložena datoteka slika v formatu JPEG, PNG, GIF, WebP ali AVIF. Zaznavanje poteka na podlagi njene signature in se ne preverja integriteta celotne datoteke. Ali slika ni poškodovana, lahko ugotovite na primer s poskusom njenega [nalaganjem |#toImage]. - -.[caution] -Zahteva PHP razširitev `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Vrača par `[širina, višina]` z dimenzijami naložene slike. V primeru, da nalaganje ni bilo uspešno ali ne gre za veljavno sliko, vrača `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Naloži sliko kot objekt [Image|utils:images]. V primeru, da nalaganje ni bilo uspešno ali ne gre za veljavno sliko, vrže izjemo `Nette\Utils\ImageException`. diff --git a/http/sl/response.texy b/http/sl/response.texy deleted file mode 100644 index e93fd0e234..0000000000 --- a/http/sl/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -Odgovor HTTP -************ - -.[perex] -Nette inkapsulira HTTP odgovor v objekte z razumljivim API-jem. - -HTTP odgovor predstavlja objekt [api:Nette\Http\Response]. Če delate z Nette, ta objekt samodejno ustvari ogrodje in si ga lahko pustite predati s pomočjo [dependency injection |dependency-injection:passing-dependencies]. V presenterjih je dovolj le poklicati metodo `$this->getHttpResponse()`. - -→ [Namestitev in zahteve |@home#Namestitev] - - -Nette\Http\Response -=================== - -Objekt je za razliko od [Nette\Http\Request|request] spremenljiv (mutable), torej s pomočjo nastavitvenih metod lahko spreminjate stanje, torej npr. pošiljate glave. Ne pozabite, da morajo biti vse nastavitvene metode poklicane **pred pošiljanjem kakršnega koli izpisa.** Ali je bil izpis že poslan, pove metoda `isSent()`. Če vrača `true`, vsak poskus pošiljanja glave sproži izjemo `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Spremeni [statusno kodo odgovora |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Zaradi boljše razumljivosti izvorne kode priporočamo, da za kodo namesto številk uporabljate [preddefinirane konstante |api:Nette\Http\IResponse]. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Vrača statusno kodo odgovora. - - -isSent(): bool .[method] ------------------------- -Vrača, ali so bile glave že poslane s strežnika v brskalnik, in torej ni več mogoče pošiljati glav ali spreminjati statusne kode. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Pošlje HTTP glavo in **prepiše** prej poslano glavo istega imena. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Pošlje HTTP glavo in **ne prepiše** prej poslane glave istega imena. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Izbriše prej poslano HTTP glavo. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Vrača poslano HTTP glavo ali `null`, če takšna ne obstaja. Parameter je neobčutljiv na velikost črk. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Vrača vse poslane HTTP glave kot asociativno polje. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Spremeni glavo `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Preusmeri na drug URL. Ne pozabite nato končati skripta. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Nastavi potek HTTP dokumenta s pomočjo glav `Cache-Control` in `Expires`. Parameter je bodisi časovni interval (kot besedilo) ali `null`, kar onemogoči predpomnjenje. - -```php -// predpomnilnik v brskalniku poteče čez eno uro -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -Odgovor bo prenesen s pomočjo pogovornega okna *Shrani kot* pod navedenim imenom. Same datoteke pri tem ne pošilja. - -```php -$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] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Pošlje piškotek. Privzete vrednosti parametrov: - -| `$path` | `'/'` | piškotek ima doseg na vse poti v (pod)domeni *(nastavljivo)* -| `$domain` | `null` | kar pomeni z dosegom na trenutno (pod)domeno, vendar ne njenih poddomen *(nastavljivo)* -| `$secure` | `true` | če spletno mesto teče na HTTPS, sicer `false` *(nastavljivo)* -| `$httpOnly` | `true` | piškotek je za JavaScript nedostopen -| `$sameSite` | `'Lax'` | piškotek ni nujno poslan pri [dostopu iz druge domene |nette:glossary#SameSite cookie] - -Privzete vrednosti parametrov `$path`, `$domain` in `$secure` lahko spremenite v [konfiguraciji |configuration#Piškotki HTTP]. - -Čas lahko navajate kot število sekund ali niz: - -```php -$httpResponse->setCookie('lang', 'cs', '100 days'); -``` - -Parameter `$domain` določa, katere domene lahko sprejemajo piškotek. Če ni naveden, piškotek sprejema ista (pod)domena, kot ga je nastavila, vendar ne njenih poddomen. Če je `$domain` določen, so vključene tudi poddomene. Zato je navedba `$domain` manj omejujoča kot izpustitev. Na primer, pri `$domain = 'nette.org'` so piškotki dostopni tudi na vseh poddomenah kot `doc.nette.org`. - -Za vrednost `$sameSite` lahko uporabite konstante `Response::SameSiteLax`, `SameSiteStrict` in `SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Izbriše piškotek. Privzete vrednosti parametrov so: -- `$path` z dosegom na vse imenike (`'/'`) -- `$domain` z dosegom na trenutno (pod)domeno, vendar ne njenih poddomen -- `$secure` se ravna po nastavitvah v [konfiguraciji |configuration#Piškotki HTTP] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/sl/sessions.texy b/http/sl/sessions.texy deleted file mode 100644 index dd1bec2462..0000000000 --- a/http/sl/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Seje -**** - -<div class=perex> - -HTTP je brezstanje protokol, vendar skoraj vsaka aplikacija potrebuje ohranjati stanje med zahtevami, na primer vsebino nakupovalne košarice. Prav temu služijo seje ali relacije. Pokazali si bomo, - -- kako uporabljati seje -- kako preprečiti konflikte imen -- kako nastaviti potek - -</div> - -Pri uporabi sej vsak uporabnik prejme edinstven identifikator, imenovan ID seje, ki se prenaša v piškotku. Ta služi kot ključ do podatkov seje. Za razliko od piškotkov, ki se shranjujejo na strani brskalnika, se podatki v seji shranjujejo na strani strežnika. - -Sejo nastavljamo v [konfiguraciji |configuration#Seja], pomembna je zlasti izbira časa poteka. - -Upravljanje sej ima na skrbi objekt [api:Nette\Http\Session], do katerega pridete tako, da si ga pustite predati s pomočjo [dependency injection |dependency-injection:passing-dependencies]. V presenterjih je dovolj le poklicati `$session = $this->getSession()`. - -→ [Namestitev in zahteve |@home#Namestitev] - - -Zagon seje -========== - -Nette v privzeti nastavitvi samodejno zažene sejo samodejno v trenutku, ko iz nje začnemo brati ali vanjo zapisovati podatke. Ročno se seja zažene s pomočjo `$session->start()`. - -PHP ob zagonu seje pošlje HTTP glave, ki vplivajo na predpomnjenje, glej [php:session_cache_limiter], in po potrebi tudi piškotek z ID-jem seje. Zato je treba vedno sejo zagnati še pred pošiljanjem kakršnega koli izpisa v brskalnik, sicer pride do sprožitve izjeme. Če torej veste, da se bo med izrisovanjem strani uporabljala seja, jo zaženite ročno prej, na primer v presenterju. - -V razvijalskem načinu sejo zažene Tracy, ker jo uporablja za prikazovanje trakov s preusmeritvami in AJAX zahtevami v Tracy Baru. - - -Sekcije -======= - -V čistem PHP je podatkovno skladišče seje realizirano kot polje, dostopno preko globalne spremenljivke `$_SESSION`. Problem je v tem, da se aplikacije običajno sestojijo iz cele vrste medsebojno neodvisnih delov in če imajo vsi na voljo le eno polje, prej ali slej pride do kolizije imen. - -Nette Framework problem rešuje tako, da celoten prostor razdeli na sekcije (objekte [api:Nette\Http\SessionSection]). Vsaka enota nato uporablja svojo sekcijo z edinstvenim imenom in do nobene kolizije več ne more priti. - -Sekcijo dobimo iz seje: - -```php -$section = $session->getSection('unikatno ime'); -``` - -V presenterju je dovolj uporabiti `getSession()` s parametrom: - -```php -// $this je Presenter -$section = $this->getSession('unikatno ime'); -``` - -Preveriti obstoj sekcije je mogoče z metodo `$session->hasSection('unikatno ime')`. - -S samo sekcijo se nato dela zelo enostavno s pomočjo metod `set()`, `get()` in `remove()`: - -```php -// zapis spremenljivke -$section->set('userName', 'franta'); - -// branje spremenljivke, vrne null če ne obstaja -echo $section->get('userName'); - -// preklic spremenljivke -$section->remove('userName'); -``` - -Za pridobitev vseh spremenljivk iz sekcije je mogoče uporabiti zanko `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Nastavitev poteka ------------------ - -Za posamezne sekcije ali celo posamezne spremenljivke je mogoče nastaviti potek. Lahko tako pustimo poteči prijavo uporabnika čez 20 minut, vendar si pri tem še naprej zapomnimo vsebino košarice. - -```php -// sekcija poteče po 20 minutah -$section->setExpiration('20 minutes'); -``` - -Za nastavitev poteka posameznih spremenljivk služi tretji parameter metode `set()`: - -```php -// spremenljivka 'flash' poteče že po 30 sekundah -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Ne pozabite, da mora biti čas poteka celotne seje (glej [konfiguracija seje |configuration#Seja]) enak ali daljši od časa, nastavljenega pri posameznih sekcijah ali spremenljivkah. - -Preklic prej nastavljenega poteka dosežemo z metodo `removeExpiration()`. Takojšen preklic celotne sekcije zagotovi metoda `remove()`. - - -Dogodka $onStart, $onBeforeWrite --------------------------------- - -Objekt `Nette\Http\Session` ima [dogodke |nette:glossary#Dogodki eventi] `$onStart` in `$onBeforeWrite`, lahko torej dodate povratne klice, ki se sprožijo po zagonu seje ali pred njenim zapisom na disk in posledičnim zaključkom. - -```php -$session->onBeforeWrite[] = function () { - // zapišemo podatke v sejo - $this->section->set('basket', $this->basket); -}; -``` - - -Upravljanje sej -=============== - -Pregled metod razreda `Nette\Http\Session` za upravljanje sej: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Zažene sejo. - - -isStarted(): bool .[method] ---------------------------- -Je seja zagnana? - - -close(): void .[method] ------------------------ -Zaključi sejo. Seja se samodejno zaključi na koncu izvajanja skripta. - - -destroy(): void .[method] -------------------------- -Zaključi in izbriše sejo. - - -exists(): bool .[method] ------------------------- -Ali HTTP zahteva vsebuje piškotek z ID-jem seje? - - -regenerateId(): void .[method] ------------------------------- -Generira nov naključni ID seje. Podatki ostanejo ohranjeni. - - -getId(): string .[method] -------------------------- -Vrne ID seje. - -</div> - - -Konfiguracija -------------- - -Sejo nastavljamo v [konfiguraciji |configuration#Seja]. Če pišete aplikacijo, ki ne uporablja DI vsebnika, služijo za konfiguracijo te metode. Morajo biti poklicane še pred zagonom seje. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Nastavi ime piškotka, v katerem se prenaša ID seje. Standardno ime je `PHPSESSID`. Koristno je v primeru, ko v okviru enega spletnega mesta poganjate več različnih aplikacij. - - -getName(): string .[method] ---------------------------- -Vrača ime piškotka, v katerem se prenaša ID seje. - - -setOptions(array $options): static .[method] --------------------------------------------- -Konfigurira sejo. Lahko nastavljate vse PHP [direktive seje |https://www.php.net/manual/en/session.configuration.php] (v formatu camelCase, npr. namesto `session.save_path` zapišemo `savePath`) in tudi [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Nastavi čas neaktivnosti, po katerem seja poteče. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Nastavitev parametrov za piškotek. Privzete vrednosti parametrov lahko spremenite v [konfiguraciji |configuration#Piškotek seje]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Nastavi imenik, kamor se shranjujejo datoteke s sejo. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Nastavitev lastnega obravnavalnika, glej [dokumentacija PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Varnost na prvem mestu -====================== - -Strežnik predpostavlja, da komunicira vedno z istim uporabnikom, dokler zahteve spremlja isti ID seje. Naloga varnostnih mehanizmov je zagotoviti, da je temu res tako in da ni mogoče identifikatorja ukrasti ali podtakniti. - -Nette Framework zato pravilno konfigurira PHP direktive, da ID seje prenaša samo v piškotku, ga onemogoči JavaScriptu in morebitne identifikatorje v URL-ju ignorira. Poleg tega v kritičnih trenutkih, kot je na primer prijava uporabnika, generira nov ID seje. - -.[note] -Za konfiguracijo PHP se uporablja funkcija ini_set, ki jo na žalost nekateri gostitelji prepovedujejo. Če je to primer tudi vašega gostitelja, se poskusite z njim dogovoriti, da vam funkcijo dovoli ali vsaj strežnik konfigurira. diff --git a/http/sl/urls.texy b/http/sl/urls.texy deleted file mode 100644 index 64f5164a46..0000000000 --- a/http/sl/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Delo z URL-ji -************* - -.[perex] -Razreda [#Url], [#UrlImmutable] in [#UrlScript] omogočata enostavno generiranje, razčlenjevanje in manipulacijo z URL-ji. - -→ [Namestitev in zahteve |@home#Namestitev] - - -Url -=== - -Razred [api:Nette\Http\Url] omogoča enostavno delo z URL-ji in njihovimi posameznimi komponentami, ki jih zajema ta skica: - -/--pre - shema uporabnik geslo gostitelj vrata pot poizvedba fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl avtoriteta -\-- - -Generiranje URL-jev je intuitivno: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Lahko tudi URL razčlenite in ga nadalje manipulirate: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -Razred `Url` implementira vmesnik `JsonSerializable` in ima metodo `__toString()`, tako da lahko objekt izpišete ali uporabite v podatkih, predanih v `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Komponente URL .[method] ------------------------- - -Za vračanje ali spreminjanje posameznih komponent URL-ja so vam na voljo te metode: - -.[language-php] -| Setter | Getter | Vrnjena vrednost -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | celoten URL - -Opozorilo: Ko delate z URL-jem, ki je pridobljen iz [zahteve HTTP|request], imejte v mislih, da ne bo vseboval fragmenta, ker ga brskalnik ne pošilja na strežnik. - -Lahko delamo tudi s posameznimi query parametri s pomočjo: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Vrača desni ali levi del gostitelja. Tako deluje, če je gostitelj `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)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Preveri, ali sta dva URL-ja enaka. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Preverja, ali je URL absoluten. URL se šteje za absoluten, če se začne s shemo (npr. http, https, ftp), ki ji sledi dvopičje. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Normalizira pot v URL-ju z odstranitvijo posebnih segmentov `.` in `..`. Metoda odstranjuje odvečne elemente poti na enak način, kot to počnejo spletni brskalniki. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -Razred [api:Nette\Http\UrlImmutable] je nespremenljiva (immutable) alternativa razredu [#Url] (podobno kot je v PHP `DateTimeImmutable` nespremenljiva alternativa `DateTime`). Namesto nastavitvenih metod ima t.i. wither metode, ki objekta ne spreminjajo, ampak vračajo nove instance s prilagojeno vrednostjo: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/cs/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/cs/?name=param#footer' -``` - -Razred `UrlImmutable` implementira vmesnik `JsonSerializable` in ima metodo `__toString()`, tako da lahko objekt izpišete ali uporabite v podatkih, predanih v `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Komponente URL .[method] ------------------------- - -Za vračanje ali spreminjanje posameznih komponent URL-ja služijo metode: - -.[language-php] -| Wither | Getter | Vrnjena vrednost -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | celoten URL - -Metoda `withoutUserInfo()` odstranjuje `user` in `password`. - -Lahko delamo tudi s posameznimi query parametri s pomočjo: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Vrača desni ali levi del gostitelja. Tako deluje, če je gostitelj `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} ----------------------------------------------------------------------- -Izpelje absolutni URL na enak način, kot brskalnik obdeluje povezave na HTML strani: -- če je povezava absolutni URL (vsebuje shemo), se uporabi nespremenjena -- če se povezava začne z `//`, se prevzame samo shema iz trenutnega URL-ja -- če se povezava začne z `/`, se ustvari absolutna pot od korena domene -- v ostalih primerih se URL sestavi relativno glede na trenutno pot - -```php -$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' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Preveri, ali sta dva URL-ja enaka. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -Razred [api:Nette\Http\UrlScript] je potomec [#UrlImmutable] in ga razširja z dodatnimi virtualnimi komponentami URL-ja, kot je korenski imenik projekta ipd. Tako kot starševski razred je nespremenljiv (immutable) objekt. - -Naslednji diagram prikazuje komponente, ki jih UrlScript prepoznava: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` je osnovni URL naslov aplikacije, vključno z domeno in delom poti do korenskega imenika aplikacije -- `basePath` je del poti do korenskega imenika aplikacije -- `scriptPath` je pot do trenutnega skripta -- `relativePath` je ime skripta (po potrebi dodatni segmenti poti) relativno glede na basePath -- `relativeUrl` je celoten del URL-ja za baseUrl, vključno s query stringom in fragmentom. -- `pathInfo` danes že malo uporabljen del URL-ja za imenom skripta - -Za vračanje delov URL-ja so na voljo metode: - -.[language-php] -| Getter | Vrnjena vrednost -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Objektov `UrlScript` običajno ne ustvarjamo neposredno, ampak jih vrača metoda [Nette\Http\Request::getUrl()|request] z že pravilno nastavljenimi komponentami za trenutno HTTP zahtevo. diff --git a/http/uk/@home.texy b/http/uk/@home.texy deleted file mode 100644 index dfc6127791..0000000000 --- a/http/uk/@home.texy +++ /dev/null @@ -1,15 +0,0 @@ -Nette HTTP -********** - -.[perex] -Пакет `nette/http` інкапсулює [HTTP request|request] та [response |response], роботу з [sessions |sessions] та [парсинг і складання URL |urls]. - - -Встановлення ------------- - -Бібліотеку можна завантажити та встановити за допомогою інструменту [Composer|best-practices:composer]: - -```shell -composer require nette/http -``` diff --git a/http/uk/@left-menu.texy b/http/uk/@left-menu.texy deleted file mode 100644 index fb27b98ab9..0000000000 --- a/http/uk/@left-menu.texy +++ /dev/null @@ -1,8 +0,0 @@ -Nette HTTP -********** -- [Вступ |@home] -- [HTTP запит|request] -- [HTTP відповідь|response] -- [Сесії |Sessions] -- [Утиліти URL |urls] -- [Конфігурація |configuration] diff --git a/http/uk/@meta.texy b/http/uk/@meta.texy deleted file mode 100644 index 96e2d9752a..0000000000 --- a/http/uk/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документація Nette}} diff --git a/http/uk/configuration.texy b/http/uk/configuration.texy deleted file mode 100644 index 1fddd1b3be..0000000000 --- a/http/uk/configuration.texy +++ /dev/null @@ -1,171 +0,0 @@ -Конфігурація HTTP -***************** - -.[perex] -Огляд параметрів конфігурації для Nette HTTP. - -Якщо ви не використовуєте весь фреймворк, а лише цю бібліотеку, прочитайте, [як завантажити конфігурацію|bootstrap:]. - - -HTTP-заголовки -============== - -```neon -http: - # заголовки, які надсилаються з кожним запитом - headers: - X-Powered-By: MyCMS - X-Content-Type-Options: nosniff - X-XSS-Protection: '1; mode=block' - - # впливає на заголовок X-Frame-Options - frames: ... # (string|bool) за замовчуванням 'SAMEORIGIN' -``` - -Фреймворк з міркувань безпеки надсилає заголовок `X-Frame-Options: SAMEORIGIN`, який вказує, що сторінку можна відображати всередині іншої сторінки (в елементі `<iframe>`) лише якщо вона знаходиться на тому ж домені. Це може бути небажаним у деяких ситуаціях (наприклад, якщо ви розробляєте програму для Facebook), тому поведінку можна змінити, встановивши `frames: http://allowed-host.com` або `frames: true`. - - -Content Security Policy ------------------------ - -Легко можна створювати заголовки `Content-Security-Policy` (далі CSP), їх опис ви знайдете в [опису CSP |https://content-security-policy.com]. Директиви CSP (наприклад, `script-src`) можуть бути записані або як рядки відповідно до специфікації, або як масив значень для кращої читабельності. Тоді не потрібно навколо ключових слів, як-от `'self'`, ставити лапки. Nette також автоматично генерує значення `nonce`, тому в заголовку буде, наприклад, `'nonce-y4PopTLM=='`. - -```neon -http: - # Content Security Policy - csp: - # рядок у форматі відповідно до специфікації CSP - default-src: "'self' https://example.com" - - # масив значень - script-src: - - nonce - - strict-dynamic - - self - - https://example.com - - # bool у випадку перемикачів - upgrade-insecure-requests: true - block-all-mixed-content: false -``` - -У шаблонах використовуйте `<script n:nonce>...</script>`, і значення nonce доповниться автоматично. Робити безпечні сайти в Nette справді легко. - -Подібно можна створити й заголовки `Content-Security-Policy-Report-Only` (які можна використовувати одночасно з CSP) та [Feature Policy|https://developers.google.com/web/updates/2018/06/feature-policy]: - -```neon -http: - # Content Security Policy Report-Only - cspReportOnly: - default-src: self - report-uri: 'https://my-report-uri-endpoint' - - # Feature Policy - featurePolicy: - unsized-media: none - geolocation: - - self - - https://example.com -``` - - -HTTP cookie ------------ - -Можна змінити стандартні значення деяких параметрів методу [Nette\Http\Response::setCookie() |response#setCookie] та сесії. - -```neon -http: - # область дії cookie за шляхом - cookiePath: ... # (string) за замовчуванням '/' - - # домени, які приймають cookie - cookieDomain: 'example.com' # (string|domain) за замовчуванням не встановлено - - # надсилати cookie лише через HTTPS? - cookieSecure: ... # (bool|auto) за замовчуванням auto - - # вимкне надсилання cookie, яку Nette використовує як захист від CSRF - disableNetteCookie: ... # (bool) за замовчуванням false -``` - -Атрибут `cookieDomain` визначає, які домени можуть приймати cookie. Якщо він не вказаний, cookie приймає той самий (під)домен, що й встановив його, *але не* його піддомени. Якщо `cookieDomain` вказаний, піддомени також включаються. Тому вказання `cookieDomain` є менш обмежувальним, ніж його відсутність. - -Наприклад, при `cookieDomain: nette.org` cookies доступні також на всіх піддоменах, таких як `doc.nette.org`. Того ж можна досягти також за допомогою спеціального значення `domain`, тобто `cookieDomain: domain`. - -Стандартне значення `auto` для атрибута `cookieSecure` означає, що якщо сайт працює на HTTPS, cookies будуть надсилатися з прапором `Secure` і, отже, будуть доступні лише через HTTPS. - - -HTTP-проксі ------------ - -Якщо сайт працює за HTTP-проксі, вкажіть його IP-адресу, щоб правильно працювало визначення з'єднання через HTTPS, а також IP-адреси клієнта. Тобто, щоб функції [Nette\Http\Request::getRemoteAddress() |request#getRemoteAddress] та [isSecured() |request#isSecured] повертали правильні значення, а в шаблонах генерувалися посилання з протоколом `https:`. - -```neon -http: - # IP-адреса, діапазон (напр. 127.0.0.1/8) або масив цих значень - proxy: 127.0.0.1 # (string|string[]) за замовчуванням не встановлено -``` - - -Сесія -===== - -Базові налаштування [сесій |sessions]: - -```neon -session: - # показувати панель сесії в Tracy Bar? - debugger: ... # (bool) за замовчуванням false - - # час неактивності, після якого сесія закінчиться - expiration: 14 days # (string) за замовчуванням '3 hours' - - # коли має запускатися сесія? - autoStart: ... # (smart|always|never) за замовчуванням 'smart' - - # обробник, сервіс, що реалізує інтерфейс SessionHandlerInterface - handler: @handlerService -``` - -Опція `autoStart` керує тим, коли має запускатися сесія. Значення `always` означає, що сесія запуститься завжди при запуску програми. Значення `smart` означає, що сесія запуститься при старті програми лише тоді, коли вона вже існує, або в момент, коли ми хочемо з неї читати або в неї записувати. І нарешті, значення `never` забороняє автоматичний запуск сесії. - -Далі можна налаштовувати всі PHP [директиви сесії |https://www.php.net/manual/en/session.configuration.php] (у форматі camelCase) та також [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. Приклад: - -```neon -session: - # 'session.name' запишемо як 'name' - name: MYID - - # 'session.save_path' запишемо як 'savePath' - savePath: "%tempDir%/sessions" -``` - - -Session cookie --------------- - -Session cookie надсилається з тими ж параметрами, що й [інші cookie |#HTTP cookie], але ці ви можете для неї змінити: - -```neon -session: - # домени, які приймають cookie - cookieDomain: 'example.com' # (string|domain) - - # обмеження при доступі з іншого домену - cookieSamesite: None # (Strict|Lax|None) за замовчуванням Lax -``` - -Атрибут `cookieSamesite` впливає на те, чи буде cookie надіслано при [доступі з іншого домену |nette:glossary#SameSite cookie], що забезпечує певний захист від атак [Cross-Site Request Forgery |nette:glossary#Cross-Site Request Forgery CSRF] (CSRF). - - -Сервіси DI -========== - -Ці сервіси додаються до DI-контейнера: - -| Назва | Тип | Опис -|----------------------------------------------------- -| `http.request` | [api:Nette\Http\Request] | [HTTP-запит| request] -| `http.response` | [api:Nette\Http\Response] | [HTTP-відповідь| response] -| `session.session` | [api:Nette\Http\Session] | [керування сесіями| sessions] diff --git a/http/uk/request.texy b/http/uk/request.texy deleted file mode 100644 index b3537c1f0d..0000000000 --- a/http/uk/request.texy +++ /dev/null @@ -1,407 +0,0 @@ -HTTP-запит -********** - -.[perex] -Nette інкапсулює HTTP-запит в об'єкти зі зрозумілим API і водночас надає фільтр санітизації. - -HTTP-запит представляє об'єкт [api:Nette\Http\Request]. Якщо ви працюєте з Nette, цей об'єкт автоматично створюється фреймворком, і ви можете отримати його за допомогою [впровадження залежностей |dependency-injection:passing-dependencies]. У презентерах достатньо лише викликати метод `$this->getHttpRequest()`. Якщо ви працюєте поза Nette Framework, ви можете створити об'єкт за допомогою [#RequestFactory]. - -Великою перевагою Nette є те, що при створенні об'єкта він автоматично очищає всі вхідні параметри GET, POST, COOKIE, а також URL від керуючих символів та недійсних UTF-8 послідовностей. З цими даними потім можна безпечно працювати далі. Очищені дані потім використовуються в презентерах та формах. - -→ [Встановлення та вимоги |@home#Встановлення] - - -Nette\Http\Request -================== - -Цей об'єкт є immutable (незмінним). Він не має жодних сеттерів, має лише один так званий wither `withUrl()`, який не змінює об'єкт, а повертає новий екземпляр зі зміненим значенням. - - -withUrl(Nette\Http\UrlScript $url): Nette\Http\Request .[method] ----------------------------------------------------------------- -Повертає клон з іншим URL. - - -getUrl(): Nette\Http\UrlScript .[method] ----------------------------------------- -Повертає URL запиту як об'єкт [UrlScript |urls#UrlScript]. - -```php -$url = $httpRequest->getUrl(); -echo $url; // https://doc.nette.org/uk/?action=edit -echo $url->getHost(); // nette.org -``` - -Попередження: браузери не надсилають на сервер фрагмент, тому `$url->getFragment()` повертатиме порожній рядок. - - -getQuery(?string $key=null): string|array|null .[method] --------------------------------------------------------- -Повертає параметри GET-запиту. - -```php -$all = $httpRequest->getQuery(); // повертає масив усіх параметрів з URL -$id = $httpRequest->getQuery('id'); // повертає GET-параметр 'id' (або null) -``` - - -getPost(?string $key=null): string|array|null .[method] -------------------------------------------------------- -Повертає параметри POST-запиту. - -```php -$all = $httpRequest->getPost(); // повертає масив усіх параметрів з POST -$id = $httpRequest->getPost('id'); // повертає POST-параметр 'id' (або null) -``` - - -getFile(string|string[] $key): Nette\Http\FileUpload|array|null .[method] -------------------------------------------------------------------------- -Повертає [завантаження |#Завантажені файли] як об'єкт [api:Nette\Http\FileUpload]: - -```php -$file = $httpRequest->getFile('avatar'); -if ($file?->hasFile()) { // чи був якийсь файл завантажений? - $file->getUntrustedName(); // ім'я файлу, надіслане користувачем - $file->getSanitizedName(); // ім'я без небезпечних символів -} -``` - -Для доступу до вкладеної структури вкажіть масив ключів. - -```php -//<input type="file" name="my-form[details][avatar]" multiple> -$file = $request->getFile(['my-form', 'details', 'avatar']); -``` - -Оскільки не можна довіряти даним ззовні і, отже, покладатися на структуру файлів, цей спосіб є безпечнішим, ніж, наприклад, `$request->getFiles()['my-form']['details']['avatar']`, який може зазнати невдачі. - - -getFiles(): array .[method] ---------------------------- -Повертає дерево [всіх завантажень |#Завантажені файли] у нормалізованій структурі, листками якої є об'єкти [api:Nette\Http\FileUpload]: - -```php -$files = $httpRequest->getFiles(); -``` - - -getCookie(string $key): string|array|null .[method] ---------------------------------------------------- -Повертає cookie або `null`, якщо вона не існує. - -```php -$sessId = $httpRequest->getCookie('sess_id'); -``` - - -getCookies(): array .[method] ------------------------------ -Повертає всі cookies. - -```php -$cookies = $httpRequest->getCookies(); -``` - - -getMethod(): string .[method] ------------------------------ -Повертає HTTP-метод, яким був зроблений запит. - -```php -$httpRequest->getMethod(); // GET, POST, HEAD, PUT -``` - - -isMethod(string $method): bool .[method] ----------------------------------------- -Перевіряє HTTP-метод, яким був зроблений запит. Параметр нечутливий до регістру. - -```php -if ($httpRequest->isMethod('GET')) // ... -``` - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Повертає HTTP-заголовок або `null`, якщо він не існує. Параметр нечутливий до регістру. - -```php -$userAgent = $httpRequest->getHeader('User-Agent'); -``` - - -getHeaders(): array .[method] ------------------------------ -Повертає всі HTTP-заголовки як асоціативний масив. - -```php -$headers = $httpRequest->getHeaders(); -echo $headers['Content-Type']; -``` - - -isSecured(): bool .[method] ---------------------------- -Чи є з'єднання зашифрованим (HTTPS)? Для правильної роботи може знадобитися [налаштувати проксі |configuration#HTTP-проксі]. - - -isSameSite(): bool .[method] ----------------------------- -Чи надходить запит з того самого (під)домену і чи ініційований він кліком на посилання? Nette для визначення використовує cookie `_nss` (раніше `nette-samesite`). - - -isAjax(): bool .[method] ------------------------- -Чи це AJAX-запит? - - -getRemoteAddress(): ?string .[method] -------------------------------------- -Повертає IP-адресу користувача. Для правильної роботи може знадобитися [налаштувати проксі |configuration#HTTP-проксі]. - - -getRemoteHost(): ?string .[method deprecated] ---------------------------------------------- -Повертає DNS-перетворення IP-адреси користувача. Для правильної роботи може знадобитися [налаштувати проксі |configuration#HTTP-проксі]. - - -getBasicCredentials(): ?array .[method] ---------------------------------------- -Повертає облікові дані для [Basic HTTP authentication |https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication]. - -```php -[$user, $password] = $httpRequest->getBasicCredentials(); -``` - - -getRawBody(): ?string .[method] -------------------------------- -Повертає тіло HTTP-запиту. - -```php -$body = $httpRequest->getRawBody(); -``` - - -detectLanguage(array $langs): ?string .[method] ------------------------------------------------ -Визначає мову. Як параметр `$langs` передаємо масив мов, які підтримує програма, і вона поверне ту, яку браузер відвідувача хотів би бачити найбільше. Це не магія, просто використовується заголовок `Accept-Language`. Якщо збігу не знайдено, повертає `null`. - -```php -// браузер надсилає, напр., Accept-Language: uk,en-us;q=0.8,en;q=0.5,sl;q=0.3 - -$langs = ['hu', 'pl', 'uk']; // мови, підтримувані програмою -echo $httpRequest->detectLanguage($langs); // uk -``` - - -RequestFactory -============== - -Клас [api:Nette\Http\RequestFactory] служить для створення екземпляра `Nette\Http\Request`, який представляє поточний HTTP-запит. (Якщо ви працюєте з Nette, об'єкт HTTP-запиту автоматично створюється фреймворком.) - -```php -$factory = new Nette\Http\RequestFactory; -$httpRequest = $factory->fromGlobals(); -``` - -Метод `fromGlobals()` створює об'єкт запиту на основі поточних глобальних змінних PHP (`$_GET`, `$_POST`, `$_COOKIE`, `$_FILES` та `$_SERVER`). При створенні об'єкта він автоматично очищає всі вхідні параметри GET, POST, COOKIE, а також URL від керуючих символів та недійсних UTF-8 послідовностей, що забезпечує безпеку при подальшій роботі з цими даними. - -RequestFactory можна конфігурувати перед викликом `fromGlobals()`: - -- методом `$factory->setBinary()` вимкнете автоматичне очищення вхідних параметрів від керуючих символів та недійсних UTF-8 послідовностей. -- методом `$factory->setProxy(...)` вкажете IP-адресу [проксі-сервера |configuration#HTTP-проксі], що необхідно для правильного визначення IP-адреси користувача. - -RequestFactory дозволяє визначати фільтри, які автоматично трансформують частини URL запиту. Ці фільтри видаляють небажані символи з URL, які там можуть бути вставлені, наприклад, неправильною реалізацією систем коментарів на різних сайтах: - -```php -// видалення пробілів зі шляху -$requestFactory->urlFilters['path']['%20'] = ''; - -// видалення крапки, коми або правої дужки з кінця URI -$requestFactory->urlFilters['url']['[.,)]$'] = ''; - -// очищення шляху від подвійних слешів (стандартний фільтр) -$requestFactory->urlFilters['path']['/{2,}'] = '/'; -``` - -Перший ключ `'path'` або `'url'` визначає, до якої частини URL застосовується фільтр. Другий ключ — це регулярний вираз, який потрібно знайти, а значення — це заміна, яка використовується замість знайденого тексту. - - -Завантажені файли -================= - -Метод `Nette\Http\Request::getFiles()` повертає масив усіх завантажень у нормалізованій структурі, листками якої є об'єкти [api:Nette\Http\FileUpload]. Вони інкапсулюють дані, надіслані елементом форми `<input type=file>`. - -Структура відображає іменування елементів у HTML. У найпростішому випадку це може бути єдиний іменований елемент форми, надісланий як: - -```latte -<input type="file" name="avatar"> -``` - -У цьому випадку `$request->getFiles()` повертає масив: - -```php -[ - 'avatar' => /* FileUpload instance */ -] -``` - -Об'єкт `FileUpload` створюється навіть у випадку, якщо користувач не надіслав жодного файлу або надсилання не вдалося. Чи був файл надісланий, повертає метод `hasFile()`: - -```php -$request->getFile('avatar')?->hasFile(); -``` - -У випадку назви елемента, що використовує нотацію для масиву: - -```latte -<input type="file" name="my-form[details][avatar]"> -``` - -повернене дерево виглядає так: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatar' => /* FileUpload instance */ - ], - ], -] -``` - -Можна створити і масив файлів: - -```latte -<input type="file" name="my-form[details][avatars][]" multiple> -``` - -У такому випадку структура виглядає так: - -```php -[ - 'my-form' => [ - 'details' => [ - 'avatars' => [ - 0 => /* FileUpload instance */, - 1 => /* FileUpload instance */, - 2 => /* FileUpload instance */, - ], - ], - ], -] -``` - -Доступ до індексу 1 вкладеного масиву найкраще отримати так: - -```php -$file = $request->getFile(['my-form', 'details', 'avatars', 1]); -if ($file instanceof FileUpload) { - // ... -} -``` - -Оскільки не можна довіряти даним ззовні і, отже, покладатися на структуру файлів, цей спосіб є безпечнішим, ніж, наприклад, `$request->getFiles()['my-form']['details']['avatars'][1]`, який може зазнати невдачі. - - -Огляд методів `FileUpload` .{toc: FileUpload} ---------------------------------------------- - - -hasFile(): bool .[method] -------------------------- -Повертає `true`, якщо користувач завантажив якийсь файл. - - -isOk(): bool .[method] ----------------------- -Повертає `true`, якщо файл був завантажений успішно. - - -getError(): int .[method] -------------------------- -Повертає код помилки при завантаженні файлу. Це одна з констант [UPLOAD_ERR_XXX|http://php.net/manual/en/features.file-upload.errors.php]. У випадку, якщо завантаження пройшло успішно, повертає `UPLOAD_ERR_OK`. - - -move(string $dest) .[method] ----------------------------- -Переміщує завантажений файл у нове місце. Якщо цільовий файл вже існує, він буде перезаписаний. - -```php -$file->move('/path/to/files/name.ext'); -``` - - -getContents(): ?string .[method] --------------------------------- -Повертає вміст завантаженого файлу. У випадку, якщо завантаження не було успішним, повертає `null`. - - -getContentType(): ?string .[method] ------------------------------------ -Визначає MIME content type завантаженого файлу на основі його сигнатури. У випадку, якщо завантаження не було успішним або визначення не вдалося, повертає `null`. - -.[caution] -Вимагає PHP-розширення `fileinfo`. - - -getUntrustedName(): string .[method] ------------------------------------- -Повертає оригінальну назву файлу, як її надіслав браузер. - -.[caution] -Не довіряйте значенню, повернутому цим методом. Клієнт міг надіслати шкідливу назву файлу з наміром пошкодити або зламати вашу програму. - - -getSanitizedName(): string .[method] ------------------------------------- -Повертає санітизовану назву файлу. Містить лише ASCII-символи `[a-zA-Z0-9.-]`. Якщо назва не містить таких символів, поверне `'unknown'`. Якщо файл є зображенням у форматі JPEG, PNG, GIF, WebP або AVIF, поверне також правильне розширення. - -.[caution] -Вимагає PHP-розширення `fileinfo`. - - -getSuggestedExtension(): ?string .[method]{data-version:3.2.4} --------------------------------------------------------------- -Повертає відповідне розширення файлу (без крапки), що відповідає виявленому MIME-типу. - -.[caution] -Вимагає PHP-розширення `fileinfo`. - - -getUntrustedFullPath(): string .[method] ----------------------------------------- -Повертає оригінальний шлях до файлу, як його надіслав браузер при завантаженні папки. Повний шлях доступний лише в PHP 8.1 та вище. У попередніх версіях цей метод повертає оригінальну назву файлу. - -.[caution] -Не довіряйте значенню, повернутому цим методом. Клієнт міг надіслати шкідливу назву файлу з наміром пошкодити або зламати вашу програму. - - -getSize(): int .[method] ------------------------- -Повертає розмір завантаженого файлу. У випадку, якщо завантаження не було успішним, повертає `0`. - - -getTemporaryFile(): string .[method] ------------------------------------- -Повертає шлях до тимчасового розташування завантаженого файлу. У випадку, якщо завантаження не було успішним, повертає `''`. - - -isImage(): bool .[method] -------------------------- -Повертає `true`, якщо завантажений файл є зображенням у форматі JPEG, PNG, GIF, WebP або AVIF. Визначення відбувається на основі його сигнатури і не перевіряється цілісність усього файлу. Чи не пошкоджене зображення, можна з'ясувати, наприклад, спробувавши його [завантажити |#toImage]. - -.[caution] -Вимагає PHP-розширення `fileinfo`. - - -getImageSize(): ?array .[method] --------------------------------- -Повертає пару `[ширина, висота]` з розмірами завантаженого зображення. У випадку, якщо завантаження не було успішним або це не дійсне зображення, повертає `null`. - - -toImage(): Nette\Utils\Image .[method] --------------------------------------- -Завантажує зображення як об'єкт [Image|utils:images]. У випадку, якщо завантаження не було успішним або це не дійсне зображення, викине виняток `Nette\Utils\ImageException`. diff --git a/http/uk/response.texy b/http/uk/response.texy deleted file mode 100644 index d9dcc7e50f..0000000000 --- a/http/uk/response.texy +++ /dev/null @@ -1,150 +0,0 @@ -HTTP-відповідь -************** - -.[perex] -Nette інкапсулює HTTP-відповідь в об'єкти зі зрозумілим API. - -HTTP-відповідь представляє об'єкт [api:Nette\Http\Response]. Якщо ви працюєте з Nette, цей об'єкт автоматично створюється фреймворком, і ви можете отримати його за допомогою [впровадження залежностей |dependency-injection:passing-dependencies]. У презентерах достатньо лише викликати метод `$this->getHttpResponse()`. - -→ [Встановлення та вимоги |@home#Встановлення] - - -Nette\Http\Response -=================== - -Об'єкт, на відміну від [Nette\Http\Request|request], є mutable (змінним), тобто за допомогою сеттерів ви можете змінювати стан, наприклад, надсилати заголовки. Не забувайте, що всі сеттери повинні бути викликані **перед надсиланням будь-якого виводу.** Чи був вже надісланий вивід, покаже метод `isSent()`. Якщо він повертає `true`, кожна спроба надіслати заголовок викличе виняток `Nette\InvalidStateException`. - - -setCode(int $code, ?string $reason=null) .[method] --------------------------------------------------- -Змінює [код стану відповіді |https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10]. Для кращої зрозумілості вихідного коду рекомендуємо для коду використовувати замість чисел [передбачені константи |api:Nette\Http\IResponse]. - -```php -$httpResponse->setCode(Nette\Http\Response::S404_NotFound); -``` - - -getCode(): int .[method] ------------------------- -Повертає код стану відповіді. - - -isSent(): bool .[method] ------------------------- -Повертає, чи вже відбулося надсилання заголовків з сервера до браузера, і отже, вже неможливо надсилати заголовки чи змінювати код стану. - - -setHeader(string $name, string $value) .[method] ------------------------------------------------- -Надсилає HTTP-заголовок і **перезаписує** раніше надісланий заголовок з тією ж назвою. - -```php -$httpResponse->setHeader('Pragma', 'no-cache'); -``` - - -addHeader(string $name, string $value) .[method] ------------------------------------------------- -Надсилає HTTP-заголовок і **не перезаписує** раніше надісланий заголовок з тією ж назвою. - -```php -$httpResponse->addHeader('Accept', 'application/json'); -$httpResponse->addHeader('Accept', 'application/xml'); -``` - - -deleteHeader(string $name) .[method] ------------------------------------- -Видаляє раніше надісланий HTTP-заголовок. - - -getHeader(string $header): ?string .[method] --------------------------------------------- -Повертає надісланий HTTP-заголовок або `null`, якщо такий не існує. Параметр нечутливий до регістру. - -```php -$pragma = $httpResponse->getHeader('Pragma'); -``` - - -getHeaders(): array .[method] ------------------------------ -Повертає всі надіслані HTTP-заголовки як асоціативний масив. - -```php -$headers = $httpResponse->getHeaders(); -echo $headers['Pragma']; -``` - - -setContentType(string $type, ?string $charset=null) .[method] -------------------------------------------------------------- -Змінює заголовок `Content-Type`. - -```php -$httpResponse->setContentType('text/plain', 'UTF-8'); -``` - - -redirect(string $url, int $code=self::S302_Found): void .[method] ------------------------------------------------------------------ -Перенаправляє на інший URL. Не забудьте потім завершити скрипт. - -```php -$httpResponse->redirect('http://example.com'); -exit; -``` - - -setExpiration(?string $time) .[method] --------------------------------------- -Встановлює термін дії HTTP-документа за допомогою заголовків `Cache-Control` та `Expires`. Параметром є або часовий інтервал (як текст), або `null`, що заборонить кешування. - -```php -// кеш у браузері закінчиться через годину -$httpResponse->setExpiration('1 hour'); -``` - - -sendAsFile(string $fileName) .[method] --------------------------------------- -Відповідь буде завантажена за допомогою діалогового вікна *Зберегти як* під вказаною назвою. Сам файл при цьому не надсилається. - -```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] -------------------------------------------------------------------------------------------------------------------------------------------------------------------- -Надсилає cookie. Значення параметрів за замовчуванням: - -| `$path` | `'/'` | cookie має область дії на всі шляхи в (під)домені *(конфігурується)* -| `$domain` | `null` | що означає з областю дії на поточний (під)домен, але не на його піддомени *(конфігурується)* -| `$secure` | `true` | якщо сайт працює на HTTPS, інакше `false` *(конфігурується)* -| `$httpOnly` | `true` | cookie недоступна для JavaScript -| `$sameSite` | `'Lax'` | cookie може не надсилатися при [доступі з іншого домену |nette:glossary#SameSite cookie] - -Значення параметрів `$path`, `$domain` та `$secure` за замовчуванням можна змінити в [конфігурації |configuration#HTTP cookie]. - -Час можна вказувати як кількість секунд або рядок: - -```php -$httpResponse->setCookie('lang', 'uk', '100 days'); // Змінено мову на 'uk' -``` - -Параметр `$domain` визначає, які домени можуть приймати cookie. Якщо він не вказаний, cookie приймає той самий (під)домен, що й встановив його, але не його піддомени. Якщо `$domain` вказаний, піддомени також включаються. Тому вказання `$domain` є менш обмежувальним, ніж його відсутність. Наприклад, при `$domain = 'nette.org'` cookies доступні також на всіх піддоменах, таких як `doc.nette.org`. - -Для значення `$sameSite` ви можете використовувати константи `Response::SameSiteLax`, `Response::SameSiteStrict` та `Response::SameSiteNone`. - - -deleteCookie(string $name, ?string $path=null, ?string $domain=null, ?bool $secure=null): void .[method] --------------------------------------------------------------------------------------------------------- -Видаляє cookie. Значення параметрів за замовчуванням: -- `$path` з областю дії на всі каталоги (`'/'`) -- `$domain` з областю дії на поточний (під)домен, але не на його піддомени -- `$secure` керується налаштуваннями в [конфігурації |configuration#HTTP cookie] - -```php -$httpResponse->deleteCookie('lang'); -``` diff --git a/http/uk/sessions.texy b/http/uk/sessions.texy deleted file mode 100644 index e69a4d6490..0000000000 --- a/http/uk/sessions.texy +++ /dev/null @@ -1,211 +0,0 @@ -Сесії -***** - -<div class=perex> - -HTTP — це протокол без стану, однак майже кожна програма потребує зберігати стан між запитами, наприклад, вміст кошика покупок. Саме для цього служать сесії або сеанси. Покажемо, - -- як використовувати сесії -- як уникнути конфліктів імен -- як налаштувати термін дії - -</div> - -При використанні сесій кожен користувач отримує унікальний ідентифікатор, який називається ID сесії, що передається в cookie. Він служить ключем до даних сесії. На відміну від cookies, які зберігаються на стороні браузера, дані в сесії зберігаються на стороні сервера. - -Сесію налаштовуємо в [конфігурації |configuration#Сесія], особливо важливим є вибір терміну дії. - -Керування сесіями здійснює об'єкт [api:Nette\Http\Session], до якого ви можете отримати доступ, попросивши передати його за допомогою [впровадження залежностей |dependency-injection:passing-dependencies]. У презентерах достатньо лише викликати `$session = $this->getSession()`. - -→ [Встановлення та вимоги |@home#Встановлення] - - -Запуск сесії -============ - -Nette за замовчуванням автоматично розпочинає сесію в момент, коли ми починаємо з неї читати або в неї записувати дані. Ручний запуск сесії здійснюється за допомогою `$session->start()`. - -PHP надсилає при запуску сесії HTTP-заголовки, що впливають на кешування, див. [php:session_cache_limiter], і, можливо, cookie з ID сесії. Тому необхідно завжди запускати сесію ще до надсилання будь-якого виводу в браузер, інакше буде викинуто виняток. Якщо ви знаєте, що під час відображення сторінки буде використовуватися сесія, запустіть її вручну заздалегідь, наприклад, у презентері. - -У режимі розробки сесію запускає Tracy, оскільки вона використовує її для відображення смуг з перенаправленнями та AJAX-запитами в Tracy Bar. - - -Секції -====== - -У чистому PHP сховище даних сесії реалізовано як масив, доступний через глобальну змінну `$_SESSION`. Проблема полягає в тому, що програми зазвичай складаються з цілої низки взаємно незалежних частин, і якщо всі вони мають доступ лише до одного масиву, рано чи пізно виникне колізія імен. - -Nette Framework вирішує цю проблему, розділяючи весь простір на секції (об'єкти [api:Nette\Http\SessionSection]). Кожна одиниця потім використовує свою секцію з унікальною назвою, і жодної колізії вже виникнути не може. - -Секцію отримуємо з сесії: - -```php -$section = $session->getSection('unique_name'); // Змінено на англійську для прикладу -``` - -У презентері достатньо використати `getSession()` з параметром: - -```php -// $this є Presenter -$section = $this->getSession('unique_name'); // Змінено на англійську для прикладу -``` - -Перевірити існування секції можна методом `$session->hasSection('unique_name')`. - -З самою секцією потім працювати дуже легко за допомогою методів `set()`, `get()` та `remove()`: - -```php -// запис змінної -$section->set('userName', 'frank'); // Змінено на англійську для прикладу - -// читання змінної, поверне null, якщо не існує -echo $section->get('userName'); - -// видалення змінної -$section->remove('userName'); -``` - -Для отримання всіх змінних із секції можна використовувати цикл `foreach`: - -```php -foreach ($section as $key => $val) { - echo "$key = $val"; -} -``` - - -Налаштування терміну дії ------------------------- - -Для окремих секцій або навіть окремих змінних можна встановити термін дії. Ми можемо, наприклад, дозволити закінчитися терміну дії входу користувача через 20 хвилин, але при цьому продовжувати пам'ятати вміст кошика. - -```php -// секція закінчиться через 20 хвилин -$section->setExpiration('20 minutes'); -``` - -Для налаштування терміну дії окремих змінних служить третій параметр методу `set()`: - -```php -// змінна 'flash' закінчиться вже через 30 секунд -$section->set('flash', $message, '30 seconds'); -``` - -.[note] -Не забувайте, що термін дії всієї сесії (див. [конфігурацію сесії |configuration#Сесія]) повинен бути таким самим або більшим, ніж термін, встановлений для окремих секцій чи змінних. - -Скасування раніше встановленого терміну дії досягається методом `removeExpiration()`. Негайне скасування всієї секції забезпечує метод `remove()`. - - -Події $onStart, $onBeforeWrite ------------------------------- - -Об'єкт `Nette\Http\Session` має [події |nette:glossary#Події události] `$onStart` та `$onBeforeWrite`, тому ви можете додати callback-и, які будуть викликані після запуску сесії або перед її записом на диск та подальшим завершенням. - -```php -$session->onBeforeWrite[] = function () { - // запишемо дані в сесію - $this->section->set('basket', $this->basket); -}; -``` - - -Керування сесіями -================= - -Огляд методів класу `Nette\Http\Session` для керування сесіями: - -<div class=wiki-methods-brief> - - -start(): void .[method] ------------------------ -Розпочинає сесію. - - -isStarted(): bool .[method] ---------------------------- -Чи розпочата сесія? - - -close(): void .[method] ------------------------ -Завершує сесію. Сесія автоматично завершується в кінці виконання скрипта. - - -destroy(): void .[method] -------------------------- -Завершує та видаляє сесію. - - -exists(): bool .[method] ------------------------- -Чи містить HTTP-запит cookie з ID сесії? - - -regenerateId(): void .[method] ------------------------------- -Генерує нове випадкове ID сесії. Дані залишаються збереженими. - - -getId(): string .[method] -------------------------- -Повертає ID сесії. - -</div> - - -Конфігурація ------------- - -Сесію налаштовуємо в [конфігурації |configuration#Сесія]. Якщо ви пишете програму, яка не використовує DI-контейнер, для конфігурації служать ці методи. Вони повинні бути викликані ще до запуску сесії. - -<div class=wiki-methods-brief> - - -setName(string $name): static .[method] ---------------------------------------- -Встановлює назву cookie, в якій передається ID сесії. Стандартна назва — `PHPSESSID`. Це корисно у випадку, коли в рамках одного сайту ви запускаєте кілька різних програм. - - -getName(): string .[method] ---------------------------- -Повертає назву cookie, в якій передається ID сесії. - - -setOptions(array $options): static .[method] --------------------------------------------- -Конфігурує сесію. Можна налаштовувати всі PHP [директиви сесії |https://www.php.net/manual/en/session.configuration.php] (у форматі camelCase, наприклад, замість `session.save_path` запишемо `savePath`), а також [readAndClose |https://www.php.net/manual/en/function.session-start.php#refsect1-function.session-start-parameters]. - - -setExpiration(?string $time): static .[method] ----------------------------------------------- -Встановлює час неактивності, після якого сесія закінчиться. - - -setCookieParameters(string $path, ?string $domain=null, ?bool $secure=null, ?string $samesite=null): static .[method] ---------------------------------------------------------------------------------------------------------------------- -Налаштування параметрів для cookie. Значення параметрів за замовчуванням можна змінити в [конфігурації |configuration#Session cookie]. - - -setSavePath(string $path): static .[method] -------------------------------------------- -Встановлює каталог, куди зберігаються файли сесій. - - -setHandler(\SessionHandlerInterface $handler): static .[method] ---------------------------------------------------------------- -Налаштування власного обробника, див. [документацію PHP|https://www.php.net/manual/en/class.sessionhandlerinterface.php]. - -</div> - - -Безпека перш за все -=================== - -Сервер припускає, що він спілкується постійно з тим самим користувачем, доки запити супроводжуються тим самим ID сесії. Завданням механізмів безпеки є забезпечення того, щоб це справді було так, і щоб неможливо було ідентифікатор вкрасти або підсунути. - -Тому Nette Framework правильно конфігурує PHP-директиви, щоб ID сесії передавався лише в cookie, зробив його недоступним для JavaScript та ігнорував можливі ідентифікатори в URL. Крім того, у критичні моменти, наприклад, при вході користувача, він генерує нове ID сесії. - -.[note] -Для конфігурації PHP використовується функція ini_set, яку, на жаль, деякі хостинги забороняють. Якщо це стосується і вашого хостера, спробуйте домовитися з ним, щоб він дозволив вам використовувати цю функцію або хоча б налаштував сервер. diff --git a/http/uk/urls.texy b/http/uk/urls.texy deleted file mode 100644 index 72448ef8be..0000000000 --- a/http/uk/urls.texy +++ /dev/null @@ -1,266 +0,0 @@ -Робота з URL -************ - -.[perex] -Класи [#Url], [#UrlImmutable] та [#UrlScript] дозволяють легко генерувати, парсити та маніпулювати URL. - -→ [Встановлення та вимоги |@home#Встановлення] - - -Url -=== - -Клас [api:Nette\Http\Url] дозволяє легко працювати з URL та його окремими компонентами, які відображає ця схема: - -/--pre - scheme user password host port path query fragment - | | | | | | | | - /--\ /--\ /------\ /-------\ /--\/----------\ /--------\ /----\ - <b>http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer</b> - \______\__________________________/ - | | - hostUrl authority -\-- - -Генерування URL є інтуїтивним: - -```php -use Nette\Http\Url; - -$url = new Url; -$url->setScheme('https') - ->setHost('localhost') - ->setPath('/edit') - ->setQueryParameter('foo', 'bar'); - -echo $url; // 'https://localhost/edit?foo=bar' -``` - -Можна також розпарсити URL і далі з ним маніпулювати: - -```php -$url = new Url( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); -``` - -Клас `Url` реалізує інтерфейс `JsonSerializable` і має метод `__toString()`, тому об'єкт можна вивести або використовувати в даних, переданих до `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Компоненти URL .[method] ------------------------- - -Для повернення або зміни окремих компонентів URL вам доступні ці методи: - -.[language-php] -| Setter | Getter | Значення, що повертається -|-------------------------------------------------------------------------------------------- -| `setScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `setUser(string $user)` | `getUser(): string` | `'john'` -| `setPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `setHost(string $host)` | `getHost(): string` | `'nette.org'` -| `setPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `setPath(string $path)` | `getPath(): string` | `'/en/download'` -| `setQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `setFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | цілий URL - -Попередження: Коли ви працюєте з URL, отриманим з [HTTP-запиту|request], майте на увазі, що він не міститиме фрагмент, оскільки браузер його не надсилає на сервер. - -Ми можемо працювати і з окремими query-параметрами за допомогою: - -.[language-php] -| Setter | Getter -|--------------------------------------------------- -| `setQuery(string\|array $query)` | `getQueryParameters(): array` -| `setQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Повертає праву чи ліву частину хоста. Так це працює, якщо хост — `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)` | `''` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Перевіряє, чи два URL однакові. - -```php -$url->isEqual('https://nette.org'); -``` - - -Url::isAbsolute(string $url): bool .[method]{data-version:3.3.2} ----------------------------------------------------------------- -Перевіряє, чи є URL абсолютним. URL вважається абсолютним, якщо він починається зі схеми (наприклад, http, https, ftp), за якою слідує двокрапка. - -```php -Url::isAbsolute('https://nette.org'); // true -Url::isAbsolute('//nette.org'); // false -``` - - -Url::removeDotSegments(string $path): string .[method]{data-version:3.3.2} --------------------------------------------------------------------------- -Нормалізує шлях в URL, видаляючи спеціальні сегменти `.` та `..`. Метод видаляє надлишкові елементи шляху так само, як це роблять веб-браузери. - -```php -Url::removeDotSegments('/path/../subtree/./file.txt'); // '/subtree/file.txt' -Url::removeDotSegments('/../foo/./bar'); // '/foo/bar' -Url::removeDotSegments('./today/../file.txt'); // 'file.txt' -``` - - -UrlImmutable -============ - -Клас [api:Nette\Http\UrlImmutable] є immutable (незмінною) альтернативою класу [#Url] (подібно до того, як у PHP `DateTimeImmutable` є незмінною альтернативою `DateTime`). Замість сеттерів він має так звані wither-и, які не змінюють об'єкт, а повертають нові екземпляри зі зміненим значенням: - -```php -use Nette\Http\UrlImmutable; - -$url = new UrlImmutable( - 'http://john:xyz%2A12@nette.org:8080/en/download?name=param#footer', -); - -$newUrl = $url - ->withUser('') - ->withPassword('') - ->withPath('/uk/'); - -echo $newUrl; // 'http://john:xyz%2A12@nette.org:8080/uk/?name=param#footer' -``` - -Клас `UrlImmutable` реалізує інтерфейс `JsonSerializable` і має метод `__toString()`, тому об'єкт можна вивести або використовувати в даних, переданих до `json_encode()`. - -```php -echo $url; -echo json_encode([$url]); -``` - - -Компоненти URL .[method] ------------------------- - -Для повернення або зміни окремих компонентів URL служать методи: - -.[language-php] -| Wither | Getter | Значення, що повертається -|-------------------------------------------------------------------------------------------- -| `withScheme(string $scheme)` | `getScheme(): string` | `'http'` -| `withUser(string $user)` | `getUser(): string` | `'john'` -| `withPassword(string $password)` | `getPassword(): string` | `'xyz*12'` -| `withHost(string $host)` | `getHost(): string` | `'nette.org'` -| `withPort(int $port)` | `getPort(): ?int` | `8080` -| | `getDefaultPort(): ?int` | `80` -| `withPath(string $path)` | `getPath(): string` | `'/en/download'` -| `withQuery(string\|array $query)` | `getQuery(): string` | `'name=param'` -| `withFragment(string $fragment)` | `getFragment(): string` | `'footer'` -| | `getAuthority(): string` | `'john:xyz%2A12@nette.org:8080'` -| | `getHostUrl(): string` | `'http://john:xyz%2A12@nette.org:8080'` -| | `getAbsoluteUrl(): string` | цілий URL - -Метод `withoutUserInfo()` видаляє `user` та `password`. - -Ми можемо працювати і з окремими query-параметрами за допомогою: - -.[language-php] -| Wither | Getter -|----------------------------------------------- -| `withQuery(string\|array $query)` | `getQueryParameters(): array` -| `withQueryParameter(string $name, $val)` | `getQueryParameter(string $name)` - - -getDomain(int $level = 2): string .[method] -------------------------------------------- -Повертає праву чи ліву частину хоста. Так це працює, якщо хост — `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} ----------------------------------------------------------------------- -Виводить абсолютний URL так само, як браузер обробляє посилання на HTML-сторінці: -- якщо посилання є абсолютним URL (містить схему), воно використовується без змін -- якщо посилання починається з `//`, переймається лише схема з поточного URL -- якщо посилання починається з `/`, створюється абсолютний шлях від кореня домену -- в інших випадках URL складається відносно поточного шляху - -```php -$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' -``` - - -isEqual(string|Url $anotherUrl): bool .[method] ------------------------------------------------ -Перевіряє, чи два URL однакові. - -```php -$url->isEqual('https://nette.org'); -``` - - -UrlScript -========= - -Клас [api:Nette\Http\UrlScript] є нащадком [#UrlImmutable] і розширює його додатковими віртуальними компонентами URL, такими як кореневий каталог проєкту тощо. Так само, як і батьківський клас, він є immutable (незмінним) об'єктом. - -Наступна діаграма відображає компоненти, які розпізнає UrlScript: - -/--pre - baseUrl basePath relativePath relativeUrl - | | | | - /---------------/-----\/--------\---------------------------\ - <b>http://nette.org/admin/script.php/pathinfo/?name=param#footer</b> - \_______________/\________/ - | | - scriptPath pathInfo -\-- - -- `baseUrl` — це базова URL-адреса програми, включаючи домен та частину шляху до кореневого каталогу програми -- `basePath` — це частина шляху до кореневого каталогу програми -- `scriptPath` — це шлях до поточного скрипта -- `relativePath` — це назва скрипта (можливо, з додатковими сегментами шляху) відносно basePath -- `relativeUrl` — це вся частина URL після baseUrl, включаючи query string та фрагмент. -- `pathInfo` — сьогодні вже мало використовувана частина URL після назви скрипта - -Для повернення частин URL доступні методи: - -.[language-php] -| Getter | Значення, що повертається -|------------------------------------------------ -| `getScriptPath(): string` | `'/admin/script.php'` -| `getBasePath(): string` | `'/admin/'` -| `getBaseUrl(): string` | `'http://nette.org/admin/'` -| `getRelativePath(): string` | `'script.php'` -| `getRelativeUrl(): string` | `'script.php/pathinfo/?name=param#footer'` -| `getPathInfo(): string` | `'/pathinfo/'` - -Об'єкти `UrlScript` зазвичай безпосередньо не створюємо, але їх повертає метод [Nette\Http\Request::getUrl()|request] з уже правильно налаштованими компонентами для поточного HTTP-запиту. diff --git a/latte/bg/@home.texy b/latte/bg/@home.texy deleted file mode 100644 index a9fdedf8d8..0000000000 --- a/latte/bg/@home.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{maintitle: Latte – най-сигурните & наистина интуитивни шаблони за PHP}} -{{description: Latte е най-сигурната система за шаблони за PHP. Предотвратява много уязвимости в сигурността. Ще оцените неговия интуитивен синтаксис и много полезни функции.}} diff --git a/latte/bg/@left-menu.texy b/latte/bg/@left-menu.texy deleted file mode 100644 index d1c5d99428..0000000000 --- a/latte/bg/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -- [Да започнем с Latte |guide] -- [Защо да използваме шаблони? |why-use] -- Концепции ⚗️ - - [Безопасността преди всичко |safety-first] - - [Наследяване на шаблони |Template Inheritance] - - [Типова система |type-system] - - [Sandbox |Sandbox] - -- За дизайнери 🎨 - - [Синтаксис |syntax] - - [Тагове |tags] - - [Филтри |filters] - - [Функции |functions] - - [Съвети и трикове |recipes] - -- За разработчици 🧮 - - [Процедури за разработчици |develop] - - [Разширяване на Latte |extending-latte] - -- [Ръководства и процедури 💡|cookbook/@home] - - [Миграция от Twig |cookbook/migration-from-twig] - - [… други |cookbook/@home] - -- "Плейграунд .[link-external]":https://fiddle.nette.org/latte/ .{padding-top:1em} diff --git a/latte/bg/@menu.texy b/latte/bg/@menu.texy deleted file mode 100644 index c6324fc01c..0000000000 --- a/latte/bg/@menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -<ul> -- [Въведение |@home] -- [Документация |guide] -- "GitHub .[link-external]":https://github.com/nette/latte -<li class="dropdown"><a class="dropdown-toggle" href="#">Инструменти</a> - <ul class="dropdown-flyout"> -- [fiddle |https://fiddle.nette.org] -- [php2Latte |https://fiddle.nette.org/php2latte/] -- [twig2Latte|https://fiddle.nette.org/twig2latte/] - </ul> -</li> -</ul> diff --git a/latte/bg/@meta.texy b/latte/bg/@meta.texy deleted file mode 100644 index 4297aeff19..0000000000 --- a/latte/bg/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Документация на Latte}} diff --git a/latte/bg/compiler-passes.texy b/latte/bg/compiler-passes.texy deleted file mode 100644 index 198667999f..0000000000 --- a/latte/bg/compiler-passes.texy +++ /dev/null @@ -1,555 +0,0 @@ -Компилационни проходи -********************* - -.[perex] -Компилационните проходи предоставят мощен механизъм за анализ и модификация на Latte шаблони *след* тяхното парсване в абстрактно синтактично дърво (AST) и *преди* генерирането на финалния PHP код. Това позволява напреднала манипулация на шаблони, оптимизации, проверки за сигурност (като Sandbox) и събиране на информация за шаблоните. Това ръководство ще ви преведе през създаването на собствени компилационни проходи. - - -Какво е компилационен проход? -============================= - -За да разберете ролята на компилационните проходи, погледнете [процеса на компилация на Latte |custom-tags#Разбиране на процеса на компилация]. Както можете да видите, компилационните проходи оперират в ключова фаза, позволявайки дълбока намеса между първоначалното парсване и финалния изход на кода. - -По същество, компилационният проход е просто PHP callable обект (като функция, статичен метод или метод на инстанция), който приема един аргумент: коренния възел на AST на шаблона, който винаги е инстанция на `Latte\Compiler\Nodes\TemplateNode`. - -Основната цел на компилационния проход обикновено е една или и двете от следните: - -- Анализ: Обхождане на AST и събиране на информация за шаблона (напр. намиране на всички дефинирани блокове, проверка на използването на специфични тагове, осигуряване на спазването на определени ограничения за сигурност). -- Модификация: Промяна на структурата на AST или атрибутите на възлите (напр. автоматично добавяне на HTML атрибути, оптимизиране на определени комбинации от тагове, замяна на остарели тагове с нови, прилагане на правила на sandbox). - - -Регистрация -=========== - -Компилационните проходи се регистрират с помощта на метода [`getPasses()` на разширението |extending-latte#getPasses]. Този метод връща асоциативен масив, където ключовете са уникални имена на проходите (използвани вътрешно и за сортиране), а стойностите са PHP callable обекти, имплементиращи логиката на прохода. - -```php -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Extension; - -class MyExtension extends Extension -{ - public function getPasses(): array - { - return [ - 'modificationPass' => $this->modifyTemplateAst(...), - // ... други проходи ... - ]; - } - - public function modifyTemplateAst(TemplateNode $templateNode): void - { - // Имплементация... - } -} -``` - -Проходите, регистрирани от основните разширения на Latte и вашите собствени разширения, се изпълняват последователно. Редът може да бъде важен, особено ако един проход зависи от резултатите или модификациите на друг. Latte предоставя помощен механизъм за контрол на този ред, ако е необходимо; вижте документацията за [`Extension::getPasses()` |extending-latte#getPasses] за подробности. - - -Пример за AST -============= - -За по-добра представа за AST, добавяме пример. Това е изходният шаблон: - -```latte -{foreach $category->getItems() as $item} - <li>{$item->name|upper}</li> - {else} - no items found -{/foreach} -``` - -А това е неговото представяне под формата на AST: - -/--pre -Latte\Compiler\Nodes\<b>TemplateNode</b>( - Latte\Compiler\Nodes\<b>FragmentNode</b>( - - Latte\Essential\Nodes\<b>ForeachNode</b>( - expression: Latte\Compiler\Nodes\Php\Expression\<b>MethodCallNode</b>( - object: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$category') - name: Latte\Compiler\Nodes\Php\<b>IdentifierNode</b>('getItems') - ) - value: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$item') - content: Latte\Compiler\Nodes\<b>FragmentNode</b>( - - Latte\Compiler\Nodes\<b>TextNode</b>(' ') - - Latte\Compiler\Nodes\<b>Html\ElementNode</b>('li')( - content: Latte\Essential\Nodes\<b>PrintNode</b>( - expression: Latte\Compiler\Nodes\Php\Expression\<b>PropertyFetchNode</b>( - object: Latte\Compiler\Nodes\Php\Expression\<b>VariableNode</b>('$item') - name: Latte\Compiler\Nodes\Php\<b>IdentifierNode</b>('name') - ) - modifier: Latte\Compiler\Nodes\Php\<b>ModifierNode</b>( - filters: - - Latte\Compiler\Nodes\Php\<b>FilterNode</b>('upper') - ) - ) - ) - ) - else: Latte\Compiler\Nodes\<b>FragmentNode</b>( - - Latte\Compiler\Nodes\<b>TextNode</b>('no items found') - ) - ) - ) -) -\-- - - -Обхождане на AST с помощта на `NodeTraverser` -============================================= - -Ръчното писане на рекурсивни функции за обхождане на сложната структура на AST е уморително и податливо на грешки. Latte предоставя специален инструмент за тази цел: [api:Latte\Compiler\NodeTraverser]. Този клас имплементира [дизайн патърна Visitor |https://en.wikipedia.org/wiki/Visitor_pattern], благодарение на който обхождането на AST става систематично и лесно управляемо. - -Основното използване включва създаване на инстанция на `NodeTraverser` и извикване на нейния метод `traverse()`, като се предаде коренният възел на AST и един или два "visitor" callable обекта: - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes; - -(new NodeTraverser)->traverse( - $templateNode, - - // 'enter' visitor: Извиква се при влизане във възел (преди неговите деца) - enter: function (Node $node) { - echo "Влизане във възел от тип: " . $node::class . "\n"; - // Тук можете да изследвате възела - if ($node instanceof Nodes\TextNode) { - // echo "Намерен текст: " . $node->content . "\n"; - } - }, - - // 'leave' visitor: Извиква се при напускане на възел (след неговите деца) - leave: function (Node $node) { - echo "Напускане на възел от тип: " . $node::class . "\n"; - // Тук можете да извършвате действия след обработка на децата - }, -); -``` - -Можете да предоставите само `enter` visitor, само `leave` visitor, или и двата, в зависимост от вашите нужди. - -**`enter(Node $node)`:** Тази функция се изпълнява за всеки възел **преди** обхождащият да посети което и да е от децата на този възел. Полезна е за: - -- Събиране на информация при обхождане на дървото надолу. -- Вземане на решения *преди* обработката на децата (като решение за тяхното пропускане, вижте [#Оптимизиране на обхождането]). -- Потенциални корекции на възела преди посещение на децата (по-рядко). - -**`leave(Node $node)`:** Тази функция се изпълнява за всеки възел **след** като всички негови деца (и техните цели поддървета) са напълно посетени (както влизане, така и напускане). Това е най-честото място за: - -И двата визитора `enter` и `leave` могат по избор да връщат стойност, за да повлияят на процеса на обхождане. Връщането на `null` (или нищо) продължава обхождането нормално, връщането на инстанция на `Node` замества текущия възел, а връщането на специални константи като `NodeTraverser::RemoveNode` или `NodeTraverser::StopTraversal` модифицира потока, както е обяснено в следващите секции. - - -Как работи обхождането ----------------------- - -`NodeTraverser` вътрешно използва метода `getIterator()`, който трябва да бъде имплементиран от всеки клас `Node` (както беше обсъдено в [Създаване на собствени тагове |custom-tags#Имплементиране на getIterator за подвъзли]). Итерира през децата, получени с помощта на `getIterator()`, рекурсивно извиква `traverse()` върху тях и гарантира, че `enter` и `leave` визиторите се извикват в правилния ред „първо в дълбочина“ за всеки възел в дървото, достъпен чрез итератори. Това отново подчертава защо правилно имплементираният `getIterator()` във вашите собствени тагови възли е абсолютно необходим за правилното функциониране на компилационните проходи. - -Нека напишем прост проход, който брои колко пъти в шаблона е използван тагът `{do}` (представен от `Latte\Essential\Nodes\DoNode`). - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Essential\Nodes\DoNode; - -function countDoTags(TemplateNode $templateNode): void -{ - $count = 0; - (new NodeTraverser)->traverse( - $templateNode, - enter: function (Node $node) use (&$count): void { - if ($node instanceof DoNode) { - $count++; - } - }, - // 'leave' visitor не е необходим за тази задача - ); - - echo "Намерен таг {do} $count пъти.\n"; -} - -$latte = new Latte\Engine; -$ast = $latte->parse($templateSource); -countDoTags($ast); -``` - -В този пример ни беше необходим само visitor `enter`, за да проверим типа на всеки посетен възел. - -След това ще разгледаме как тези визитори действително модифицират AST. - - -Модификация на AST -================== - -Една от основните цели на компилационните проходи е модификацията на абстрактното синтактично дърво. Това позволява мощни трансформации, оптимизации или налагане на правила директно върху структурата на шаблона преди генерирането на PHP код. `NodeTraverser` предоставя няколко начина за постигане на това в рамките на визиторите `enter` и `leave`. - -**Важна забележка:** Модификацията на AST изисква внимание. Неправилните промени – като премахване на основни възли или замяна на възел с несъвместим тип – могат да доведат до грешки по време на генерирането на код или да причинят неочаквано поведение по време на изпълнение на програмата. Винаги тествайте обстойно вашите модификационни проходи. - - -Промяна на свойствата на възлите --------------------------------- - -Най-простият начин за модифициране на дървото е директната промяна на **публичните свойства** на възлите, посетени по време на обхождането. Всички възли съхраняват своите парснати аргументи, съдържание или атрибути в публични свойства. - -**Пример:** Нека създадем проход, който намира всички статични текстови възли (`TextNode`, представляващи обикновен HTML или текст извън Latte тагове) и преобразува тяхното съдържание на главни букви *директно в AST*. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Compiler\Nodes\TextNode; - -function uppercaseStaticText(TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // Можем да използваме 'enter', тъй като TextNode няма деца за обработка - enter: function (Node $node) { - // Този възел статичен текстов блок ли е? - if ($node instanceof TextNode) { - // Да! Директно ще променим неговото публично свойство 'content'. - $node->content = mb_strtoupper(html_entity_decode($node->content)); - } - // Не е необходимо нищо да се връща; промяната е приложена директно. - }, - ); -} -``` - -В този пример visitor `enter` проверява дали текущият `$node` е от тип `TextNode`. Ако е така, директно актуализираме неговото публично свойство `$content` с помощта на `mb_strtoupper()`. Това директно променя съдържанието на статичния текст, съхранен в AST *преди* генерирането на PHP код. Тъй като модифицираме обекта директно, не е необходимо да връщаме нищо от визитора. - -Ефект: Ако шаблонът съдържаше `<p>Hello</p>{= $var }<span>World</span>`, след този проход AST ще представя нещо като: `<p>HELLO</p>{= $var }<span>WORLD</span>`. Това НЕ ВЛИЯЕ на съдържанието на $var. - - -Замяна на възли ---------------- - -По-мощна техника за модификация е пълната замяна на възел с друг. Това се извършва чрез **връщане на нова инстанция на `Node`** от визитора `enter` или `leave`. `NodeTraverser` след това замества оригиналния възел с върнатия в структурата на родителския възел. - -**Пример:** Нека създадем проход, който намира всички употреби на константата `PHP_VERSION` (представена от `ConstantFetchNode`) и ги заменя директно с низов литерал (`StringNode`), съдържащ *действителната* версия на PHP, открита *по време на компилация*. Това е форма на оптимизация по време на компилация. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Compiler\Nodes\Php\Expression\ConstantFetchNode; -use Latte\Compiler\Nodes\Php\Scalar\StringNode; - -function inlinePhpVersion(TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // 'leave' често се използва за замяна, като гарантира, че децата (ако има такива) - // се обработват първо, въпреки че 'enter' също би работил тук. - leave: function (Node $node) { - // Този възел достъп до константа ли е и името на константата 'PHP_VERSION' ли е? - if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') { - // Създаваме нов StringNode, съдържащ текущата версия на PHP - $newNode = new StringNode(PHP_VERSION); - - // Незадължително, но добра практика: копираме информацията за позицията - $newNode->position = $node->position; - - // Връщаме новия StringNode. Traverser ще замени - // оригиналния ConstantFetchNode с този $newNode. - return $newNode; - } - // Ако не върнем Node, оригиналният $node се запазва. - }, - ); -} -``` - -Тук visitor `leave` идентифицира специфичния `ConstantFetchNode` за `PHP_VERSION`. След това създава изцяло нов `StringNode`, съдържащ стойността на константата `PHP_VERSION` *по време на компилация*. Връщайки този `$newNode`, той казва на обхождащия да замени оригиналния `ConstantFetchNode` в AST. - -Ефект: Ако шаблонът съдържаше `{= PHP_VERSION }` и компилацията се изпълнява на PHP 8.2.1, AST след този проход ефективно ще представя `{= '8.2.1' }`. - -**Избор на `enter` срещу `leave` за замяна:** - -- Използвайте `leave`, ако създаването на новия възел зависи от резултатите от обработката на децата на стария възел, или ако просто искате да гарантирате, че децата са посетени преди замяната (често срещана практика). -- Използвайте `enter`, ако искате да замените възел *преди* неговите деца изобщо да бъдат посетени. - - -Премахване на възли -------------------- - -Можете напълно да премахнете възел от AST, като върнете специалната константа `NodeTraverser::RemoveNode` от визитора. - -**Пример:** Нека премахнем всички коментари на шаблона (`{* ... *}`), които са представени от `CommentNode` в AST, генериран от ядрото на Latte (въпреки че обикновено се обработват по-рано, това служи като пример). - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Compiler\Nodes\CommentNode; - -function removeCommentNodes(TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // 'enter' тук е добре, тъй като не се нуждаем от информация за децата, за да премахнем коментара - enter: function (Node $node) { - if ($node instanceof CommentNode) { - // Сигнализираме на обхождащия да премахне този възел от AST - return NodeTraverser::RemoveNode; - } - }, - ); -} -``` - -**Внимание:** Използвайте `RemoveNode` внимателно. Премахването на възел, който съдържа основно съдържание или влияе на структурата (като премахване на съдържателния възел на цикъл), може да доведе до повредени шаблони или невалиден генериран код. Най-безопасно е за възли, които са наистина незадължителни или самостоятелни (като коментари или дебъгващи тагове) или за празни структурни възли (напр. празен `FragmentNode` може да бъде безопасно премахнат в някои контексти чрез проход за почистване). - -Тези три метода - промяна на свойства, замяна на възли и премахване на възли - предоставят основните инструменти за манипулиране на AST в рамките на вашите компилационни проходи. - - -Оптимизиране на обхождането -=========================== - -AST на шаблоните може да бъде доста голям, потенциално съдържащ хиляди възли. Обхождането на всеки отделен възел може да бъде ненужно и да повлияе на производителността на компилацията, ако вашият проход се интересува само от специфични части на дървото. `NodeTraverser` предлага начини за оптимизиране на обхождането: - - -Пропускане на деца ------------------- - -Ако знаете, че щом срещнете определен тип възел, нито един от неговите потомци не може да съдържа възли, които търсите, можете да кажете на обхождащия да пропусне посещението на неговите деца. Това се извършва чрез връщане на константата `NodeTraverser::DontTraverseChildren` от визитора **`enter`**. По този начин пропускате цели клонове при обхождането, което потенциално спестява значително време, особено в шаблони със сложни PHP изрази вътре в тагове. - - -Спиране на обхождането ----------------------- - -Ако вашият проход трябва да намери само *първото* срещане на нещо (специфичен тип възел, изпълнение на условие), можете напълно да спрете целия процес на обхождане, щом го намерите. Това се постига чрез връщане на константата `NodeTraverser::StopTraversal` от визитора `enter` или `leave`. Методът `traverse()` спира да посещава всякакви други възли. Това е изключително ефективно, ако се нуждаете само от първото съвпадение в потенциално много голямо дърво. - - -Полезен помощник `NodeHelpers` -============================== - -Въпреки че `NodeTraverser` предлага фин контрол, Latte също предоставя практичен помощен клас, [api:Latte\Compiler\NodeHelpers], който капсулира `NodeTraverser` за няколко често срещани задачи за търсене и анализ, често изискващи по-малко подготвителен код. - - -find(Node $startNode, callable $filter): array .[method] --------------------------------------------------------- - -Този статичен метод намира **всички** възли в поддървото, започващо от `$startNode` (включително), които отговарят на callback `$filter`. Връща масив от съответстващи възли. - -**Пример:** Намиране на всички възли на променливи (`VariableNode`) в целия шаблон. - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\Php\Expression\VariableNode; -use Latte\Compiler\Nodes\TemplateNode; - -function findAllVariables(TemplateNode $templateNode): array -{ - return NodeHelpers::find( - $templateNode, - fn($node) => $node instanceof VariableNode, - ); -} -``` - - -findFirst(Node $startNode, callable $filter): ?Node .[method] --------------------------------------------------------------- - -Подобно на `find`, но спира обхождането незабавно след намиране на **първия** възел, който отговаря на callback `$filter`. Връща намерения обект `Node` или `null`, ако не е намерен съответстващ възел. Това е по същество практична обвивка около `NodeTraverser::StopTraversal`. - -**Пример:** Намиране на възела `{parameters}` (същото като ръчния пример преди, но по-кратко). - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Essential\Nodes\ParametersNode; - -function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode -{ - return NodeHelpers::findFirst( - $templateNode->head, // Търсене само в главната секция за ефективност - fn($node) => $node instanceof ParametersNode, - ); -} -``` - - -toValue(ExpressionNode $node, bool $constants = false): mixed .[method] ------------------------------------------------------------------------ - -Този статичен метод се опитва да *изчисли стойността* на `ExpressionNode` **по време на компилация** и да върне неговата съответстваща PHP стойност. Работи надеждно само за прости литерални възли (`StringNode`, `IntegerNode`, `FloatNode`, `BooleanNode`, `NullNode`) и инстанции на `ArrayNode`, съдържащи само такива изчислими елементи. - -Ако `$constants` е зададено на `true`, той също ще се опита да разреши `ConstantFetchNode` и `ClassConstantFetchNode` чрез проверка с `defined()` и използване на `constant()`. - -Ако възелът съдържа променливи, извиквания на функции или други динамични елементи, той не може да бъде изчислен по време на компилация и методът ще хвърли `InvalidArgumentException`. - -**Случай на употреба:** Получаване на статичната стойност на аргумент на таг по време на компилация за вземане на решения по време на компилация. - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\Php\ExpressionNode; - -function getStaticStringArgument(ExpressionNode $argumentNode): ?string -{ - try { - $value = NodeHelpers::toValue($argumentNode); - return is_string($value) ? $value : null; - } catch (\InvalidArgumentException $e) { - // Аргументът не беше статичен литерален низ - return null; - } -} -``` - - -toText(?Node $node): ?string .[method] --------------------------------------- - -Този статичен метод е полезен за извличане на обикновено текстово съдържание от прости възли. Работи предимно с: -- `TextNode`: Връща неговото `$content`. -- `FragmentNode`: Конкатенира резултата от `toText()` за всички негови деца. Ако някое дете не може да се преобразува в текст (напр. съдържа `PrintNode`), връща `null`. -- `NopNode`: Връща празен низ. -- Други типове възли: Връща `null`. - -**Случай на употреба:** Получаване на статичното текстово съдържание на стойността на HTML атрибут или прост HTML елемент за анализ по време на компилационен проход. - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\Html\AttributeNode; - -function getStaticAttributeValue(AttributeNode $attr): ?string -{ - // $attr->value обикновено е AreaNode (като FragmentNode или TextNode) - return NodeHelpers::toText($attr->value); -} - -// Пример за използване в проход: -// if ($node instanceof Html\ElementNode && $node->name === 'meta') { -// $nameAttrValue = getStaticAttributeValue($node->getAttributeNode('name')); -// if ($nameAttrValue === 'description') { ... } -// } -``` - -`NodeHelpers` може да опрости вашите компилационни проходи, като предостави готови решения за често срещани задачи за обхождане и анализ на AST. - - -Практически примери -=================== - -Нека приложим концепциите за обхождане и модификация на AST за решаване на някои практически проблеми. Тези примери демонстрират често срещани модели, използвани в компилационните проходи. - - -Автоматично добавяне на `loading="lazy"` към `<img>` ----------------------------------------------------- - -Съвременните браузъри поддържат вградено мързеливо зареждане за изображения с помощта на атрибута `loading="lazy"`. Нека създадем проход, който автоматично добавя този атрибут към всички тагове `<img>`, които все още нямат атрибут `loading`. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes; -use Latte\Compiler\Nodes\Html; - -function addLazyLoading(Nodes\TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // Можем да използваме 'enter', тъй като модифицираме възела директно - // и не зависим от децата за това решение. - enter: function (Node $node) { - // Това HTML елемент с име 'img' ли е? - if ($node instanceof Html\ElementNode && $node->name === 'img') { - // Гарантираме, че възелът на атрибутите съществува - $node->attributes ??= new Nodes\FragmentNode; - - // Проверяваме дали вече съществува атрибут 'loading' (без значение от регистъра) - foreach ($node->attributes->children as $attrNode) { - if ($attrNode instanceof Html\AttributeNode - && $attrNode->name instanceof Nodes\TextNode // Статично име на атрибут - && strtolower($attrNode->name->content) === 'loading' - ) { - return; // Атрибутът 'loading' вече съществува, не правим нищо - } - } - - // Добавяме интервал, ако атрибутите не са празни и последният не е интервал - if ($node->attributes->children) { - $node->attributes->children[] = new Nodes\TextNode(' '); - } - - // Създаваме нов възел на атрибута: loading="lazy" - $node->attributes->children[] = new Html\AttributeNode( - name: new Nodes\TextNode('loading'), - value: new Nodes\TextNode('lazy'), - quote: '"', - ); - // Промяната се прилага директно в обекта, не е необходимо нищо да се връща. - } - }, - ); -} -``` - -Обяснение: -- Visitor `enter` търси възли `Html\ElementNode` с име `img`. -- Итерира през съществуващите атрибути (`$node->attributes->children`) и проверява дали атрибутът `loading` вече присъства. -- Ако не е намерен, създава нов `Html\AttributeNode`, представляващ `loading="lazy"`. - - -Проверка на извиквания на функции ---------------------------------- - -Компилационните проходи са основата на Latte Sandbox. Въпреки че истинският Sandbox е сложен, можем да демонстрираме основния принцип на проверка за забранени извиквания на функции. - -**Цел:** Предотвратяване на използването на потенциално опасната функция `shell_exec` в рамките на изрази в шаблона. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes; -use Latte\Compiler\Nodes\Php; -use Latte\SecurityViolationException; - -function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void -{ - $forbiddenFunctions = ['shell_exec' => true, 'exec' => true]; // Прост списък - - $traverser = new NodeTraverser; - (new NodeTraverser)->traverse( - $templateNode, - enter: function (Node $node) use ($forbiddenFunctions) { - // Това възел на директно извикване на функция ли е? - if ($node instanceof Php\Expression\FunctionCallNode - && $node->name instanceof Php\NameNode - && isset($forbiddenFunctions[strtolower((string) $node->name)]) - ) { - throw new SecurityViolationException( - "Функцията {$node->name}() не е разрешена.", - $node->position, - ); - } - }, - ); -} -``` - -Обяснение: -- Дефинираме списък със забранени имена на функции. -- Visitor `enter` проверява `FunctionCallNode`. -- Ако името на функцията (`$node->name`) е статичен `NameNode`, проверяваме неговото представяне като низ с малки букви спрямо нашия забранен списък. -- Ако е намерена забранена функция, хвърляме `Latte\SecurityViolationException`, която ясно показва нарушение на правилото за сигурност и спира компилацията. - -Тези примери показват как компилационните проходи с използването на `NodeTraverser` могат да бъдат използвани за анализ, автоматични модификации и налагане на ограничения за сигурност чрез директно взаимодействие със структурата на AST на шаблона. - - -Добри практики -============== - -При писане на компилационни проходи имайте предвид тези насоки за създаване на стабилни, поддържаеми и ефективни разширения: - -- **Редът на изпълнение е важен:** Бъдете наясно с реда, в който се изпълняват проходите. Ако вашият проход зависи от структурата на AST, създадена от друг проход (напр. основни проходи на Latte или друг персонализиран проход), или ако други проходи могат да зависят от вашите модификации, използвайте механизма за сортиране, предоставен от `Extension::getPasses()`, за да дефинирате зависимости (`before`/`after`). Вижте документацията за [`Extension::getPasses()` |extending-latte#getPasses] за подробности. -- **Една отговорност:** Стремете се към проходи, които изпълняват една добре дефинирана задача. За сложни трансформации обмислете разделянето на логиката на няколко прохода – може би един за анализ и друг за модификация, базирана на резултатите от анализа. Това подобрява прегледността и тестваемостта. -- **Производителност:** Помнете, че компилационните проходи добавят време към компилацията на шаблона (въпреки че това обикновено се случва само веднъж, докато шаблонът не се промени). Избягвайте изчислително скъпи операции във вашите проходи, ако е възможно. Използвайте оптимизации на обхождането като `NodeTraverser::DontTraverseChildren` и `NodeTraverser::StopTraversal` винаги, когато знаете, че не е необходимо да посещавате определени части от AST. -- **Използвайте `NodeHelpers`:** За често срещани задачи като търсене на специфични възли или статично изчисляване на прости изрази, проверете дали `Latte\Compiler\NodeHelpers` не предлага подходящ метод, преди да пишете собствена логика с `NodeTraverser`. Това може да спести време и да намали количеството подготвителен код. -- **Обработка на грешки:** Ако вашият проход открие грешка или невалидно състояние в AST на шаблона, хвърлете `Latte\CompileException` (или `Latte\SecurityViolationException` за проблеми със сигурността) с ясно съобщение и релевантен обект `Position` (обикновено `$node->position`). Това предоставя полезна обратна връзка на разработчика на шаблона. -- **Идемпотентност (ако е възможно):** В идеалния случай, изпълнението на вашия проход няколко пъти върху същия AST трябва да произведе същия резултат като еднократното му изпълнение. Това не винаги е изпълнимо, но опростява отстраняването на грешки и разсъжденията за взаимодействията на проходите, ако бъде постигнато. Например, уверете се, че вашият модификационен проход проверява дали модификацията вече е приложена, преди да я приложи отново. - -Следвайки тези практики, можете ефективно да използвате компилационните проходи, за да разширите възможностите на Latte по мощен и надежден начин, допринасяйки за по-безопасна, по-оптимизирана или функционално по-богата обработка на шаблони. diff --git a/latte/bg/cookbook/@home.texy b/latte/bg/cookbook/@home.texy deleted file mode 100644 index 019aa6cd32..0000000000 --- a/latte/bg/cookbook/@home.texy +++ /dev/null @@ -1,13 +0,0 @@ -Ръководства и процедури -*********************** - -.[perex] -Примери за кодове и рецепти за изпълнение на често срещани задачи с помощта на Latte. - -- [Процедури за разработчици |/develop] -- [Предаване на променливи между шаблони |passing-variables] -- [Всичко, което някога сте искали да знаете за групирането |grouping] -- [Как да пишем SQL заявки в Latte? |how-to-write-sql-queries-in-latte] -- [Миграция от PHP |migration-from-php] -- [Миграция от Twig |migration-from-twig] -- [Използване на Latte със Slim 4 |slim-framework] diff --git a/latte/bg/cookbook/@meta.texy b/latte/bg/cookbook/@meta.texy deleted file mode 100644 index 64e87d1168..0000000000 --- a/latte/bg/cookbook/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Документация на Latte}} -{{leftbar: /@left-menu}} diff --git a/latte/bg/cookbook/grouping.texy b/latte/bg/cookbook/grouping.texy deleted file mode 100644 index fe6ac48fe5..0000000000 --- a/latte/bg/cookbook/grouping.texy +++ /dev/null @@ -1,251 +0,0 @@ -Всичко, което някога сте искали да знаете за групирането -******************************************************** - -.[perex] -При работа с данни в шаблони често можете да срещнете нуждата от тяхното групиране или специфично показване според определени критерии. Latte за тази цел предлага няколко силни инструмента. - -Филтърът и функцията `|group` позволяват ефективно групиране на данни според зададен критерий, филтърът `|batch` пък улеснява разделянето на данни на предварително зададени партиди, а тагът `{iterateWhile}` предоставя възможност за по-сложно управление на протичането на цикли с условия. Всеки от тези тагове предлага специфични възможности за работа с данни, което ги прави незаменими инструменти за динамично и структурирано показване на информация в Latte шаблони. - - -Филтър и функция `group` .{data-version:3.0.16} -=============================================== - -Представете си таблица в база данни `items` с елементи, разделени на категории: - -| id | categoryId | name -|------------------ -| 1 | 1 | Apple -| 2 | 1 | Banana -| 3 | 2 | PHP -| 4 | 3 | Green -| 5 | 3 | Red -| 6 | 3 | Blue - -Прост списък на всички елементи с помощта на Latte шаблон би изглеждал така: - -```latte -<ul> -{foreach $items as $item} - <li>{$item->name}</li> -{/foreach} -</ul> -``` - -Ако обаче искахме елементите да бъдат подредени в групи според категорията, трябва да ги разделим така, че всяка категория да има свой собствен списък. Резултатът тогава трябва да изглежда по следния начин: - -```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> -``` - -Задачата може лесно и елегантно да се реши с помощта на `|group`. Като параметър посочваме `categoryId`, което означава, че елементите ще се разделят на по-малки масиви според стойността на `$item->categoryId` (ако `$item` беше масив, ще се използва `$item['categoryId']`): - -```latte -{foreach ($items|group: categoryId) as $categoryId => $categoryItems} - <ul> - {foreach $categoryItems as $item} - <li>{$item->name}</li> - {/foreach} - </ul> -{/foreach} -``` - -Филтърът може в Latte да се използва и като функция, което ни дава алтернативен синтаксис: `{foreach group($items, categoryId) ...}`. - -Ако искате да групирате елементи според по-сложни критерии, можете в параметъра на филтъра да използвате функция. Например, групиране на елементи според дължината на името би изглеждало така: - -```latte -{foreach ($items|group: fn($item) => strlen($item->name)) as $items} - ... -{/foreach} -``` - -Важно е да се осъзнае, че `$categoryItems` не е обикновен масив, а обект, който се държи като итератор. За достъп до първия елемент на групата можете да използвате функцията [`first()` |latte:functions#first]. - -Тази гъвкавост в групирането на данни прави `group` изключително полезен инструмент за представяне на данни в шаблони Latte. - - -Вложени цикли -------------- - -Представете си, че имаме база данни с допълнителна колона `subcategoryId`, която дефинира подкатегориите на отделните елементи. Искаме да покажем всяка основна категория в отделен списък `<ul>` и всяка подкатегория в отделен вложен списък `<ol>`: - -```latte -{foreach ($items|group: categoryId) as $categoryItems} - <ul> - {foreach ($categoryItems|group: subcategoryId) as $subcategoryItems} - <ol> - {foreach $subcategoryItems as $item} - <li>{$item->name} - {/foreach} - </ol> - {/foreach} - </ul> -{/foreach} -``` - - -Връзка с Nette Database ------------------------ - -Нека покажем как ефективно да използваме групирането на данни в комбинация с Nette Database. Да предположим, че работим с таблицата `items` от уводния пример, която чрез колоната `categoryId` е свързана с тази таблица `categories`: - -| categoryId | name | -|------------|------------| -| 1 | Fruits | -| 2 | Languages | -| 3 | Colors | - -Данните от таблицата `items` зареждаме с помощта на Nette Database Explorer с командата `$items = $db->table('items')`. По време на итерацията над тези данни имаме възможност да достъпваме не само атрибути като `$item->name` и `$item->categoryId`, но благодарение на връзката с таблицата `categories` също и свързания ред в нея чрез `$item->category`. На тази връзка може да се демонстрира интересно използване: - -```latte -{foreach ($items|group: category) as $category => $categoryItems} - <h1>{$category->name}</h1> - <ul> - {foreach $categoryItems as $item} - <li>{$item->name}</li> - {/foreach} - </ul> -{/foreach} -``` - -В този случай използваме филтъра `|group` за групиране според свързания ред `$item->category`, а не само според колоната `categoryId`. Благодарение на това в променливата ключ имаме директно `ActiveRow` на дадената категория, което ни позволява директно да изписваме нейното име с `{$category->name}`. Това е практичен пример за това как групирането може да изясни шаблоните и да улесни работата с данни. - - -Филтър `|batch` -=============== - -Филтърът позволява да се раздели списък от елементи на групи с предварително определен брой елементи. Този филтър е идеален за ситуации, когато искате да представите данните в няколко по-малки групи, например за по-добра прегледност или визуално подреждане на страницата. - -Представете си, че имаме списък с елементи и искаме да ги покажем в списъци, където всеки съдържа максимум три елемента. Използването на филтъра `|batch` в такъв случай е много практично: - -```latte -<ul> -{foreach ($items|batch: 3) as $batch} - {foreach $batch as $item} - <li>{$item->name}</li> - {/foreach} -{/foreach} -</ul> -``` - -В този пример списъкът `$items` е разделен на по-малки групи, като всяка група (`$batch`) съдържа до три елемента. Всяка група след това се показва в отделен `<ul>` списък. - -Ако последната група не съдържа достатъчно елементи за достигане на желания брой, вторият параметър на филтъра позволява да се дефинира с какво ще бъде допълнена тази група. Това е идеално за естетическо подравняване на елементите там, където непълният ред би могъл да изглежда неподреден. - -```latte -{foreach ($items|batch: 3, '—') as $batch} - ... -{/foreach} -``` - - -Таг `{iterateWhile}` -==================== - -Същите задачи, които решавахме с филтъра `|group`, ще покажем с използването на тага `{iterateWhile}`. Основната разлика между двата подхода е в това, че `group` първо обработва и групира всички входни данни, докато `{iterateWhile}` управлява протичането на цикли с условия, така че итерацията протича постепенно. - -Първо ще рендираме таблицата с категориите с помощта на iterateWhile: - -```latte -{foreach $items as $item} - <ul> - {iterateWhile} - <li>{$item->name}</li> - {/iterateWhile $item->categoryId === $iterator->nextValue->categoryId} - </ul> -{/foreach} -``` - -Докато `{foreach}` обозначава външната част на цикъла, т.е. рендирането на списъци за всяка категория, тагът `{iterateWhile}` обозначава вътрешната част, т.е. отделните елементи. Условието в крайния таг казва, че повторението ще продължи дотогава, докато текущият и следващият елемент принадлежат към същата категория (`$iterator->nextValue` е [следващият елемент |/tags#iterator]). - -Ако условието беше изпълнено винаги, тогава във вътрешния цикъл ще се рендират всички елементи: - -```latte -{foreach $items as $item} - <ul> - {iterateWhile} - <li>{$item->name} - {/iterateWhile true} - </ul> -{/foreach} -``` - -Резултатът ще изглежда така: - -```latte -<ul> - <li>Apple</li> - <li>Banana</li> - <li>PHP</li> - <li>Green</li> - <li>Red</li> - <li>Blue</li> -</ul> -``` - -За какво е полезно такова използване на iterateWhile? Когато таблицата е празна и не съдържа никакви елементи, няма да се изпише празно `<ul></ul>`. - -Ако посочим условие в отварящия таг `{iterateWhile}`, тогава поведението се променя: условието (и преходът към следващия елемент) се изпълнява още в началото на вътрешния цикъл, а не в края. Тоест, докато в `{iterateWhile}` без условие се влиза винаги, в `{iterateWhile $cond}` само при изпълнение на условието `$cond`. И същевременно с това в `$item` се записва следващият елемент. - -Което е полезно например в ситуация, когато искаме първия елемент във всяка категория да рендираме по различен начин, например така: - -```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> -``` - -Ще променим оригиналния код така, че първо да рендираме първия елемент и след това във вътрешния цикъл `{iterateWhile}` да рендираме другите елементи от същата категория: - -```latte -{foreach $items as $item} - <h1>{$item->name}</h1> - <ul> - {iterateWhile $item->categoryId === $iterator->nextValue->categoryId} - <li>{$item->name}</li> - {/iterateWhile} - </ul> -{/foreach} -``` - -В рамките на един цикъл можем да създаваме повече вътрешни цикли и дори да ги влагаме. Така биха могли да се групират например подкатегории и т.н. - -Да предположим, че в таблицата има още една колона `subcategoryId` и освен това, че всяка категория ще бъде в отделен `<ul>`, всяка подкатегория в отделен `<ol>`: - -```latte -{foreach $items as $item} - <ul> - {iterateWhile} - <ol> - {iterateWhile} - <li>{$item->name} - {/iterateWhile $item->subcategoryId === $iterator->nextValue->subcategoryId} - </ol> - {/iterateWhile $item->categoryId === $iterator->nextValue->categoryId} - </ul> -{/foreach} -``` diff --git a/latte/bg/cookbook/how-to-write-sql-queries-in-latte.texy b/latte/bg/cookbook/how-to-write-sql-queries-in-latte.texy deleted file mode 100644 index b6a002275e..0000000000 --- a/latte/bg/cookbook/how-to-write-sql-queries-in-latte.texy +++ /dev/null @@ -1,40 +0,0 @@ -Как да пишем SQL заявки в Latte? -******************************** - -.[perex] -Latte може да бъде полезен и за генериране на наистина сложни SQL заявки. - -Ако създаването на SQL заявка съдържа редица условия и променливи, може да бъде наистина по-прегледно да я напишете в Latte. Много прост пример: - -```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} -``` - -С помощта на `$latte->setContentType()` казваме на Latte да третира съдържанието като обикновен текст (а не като HTML) и след това подготвяме функция за екраниране, която ще екранира низовете директно с драйвера на базата данни: - -```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'), -}); -``` - -Използването би изглеждало така: - -```php -$sql = $latte->renderToString('query.sql.latte', ['country' => $country]); -$result = $db->query($sql); -``` - -*Посоченият пример изисква Latte v3.0.5 или по-нова версия.* diff --git a/latte/bg/cookbook/migration-from-php.texy b/latte/bg/cookbook/migration-from-php.texy deleted file mode 100644 index 73be04cf53..0000000000 --- a/latte/bg/cookbook/migration-from-php.texy +++ /dev/null @@ -1,70 +0,0 @@ -Миграция от PHP към Latte -************************* - -.[perex] -Преобразувате стар проект, написан на чист PHP, към Latte? Имаме за вас инструмент, който ще ви улесни миграцията. [Изпробвайте го онлайн |https://fiddle.nette.org/php2latte/]. - -Можете да изтеглите инструмента от [GitHub|https://github.com/nette/latte-tools] или да го инсталирате с помощта на Composer: - -```shell -composer create-project latte/tools -``` - -Преобразувателят не използва прости замени с помощта на регулярни изрази, а напротив, използва директно PHP парсера, така че може да се справи с всякакъв сложен синтаксис. - -За преобразуване от PHP към Latte служи скриптът `php-to-latte.php`: - -```shell -php php-to-latte.php input.php [output.latte] -``` - - -Пример ------- - -Входният файл може да изглежда например така (това е част от кода на форума 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> -``` - -Ще генерира този шаблон: - -```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> -``` diff --git a/latte/bg/cookbook/migration-from-twig.texy b/latte/bg/cookbook/migration-from-twig.texy deleted file mode 100644 index 04e356b464..0000000000 --- a/latte/bg/cookbook/migration-from-twig.texy +++ /dev/null @@ -1,79 +0,0 @@ -Миграция от Twig към Latte -************************** - -.[perex] -Преобразувате проект, написан на Twig, към по-модерния Latte? Имаме за вас инструмент, който ще ви улесни миграцията. [Изпробвайте го онлайн |https://fiddle.nette.org/twig2latte/]. - -Можете да изтеглите инструмента от [GitHub|https://github.com/nette/latte-tools] или да го инсталирате с помощта на Composer: - -```shell -composer create-project latte/tools -``` - -Преобразувателят не използва прости замени с помощта на регулярни изрази, а напротив, използва директно Twig парсера, така че може да се справи с всякакъв сложен синтаксис. - -За преобразуване от Twig към Latte служи скриптът `twig-to-latte.php`: - -```shell -php twig-to-latte.php input.twig.html [output.latte] -``` - - -Конверсия ---------- - -Преобразуването предполага ръчна корекция на резултата, тъй като конверсията не може да се извърши еднозначно. Twig използва точков синтаксис, където `{{ a.b }}` може да означава `$a->b`, `$a['b']` или `$a->getB()`, което не може да се разграничи при компилация. Преобразувателят затова преобразува всичко на `$a->b`. - -Някои функции, филтри или тагове нямат аналог в Latte, или могат да се държат леко по-различно. - - -Пример ------- - -Входният файл може да изглежда например така: - -```twig -{% use "blocks.twig" %} -<!DOCTYPE html> -<html> - <head> - <title>{{ block("title") }} - - -

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

    - - - -``` - -След конверсията към Latte получаваме този шаблон: - -```latte -{import 'blocks.latte'} - - - - {include title} - - -

    {block title}My Web{/block}

    - - - -``` diff --git a/latte/bg/cookbook/passing-variables.texy b/latte/bg/cookbook/passing-variables.texy deleted file mode 100644 index 4e040e9217..0000000000 --- a/latte/bg/cookbook/passing-variables.texy +++ /dev/null @@ -1,158 +0,0 @@ -Предаване на променливи между шаблони -************************************* - -Това ръководство ще ви обясни как се предават променливи между шаблони в Latte с помощта на различни тагове като `{include}`, `{import}`, `{embed}`, `{layout}`, `{sandbox}` и други. Ще научите също как да работите с променливи в тага `{block}` и `{define}`, и за какво служи тагът `{parameters}`. - - -Типове променливи ------------------ -Променливите в Latte можем да разделим на три категории според това как и къде са дефинирани: - -**Входни променливи** са тези, които се предават на шаблона отвън, например от PHP скрипт или с помощта на таг като `{include}`. - -```php -$latte->render('template.latte', ['userName' => 'Jan', 'userAge' => 30]); -``` - -**Околни променливи** са променливи, съществуващи на мястото на определен таг. Включват всички входни променливи и други променливи, създадени с помощта на тагове като `{var}`, `{default}` или в рамките на цикъл `{foreach}`. - -```latte -{foreach $users as $user} - {include 'userBox.latte', user: $user} -{/foreach} -``` - -**Експлицитни променливи** са тези, които са директно специфицирани вътре в тага и се изпращат към целевия шаблон. - -```latte -{include 'userBox.latte', name: $user->name, age: $user->age} -``` - - -`{block}` ---------- -Тагът `{block}` се използва за дефиниране на повторно използваеми блокове код, които могат да бъдат персонализирани или разширени в наследяващи шаблони. Околните променливи, дефинирани преди блока, са достъпни вътре в блока, но всякакви промени на променливите се отразяват само в рамките на този блок. - -```latte -{var $foo = 'оригинален'} -{block example} - {var $foo = 'променен'} -{/block} - -{$foo} // извежда: оригинален -``` - - -`{define}` ----------- -Тагът `{define}` служи за създаване на блокове, които се рендират едва след тяхното извикване с `{include}`. Променливите, достъпни вътре в тези блокове, зависят от това дали в дефиницията са посочени параметри. Ако да, достъп имат само до тези параметри. Ако не, достъп имат до всички входни променливи на шаблона, в който са дефинирани блоковете. - -```latte -{define hello} - {* има достъп до всички входни променливи на шаблона *} -{/define} - -{define hello $name} - {* има достъп само до параметъра $name *} -{/define} -``` - - -`{parameters}` --------------- -Тагът `{parameters}` служи за експлицитно деклариране на очакваните входни променливи в началото на шаблона. По този начин може лесно да се документират очакваните променливи и техните типове данни. Също така е възможно да се дефинират стойности по подразбиране. - -```latte -{parameters int $age, string $name = 'неизвестно'} -

    Възраст: {$age}, Име: {$name}

    -``` - - -`{include file}` ----------------- -Тагът `{include file}` служи за вмъкване на цял шаблон. На този шаблон се предават както входните променливи на шаблона, в който е използван тагът, така и променливите, експлицитно дефинирани в него. Целевият шаблон обаче може да ограничи обхвата с помощта на `{parameters}`. - -```latte -{include 'profile.latte', userId: $user->id} -``` - - -`{include block}` ------------------ -Когато вмъквате блок, дефиниран в същия шаблон, към него се предават всички околни и експлицитно дефинирани променливи: - -```latte -{define blockName} -

    Име: {$name}, Възраст: {$age}

    -{/define} - -{var $name = 'Jan', $age = 30} -{include blockName} -``` - -В този пример променливите `$name` и `$age` се предават към блока `blockName`. По същия начин се държи и `{include parent}`. - -При вмъкване на блок от друг шаблон се предават само входните променливи и експлицитно дефинираните. Околните променливи не са автоматично достъпни. - -```latte -{include blockInOtherTemplate, name: $name, age: $age} -``` - - -`{layout}` или `{extends}` --------------------------- -Тези тагове дефинират лейаут, към който се предават входните променливи на дъщерния шаблон и по-нататък променливите, създадени в кода преди блоковете: - -```latte -{layout 'layout.latte'} -{var $seo = 'index, follow'} -``` - -Шаблон `layout.latte`: - -```latte - - - -``` - - -`{embed}` ---------- -Тагът `{embed}` е подобен на тага `{include}`, но позволява вмъкване на блокове в шаблона. За разлика от `{include}`, се предават само експлицитно декларираните променливи: - -```latte -{embed 'menu.latte', items: $menuItems} -{/embed} -``` - -В този пример шаблонът `menu.latte` има достъп само до променливата `$items`. - -Напротив, в блоковете вътре в `{embed}` има достъп до всички околни променливи: - -```latte -{var $name = 'Jan'} -{embed 'menu.latte', items: $menuItems} - {block foo} - {$name} - {/block} -{/embed} -``` - - -`{import}` ----------- -Тагът `{import}` се използва за зареждане на блокове от други шаблони. Пренасят се както входните, така и експлицитно декларираните променливи към импортираните блокове. - -```latte -{import 'buttons.latte'} -``` - - -`{sandbox}` ------------ -Тагът `{sandbox}` изолира шаблона за безопасна обработка. Променливите се предават изключително експлицитно. - -```latte -{sandbox 'secure.latte', data: $secureData} -``` diff --git a/latte/bg/cookbook/slim-framework.texy b/latte/bg/cookbook/slim-framework.texy deleted file mode 100644 index 3f72e4bcb4..0000000000 --- a/latte/bg/cookbook/slim-framework.texy +++ /dev/null @@ -1,157 +0,0 @@ -Използване на Latte със Slim 4 -****************************** - -.[perex] -Тази статия, чийто автор е "Daniel Opitz":https://odan.github.io/2022/04/06/slim4-latte.html, описва използването на Latte със Slim Framework. - -Първо "инсталирайте Slim Framework":https://odan.github.io/2019/11/05/slim4-tutorial.html и след това Latte с помощта на Composer: - -```shell -composer require latte/latte -``` - - -Конфигурация ------------- - -В коренната директория на проекта създайте нова директория `templates`. Всички шаблони ще бъдат поставени в нея по-късно. - -В файла `config/defaults.php` добавете нов конфигурационен ключ `template`: - -```php -$settings['template'] = __DIR__ . '/../templates'; -``` - -Latte компилира шаблоните в нативен PHP код и ги съхранява в кеш памет на диска. Те са толкова бързи, колкото ако бяха написани на нативен PHP език. - -В файла `config/defaults.php` добавете нов конфигурационен ключ `template_temp`: Уверете се, че директорията `{project}/tmp/templates` съществува и има права за четене и запис. - -```php -$settings['template_temp'] = __DIR__ . '/../tmp/templates'; -``` - -Latte автоматично регенерира кеша при всяка промяна на шаблона, което може да бъде изключено в продукционна среда, за да се спести малко производителност: - -```php -// в продукционна среда променете на false -$settings['template_auto_refresh'] = true; -``` - -След това добавете дефиниция на DI контейнера за класа `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; - }, -]; -``` - -Самото рендиране на шаблона Latte технически би работило, но трябва също да осигурим, че работи с обекта response PSR-7. - -За тази цел ще създадем специален клас `TemplateRenderer`, който ще свърши тази работа вместо нас. - -След това създайте файл `src/Renderer/TemplateRenderer.php` и копирайте/поставете този код: - -```php -engine->renderToString($template, $data); - $response->getBody()->write($string); - - return $response; - } -} -``` - - -Използване ----------- - -Вместо директно да използваме обекта Latte Engine, ще използваме за рендиране на шаблона в обект, съвместим с PSR-7, обекта `TemplateRenderer`. - -Типичен клас за обработка на действие може да изглежда така: Рендира шаблон с име `home.latte`: - -```php - ['one', 'two', 'three'], - ]; - - return $this->renderer->template($response, 'home.latte', $viewData); - } -} -``` - -За да работи това, създайте файл на шаблона в `templates/home.latte` с това съдържание: - -```latte -
      - {foreach $items as $item} -
    • {$item|capitalize}
    • - {/foreach} -
    -``` - -Ако всичко е правилно конфигурирано, трябва да се покаже следният изход: - -```latte -One -Two -Three -``` - -{{priority: -1}} diff --git a/latte/bg/custom-filters.texy b/latte/bg/custom-filters.texy deleted file mode 100644 index db7041068c..0000000000 --- a/latte/bg/custom-filters.texy +++ /dev/null @@ -1,231 +0,0 @@ -Създаване на персонализирани филтри -*********************************** - -.[perex] -Филтрите са мощни инструменти за форматиране и промяна на данни директно в шаблоните на Latte. Те предлагат чист синтаксис с помощта на символа за тръба (`|`) за трансформиране на променливи или резултати от изрази в желания изходен формат. - - -Какво са филтрите? -================== - -Филтрите в Latte по същество са **PHP функции, проектирани специално за трансформиране на входна стойност в изходна стойност**. Те се прилагат с помощта на запис с тръба (`|`) вътре в изразите на шаблона (`{...}`). - -**Удобство:** Филтрите ви позволяват да капсулирате често срещани задачи за форматиране (като форматиране на дати, промяна на регистъра на буквите, съкращаване) или манипулиране на данни в повторно използваеми единици. Вместо да повтаряте сложен PHP код във вашите шаблони, можете просто да приложите филтър: -```latte -{* Вместо сложен PHP за съкращаване: *} -{$article->text|truncate:100} - -{* Вместо код за форматиране на дати: *} -{$event->startTime|date:'Y-m-d H:i'} - -{* Прилагане на множество трансформации: *} -{$product->name|lower|capitalize} -``` - -**Четливост:** Използването на филтри прави шаблоните по-прегледни и по-фокусирани върху презентацията, тъй като трансформационната логика се премества в дефиницията на филтъра. - -**Контекстна чувствителност:** Ключово предимство на филтрите в Latte е тяхната способност да бъдат [контекстно чувствителни |#Контекстни филтри]. Това означава, че филтърът може да разпознае типа на съдържанието, с което работи (HTML, JavaScript, обикновен текст и т.н.), и да приложи съответната логика или екраниране, което е от съществено значение за сигурността и коректността, особено при генериране на HTML. - -**Интеграция с логиката на приложението:** Подобно на персонализираните функции, PHP callable зад филтъра може да бъде затваряне (closure), статичен метод или метод на инстанция. Това позволява на филтрите да достъпват услуги или данни на приложението, ако е необходимо, въпреки че основната им цел остава *трансформация на входната стойност*. - -Latte по подразбиране предоставя богат набор от [стандартни филтри |filters]. Персонализираните филтри ви позволяват да разширите този набор с форматиране и трансформации, специфични за вашия проект. - -Ако трябва да извършвате логика, базирана на *множество* входове или нямате основна стойност за трансформиране, вероятно е по-подходящо да използвате [персонализирана функция |custom-functions]. Ако трябва да генерирате сложен маркъп или да контролирате потока на шаблона, обмислете [персонализиран таг |custom-tags]. - - -Създаване и регистриране на филтри -================================== - -Има няколко начина за дефиниране и регистриране на персонализирани филтри в Latte. - - -Директна регистрация чрез `addFilter()` ---------------------------------------- - -Най-простият начин за добавяне на филтър е използването на метода `addFilter()` директно върху обекта `Latte\Engine`. Посочвате името на филтъра (както ще бъде използван в шаблона) и съответния PHP callable. - -```php -$latte = new Latte\Engine; - -// Прост филтър без аргументи -$latte->addFilter('initial', fn(string $s): string => mb_substr($s, 0, 1) . '.'); - -// Филтър с незадължителен аргумент -$latte->addFilter('shortify', function (string $s, int $len = 10): string { - return mb_substr($s, 0, $len); -}); - -// Филтър, обработващ масив -$latte->addFilter('sum', fn(array $numbers): int|float => array_sum($numbers)); -``` - -**Използване в шаблона:** - -```latte -{$name|initial} {* Изписва 'J.' ако $name е 'John' *} -{$description|shortify} {* Използва дължина по подразбиране 10 *} -{$description|shortify:50} {* Използва дължина 50 *} -{$prices|sum} {* Изписва сумата на елементите в масива $prices *} -``` - -**Предаване на аргументи:** - -Стойността отляво на тръбата (`|`) винаги се предава като *първи* аргумент на функцията на филтъра. Всички параметри, посочени след двоеточието (`:`) в шаблона, се предават като следващи аргументи. - -```latte -{$text|shortify:30} -// Извиква PHP функцията shortify($text, 30) -``` - - -Регистрация чрез разширение ---------------------------- - -За по-добра организация, особено при създаване на повторно използваеми набори от филтри или тяхното споделяне като пакети, препоръчителният начин е да ги регистрирате в рамките на [разширение на Latte |extending-latte#Latte Extension]: - -```php -namespace App\Latte; - -use Latte\Extension; - -class MyLatteExtension extends Extension -{ - public function getFilters(): array - { - return [ - 'initial' => $this->initial(...), - 'shortify' => $this->shortify(...), - ]; - } - - public function initial(string $s): string - { - return mb_substr($s, 0, 1) . '.'; - } - - public function shortify(string $s, int $len = 10): string - { - return mb_substr($s, 0, $len); - } -} - -// Регистрация -$latte = new Latte\Engine; -$latte->addExtension(new App\Latte\MyLatteExtension); -``` - -Този подход поддържа логиката на вашия филтър капсулирана и регистрацията проста. - - -Използване на зареждач на филтри --------------------------------- - -Latte позволява да се регистрира зареждач на филтри с помощта на `addFilterLoader()`. Това е единствено callable, което Latte ще поиска за всяко непознато име на филтър по време на компилация. Зареждачът връща PHP callable на филтъра или `null`. - -```php -$latte = new Latte\Engine; - -// Зареждачът може динамично да създава/получава callable филтри -$latte->addFilterLoader(function (string $name): ?callable { - if ($name === 'myLazyFilter') { - // Представете си тук тежка инициализация... - $service = get_some_expensive_service(); - return fn($value) => $service->process($value); - } - return null; -}); -``` - -Този метод беше първоначално предназначен за мързеливо зареждане на филтри с много **тежка инициализация**. Въпреки това, съвременните практики за вмъкване на зависимости (dependency injection) обикновено се справят с мързеливите услуги по-ефективно. - -Зареждачите на филтри добавят сложност и като цяло не се препоръчват в полза на директната регистрация с `addFilter()` или в рамките на разширение с `getFilters()`. Използвайте зареждачи само ако имате сериозна, специфична причина, свързана с проблеми с производителността при инициализацията на филтри, които не могат да бъдат решени по друг начин. - - -Филтри, използващи клас с атрибути ----------------------------------- - -Друг елегантен начин за дефиниране на филтри е използването на методи във вашия [клас на параметри на шаблона |develop#Параметри като клас]. Достатъчно е да добавите атрибут `#[Latte\Attributes\TemplateFilter]` към метода. - -```php -use Latte\Attributes\TemplateFilter; - -class TemplateParameters -{ - public function __construct( - public string $description, - // други параметри... - ) {} - - #[TemplateFilter] - public function shortify(string $s, int $len = 10): string - { - return mb_substr($s, 0, $len); - } -} - -// Предаване на обекта в шаблона -$params = new TemplateParameters(description: '...'); -$latte->render('template.latte', $params); -``` - -Latte автоматично разпознава и регистрира методи, маркирани с този атрибут, когато обектът `TemplateParameters` е предаден в шаблона. Името на филтъра в шаблона ще бъде същото като името на метода (`shortify` в този случай). - -```latte -{* Използване на филтър, дефиниран в класа на параметрите *} -{$description|shortify:50} -``` - - -Контекстни филтри -================= - -Понякога филтърът се нуждае от повече информация отколкото само входната стойност. Може да се наложи да знае **типа на съдържанието** на низа, с който работи (напр. HTML, JavaScript, обикновен текст) или дори да го промени. Това е ситуация за контекстни филтри. - -Контекстният филтър се дефинира по същия начин като обикновен филтър, но неговият **първи параметър трябва да бъде** типово означен като `Latte\Runtime\FilterInfo`. Latte автоматично разпознава този подпис и при извикване на филтъра предава обект `FilterInfo`. Следващите параметри получават аргументите на филтъра както обикновено. - -```php -use Latte\Runtime\FilterInfo; -use Latte\ContentType; - -$latte->addFilter('money', function (FilterInfo $info, float $amount): string { - // 1. Проверете входния тип на съдържанието (незадължително, но препоръчително) - // Разрешете null (променлив вход) или обикновен текст. Отхвърлете, ако се прилага върху HTML и др. - if (!in_array($info->contentType, [null, ContentType::Text], true)) { - $actualType = $info->contentType ?? 'mixed'; - throw new \RuntimeException( - "Филтърът |money е използван в несъвместим тип съдържание $actualType. Очакван текст или null." - ); - } - - // 2. Извършете трансформацията - $formatted = number_format($amount, 2, '.', ',') . ' EUR'; - $htmlOutput = '' . htmlspecialchars($formatted) . ''; // Гарантирайте правилно екраниране! - - // 3. Декларирайте изходния тип на съдържанието - $info->contentType = ContentType::Html; - - // 4. Върнете резултата - return $htmlOutput; -}); -``` - -`$info->contentType` е низова константа от `Latte\ContentType` (напр. `ContentType::Html`, `ContentType::Text`, `ContentType::JavaScript` и др.) или `null`, ако филтърът се прилага върху променлива (`{$var|filter}`). Можете да **четете** тази стойност, за да проверите входния контекст, и да **записвате** в нея, за да декларирате типа на изходния контекст. - -Настройвайки типа на съдържанието на HTML, съобщавате на Latte, че низът, върнат от вашия филтър, е безопасен HTML. Latte тогава **няма** да приложи върху този резултат своето подразбиращо се автоматично екраниране. Това е от съществено значение, ако вашият филтър генерира HTML маркъп. - -.[warning] -Ако вашият филтър генерира HTML, **вие сте отговорни за правилното екраниране на всякакви входни данни**, използвани в този HTML (както в случая с извикването на `htmlspecialchars($formatted)` по-горе). Пропускането може да създаде XSS уязвимости. Ако вашият филтър връща само обикновен текст, не е необходимо да задавате `$info->contentType`. - - -Филтри върху блокове --------------------- - -Всички филтри, приложени върху [блокове |tags#block], *трябва* да бъдат контекстни. Това е така, защото съдържанието на блока има дефиниран тип на съдържанието (обикновено HTML), за който филтърът трябва да е наясно. - -```latte -{block heading|money}1000{/block} -{* Филтърът 'money' ще получи '1000' като втори аргумент - а $info->contentType ще бъде ContentType::Html *} -``` - -Контекстните филтри предоставят силен контрол върху това как данните се обработват въз основа на техния контекст, позволяват напреднали функции и гарантират правилно поведение на екранирането, особено при генериране на HTML съдържание. diff --git a/latte/bg/custom-functions.texy b/latte/bg/custom-functions.texy deleted file mode 100644 index 4f958508db..0000000000 --- a/latte/bg/custom-functions.texy +++ /dev/null @@ -1,144 +0,0 @@ -Създаване на персонализирани функции -************************************ - -.[perex] -Лесно добавяйте персонализирани помощни функции към шаблоните на Latte. Извиквайте PHP логика директно в изразите за изчисления, достъп до услуги или генериране на динамично съдържание, което поддържа вашите шаблони чисти и мощни. - - -Какво са функциите? -=================== - -Функциите в Latte ви позволяват да разширите набора от функции, които могат да бъдат извиквани в рамките на изрази в шаблоните (`{...}`). Можете да си ги представите като **персонализирани PHP функции, достъпни само вътре във вашите Latte шаблони**. Това носи няколко предимства: - -**Удобство:** Можете да дефинирате помощна логика (като изчисления, форматиране или достъп до данни на приложението) и да я извиквате с помощта на прост, познат синтаксис на функции директно в шаблона, точно както бихте извикали `strlen()` или `date()` в PHP. - -```latte -{var $userInitials = initials($userName)} {* напр. 'J. D.' *} - -{if hasPermission('article', 'edit')} - Редактиране -{/if} -``` - -**Без замърсяване на глобалното именно пространство:** За разлика от дефинирането на истинска глобална функция в PHP, функциите на Latte съществуват само в контекста на рендиране на шаблона. Не е необходимо да натоварвате глобалното именно пространство на PHP с помощници, които са специфични само за шаблоните. - -**Интеграция с логиката на приложението:** PHP callable обектът, стоящ зад функцията на Latte, може да бъде всичко – анонимна функция, статичен метод или метод на инстанция. Това означава, че вашите функции в шаблоните могат лесно да достъпват услуги на приложението, бази данни, конфигурация или всякаква друга необходима логика чрез улавяне на променливи (в случай на анонимни функции) или с помощта на dependency injection (в случай на обекти). Горният пример `hasPermission` ясно демонстрира това, като вероятно извиква на заден план услуга за авторизация. - -**Предефиниране на вградени функции (по избор):** Можете дори да дефинирате функция на Latte със същото име като вградена PHP функция. В шаблона ще бъде извикана вашата собствена версия вместо оригиналната функция. Това може да бъде полезно за предоставяне на поведение, специфично за шаблона, или за осигуряване на последователна обработка (напр. гарантиране, че `strlen` винаги ще бъде многобайтово безопасна). Използвайте тази функция внимателно, за да избегнете недоразумения. - -По подразбиране Latte позволява извикването на *всички* вградени PHP функции (ако не са ограничени от [Sandbox |sandbox]). Персонализираните функции разширяват тази вградена библиотека със специфичните нужди на вашия проект. - -Ако само трансформирате единична стойност, може да е по-подходящо да използвате [персонализиран филтър |custom-filters]. - - -Създаване и регистриране на функции -=================================== - -Подобно на филтрите, има няколко начина за дефиниране и регистриране на персонализирани функции. - - -Директна регистрация с `addFunction()` --------------------------------------- - -Най-простият метод е използването на `addFunction()` върху обекта `Latte\Engine`. Посочвате името на функцията (както ще се показва в шаблона) и съответния PHP callable обект. - -```php -$latte = new Latte\Engine; - -// Проста помощна функция -$latte->addFunction('initials', function (string $name): string { - preg_match_all('#\b\w#u', $name, $m); - return implode('. ', $m[0]) . '.'; -}); -``` - -**Използване в шаблона:** - -```latte -{var $userInitials = initials($userName)} -``` - -Аргументите на функцията в шаблона се предават директно на PHP callable обекта в същия ред. PHP функционалности като типови подсказки, стойности по подразбиране и вариативни параметри (`...`) работят според очакванията. - - -Регистрация чрез разширение ---------------------------- - -За по-добра организация и повторна използваемост, регистрирайте функции в рамките на [Latte разширение |extending-latte#Latte Extension]. Този подход е препоръчителен за по-сложни приложения или споделени библиотеки. - -```php -namespace App\Latte; - -use Latte\Extension; -use Nette\Security\Authorizator; - -class MyLatteExtension extends Extension -{ - public function __construct( - // Предполагаме, че услугата Authorizator се инжектира - private Authorizator $authorizator, - ) { - } - - public function getFunctions(): array - { - // Регистрация на методи като Latte функции - return [ - 'hasPermission' => $this->hasPermission(...), - ]; - } - - public function hasPermission(string $resource, string $action): bool - { - return $this->authorizator->isAllowed($resource, $action); - } -} - -// Регистрация (предполагаме, че $container съдържа DI контейнер) -$extension = $container->getByType(App\Latte\MyLatteExtension::class); -$latte = new Latte\Engine; -$latte->addExtension($extension); -``` - -Този подход ясно показва как функциите, дефинирани в Latte, могат да бъдат подкрепени от методи на обекти, които могат да имат свои собствени зависимости, управлявани от контейнера за dependency injection на вашето приложение или фабрика. Това поддържа логиката на вашите шаблони свързана с ядрото на приложението, като същевременно запазва ясна организация. - - -Функции, използващи клас с атрибути ------------------------------------ - -Подобно на филтрите, функциите могат да бъдат дефинирани като методи във вашия [клас на параметри на шаблона |develop#Параметри като клас] с помощта на атрибута `#[Latte\Attributes\TemplateFunction]`. - -```php -use Latte\Attributes\TemplateFunction; - -class TemplateParameters -{ - public function __construct( - public string $userName, - // други параметри... - ) {} - - // Този метод ще бъде достъпен като {initials(...)} в шаблона - #[TemplateFunction] - public function initials(string $name): string - { - preg_match_all('#\b\w#u', $name, $m); - return implode('. ', $m[0]) . '.'; - } -} - -// Предаване на обекта в шаблона -$params = new TemplateParameters(userName: 'John Doe', /* ... */); -$latte->render('template.latte', $params); -``` - -Latte автоматично открива и регистрира методи, маркирани с този атрибут, когато обектът на параметрите е предаден в шаблона. Името на функцията в шаблона съответства на името на метода. - -```latte -{* Използване на функция, дефинирана в класа на параметрите *} -{var $inits = initials($userName)} -``` - -**Контекстни функции?** - -За разлика от филтрите, не съществува директна концепция за "контекстни функции", които биха получили обект, подобен на `FilterInfo`. Функциите работят в рамките на изрази и обикновено не се нуждаят от директен достъп до контекста на рендиране или информация за типа на съдържанието по същия начин, както филтрите, приложени върху блокове. diff --git a/latte/bg/custom-tags.texy b/latte/bg/custom-tags.texy deleted file mode 100644 index 46bbf08171..0000000000 --- a/latte/bg/custom-tags.texy +++ /dev/null @@ -1,1135 +0,0 @@ -Създаване на персонализирани тагове -*********************************** - -.[perex] -Тази страница предоставя изчерпателно ръководство за създаване на персонализирани тагове в Latte. Ще обсъдим всичко - от прости тагове до по-сложни сценарии с вложено съдържание и специфични нужди от парсване, като надграждаме разбирането ви за това как Latte компилира шаблони. - -Персонализираните тагове осигуряват най-високо ниво на контрол върху синтаксиса на шаблона и логиката на рендиране, но са и най-сложната точка за разширяване. Преди да решите да създадете персонализиран таг, винаги обмисляйте дали [не съществува по-просто решение |extending-latte#Начини за разширяване на Latte] или дали вече не съществува подходящ таг в [стандартния набор |tags]. Използвайте персонализирани тагове само когато по-простите алтернативи не са достатъчни за вашите нужди. - - -Разбиране на процеса на компилация -================================== - -За ефективно създаване на персонализирани тагове е полезно да се обясни как Latte обработва шаблони. Разбирането на този процес изяснява защо таговете са структурирани по този начин и как се вписват в по-широкия контекст. - -Компилацията на шаблон в Latte, опростено, включва следните ключови стъпки: - -1. **Лексикален анализ:** Лексерът чете изходния код на шаблона (файл `.latte`) и го разделя на последователност от малки, отделни части, наречени **токени** (напр. `{`, `foreach`, `$variable`, `}`, HTML текст и т.н.). -2. **Парсване:** Парсерът взема този поток от токени и изгражда от него смислена дървовидна структура, представяща логиката и съдържанието на шаблона. Това дърво се нарича **абстрактно синтактично дърво (AST)**. -3. **Компилационни проходи:** Преди генерирането на PHP код, Latte изпълнява [компилационни проходи |compiler passes]. Това са функции, които обхождат цялото AST и могат да го модифицират или да събират информация. Тази стъпка е ключова за функции като сигурност ([Sandbox |sandbox]) или оптимизация. -4. **Генериране на код:** Накрая компилаторът обхожда (потенциално модифицираното) AST и генерира съответния код на PHP клас. Този PHP код е това, което всъщност рендира шаблона при изпълнение. -5. **Кеширане:** Генерираният PHP код се съхранява на диск, което прави последващите рендирания много бързи, тъй като стъпки 1-4 се пропускат. - -В действителност компилацията е малко по-сложна. Latte **има два** лексера и парсера: един за HTML шаблона и втори за PHP-подобния код вътре в таговете. Също така парсването не се извършва след токенизацията, а лексерът и парсерът работят паралелно в две "нишки" и се координират. Повярвайте ми, програмирането на това беше ракетна наука :-) - -Целият процес, от зареждането на съдържанието на шаблона, през парсването, до генерирането на крайния файл, може да бъде изпълнен последователно с този код, с който можете да експериментирате и да извеждате междинни резултати: - -```php -$latte = new Latte\Engine; -$source = $latte->getLoader()->getContent($file); -$ast = $latte->parse($source); -$latte->applyPasses($ast); -$code = $latte->generate($ast, $file); -``` - - -Анатомия на таг -=============== - -Създаването на напълно функционален персонализиран таг в Latte включва няколко взаимосвързани части. Преди да се заемем с имплементацията, нека разберем основните концепции и терминология, използвайки аналогия с HTML и Document Object Model (DOM). - - -Тагове срещу Възли (Аналогия с HTML) ------------------------------------- - -В HTML пишем **тагове** като `

    ` или `

    ...
    `. Тези тагове са синтаксис в изходния код. Когато браузърът парсва този HTML, той създава представяне в паметта, наречено **Document Object Model (DOM)**. В DOM HTML таговете са представени от **възли** (конкретно възли `Element` в терминологията на JavaScript DOM). С тези *възли* работим програмно (напр. с помощта на JavaScript `document.getElementById(...)` се връща възел Element). Тагът е само текстово представяне в изходния файл; възелът е обектно представяне в логическото дърво. - -Latte работи по подобен начин: - -- Във файла `.latte` на шаблона пишете **Latte тагове**, като `{foreach ...}` и `{/foreach}`. Това е синтаксисът, с който вие като автор на шаблона работите. -- Когато Latte **парсва** шаблона, той изгражда **Abstract Syntax Tree (AST)**. Това дърво е съставено от **възли**. Всеки Latte таг, HTML елемент, част от текст или израз в шаблона се превръща в един или повече възли в това дърво. -- Основният клас за всички възли в AST е `Latte\Compiler\Node`. Точно както DOM има различни типове възли (Element, Text, Comment), AST на Latte има различни типове възли. Ще се сблъскате с `Latte\Compiler\Nodes\TextNode` за статичен текст, `Latte\Compiler\Nodes\Html\ElementNode` за HTML елементи, `Latte\Compiler\Nodes\Php\ExpressionNode` за изрази вътре в таговете и ключово за персонализирани тагове, възли, наследяващи от `Latte\Compiler\Nodes\StatementNode`. - - -Защо `StatementNode`? ---------------------- - -HTML елементите (`Html\ElementNode`) основно представят структура и съдържание. PHP изразите (`Php\ExpressionNode`) представят стойности или изчисления. Но какво да кажем за Latte тагове като `{if}`, `{foreach}` или нашия собствен `{datetime}`? Тези тагове *изпълняват действия*, управляват потока на програмата или генерират изход въз основа на логика. Те са функционални единици, които правят Latte мощен шаблонен *engine*, а не просто маркиращ език. - -В програмирането такива единици, изпълняващи действия, често се наричат "statements" (инструкции). Затова възлите, представящи тези функционални Latte тагове, обикновено наследяват от `Latte\Compiler\Nodes\StatementNode`. Това ги отличава от чисто структурните възли (като HTML елементи) или възлите, представящи стойности (като изрази). - - -Ключови компоненти -================== - -Нека разгледаме основните компоненти, необходими за създаване на персонализиран таг: - - -Функция за парсване на таг --------------------------- - -- Тази PHP callable функция парсва синтаксиса на Latte тага (`{...}`) в изходния шаблон. -- Получава информация за тага (като неговото име, позиция и дали е n:атрибут) чрез обекта [api:Latte\Compiler\Tag]. -- Нейният основен инструмент за парсване на аргументи и изрази вътре в ограничителите на тага е обектът [api:Latte\Compiler\TagParser], достъпен чрез `$tag->parser` (това е различен парсер от този, който парсва целия шаблон). -- За сдвоени тагове използва `yield`, за да сигнализира на Latte да парсва вътрешното съдържание между началния и крайния таг. -- Крайната цел на парсващата функция е да създаде и върне инстанция на **класа на възела**, която се добавя към AST. -- Практика е (макар и да не е задължително) да се имплементира парсващата функция като статичен метод (често наречен `create`) директно в съответния клас на възела. Това поддържа парсващата логика и представянето на възела спретнато в един пакет, позволява достъп до private/protected елементи на класа, ако е необходимо, и подобрява организацията. - - -Клас на възела --------------- - -- Представлява *логическата функция* на вашия таг в **Abstract Syntax Tree (AST)**. -- Съдържа парсваната информация (като аргументи или съдържание) като публични свойства. Тези свойства често съдържат други инстанции на `Node` (напр. `ExpressionNode` за парсвани аргументи, `AreaNode` за парсвано съдържание). -- Методът `print(PrintContext $context): string` генерира *PHP код* (инструкция или серия от инструкции), който изпълнява действието на тага по време на рендиране на шаблона. -- Методът `getIterator(): \Generator` предоставя достъп до дъщерните възли (аргументи, съдържание) за обхождане от **компилационните проходи**. Трябва да предоставя референции (`&`), за да позволи на проходите потенциално да модифицират или заменят подвъзли. -- След като целият шаблон е парсван в AST, Latte изпълнява серия от [компилационни проходи |compiler-passes]. Тези проходи обхождат *цялото* AST, използвайки метода `getIterator()`, предоставен от всеки възел. Те могат да инспектират възли, да събират информация и дори да *модифицират* дървото (напр. чрез промяна на публичните свойства на възлите или пълна замяна на възли). Този дизайн, изискващ комплексен `getIterator()`, е фундаментален. Той позволява на мощни функции като [Sandbox |sandbox] да анализират и потенциално да променят поведението на *всяка* част от шаблона, включително вашите персонализирани тагове, осигурявайки сигурност и консистентност. - - -Регистрация чрез разширение ---------------------------- - -- Трябва да информирате Latte за вашия нов таг и коя парсваща функция трябва да се използва за него. Това се случва в рамките на [Latte разширение |extending-latte#Latte Extension]. -- Вътре във вашия клас на разширението имплементирате метода `getTags(): array`. Този метод връща асоциативен масив, където ключовете са имената на таговете (напр. `'mytag'`, `'n:myattribute'`), а стойностите са PHP callable функции, представляващи техните съответни парсващи функции (напр. `MyNamespace\DatetimeNode::create(...)`). - -Резюме: **Функцията за парсване на таг** преобразува *изходния код на шаблона* на вашия таг във **възел на AST**. **Класът на възела** след това може да преобразува *себе си* в изпълним *PHP код* за компилирания шаблон и предоставя достъп до своите подвъзли за **компилационните проходи** чрез `getIterator()`. **Регистрацията чрез разширение** свързва името на тага с парсващата функция и уведомява Latte за него. - -Сега ще разгледаме как да имплементираме тези компоненти стъпка по стъпка. - - -Създаване на прост таг -====================== - -Нека се заемем със създаването на вашия първи персонализиран Latte таг. Ще започнем с много прост пример: таг с име `{datetime}`, който извежда текущата дата и час. **Първоначално този таг няма да приема никакви аргументи**, но ще го подобрим по-късно в секцията [#"Парсване на аргументи на таг"]. Той също така няма вътрешно съдържание. - -Този пример ще ви преведе през основните стъпки: дефиниране на класа на възела, имплементиране на неговите методи `print()` и `getIterator()`, създаване на парсваща функция и накрая регистриране на тага. - -**Цел:** Имплементиране на `{datetime}` за извеждане на текущата дата и час с помощта на PHP функцията `date()`. - - -Създаване на класа на възела ----------------------------- - -Първо, имаме нужда от клас, който ще представлява нашия таг в Abstract Syntax Tree (AST). Както беше обсъдено по-горе, наследяваме от `Latte\Compiler\Nodes\StatementNode`. - -Създайте файл (напр. `DatetimeNode.php`) и дефинирайте класа: - -```php -node = new self; - return $node; - } - - /** - * Генерира PHP код, който ще бъде изпълнен при рендиране на шаблона. - */ - public function print(PrintContext $context): string - { - return $context->format( - 'echo date(\'Y-m-d H:i:s\') %line;', - $this->position, - ); - } - - /** - * Предоставя достъп до дъщерните възли за компилационните проходи на Latte. - */ - public function &getIterator(): \Generator - { - false && yield; - } -} -``` - -Когато Latte срещне `{datetime}` в шаблона, той извиква парсващата функция `create()`. Нейната задача е да върне инстанция на `DatetimeNode`. - -Методът `print()` генерира PHP код, който ще бъде изпълнен при рендиране на шаблона. Извикваме метода `$context->format()`, който съставя крайния низ от PHP код за компилирания шаблон. Първият аргумент, `'echo date('Y-m-d H:i:s') %line;'`, е маска, в която се попълват следващите параметри. Placeholder-ът `%line` казва на метода `format()` да използва втория аргумент, който е `$this->position`, и да вмъкне коментар като `/* line 15 */`, който свързва генерирания PHP код обратно към оригиналния ред на шаблона, което е ключово за дебъгване. - -Свойството `$this->position` се наследява от базовия клас `Node` и се задава автоматично от парсера на Latte. То съдържа обект [api:Latte\Compiler\Position], който показва къде е намерен тагът в изходния файл `.latte`. - -Методът `getIterator()` е от съществено значение за компилационните проходи. Той трябва да предоставя всички дъщерни възли, но нашият прост `DatetimeNode` в момента няма никакви аргументи или съдържание, следователно няма дъщерни възли. Въпреки това, методът все още трябва да съществува и да бъде генератор, т.е. ключовата дума `yield` трябва да присъства по някакъв начин в тялото на метода. - - -Регистрация чрез разширение ---------------------------- - -Накрая, нека информираме Latte за новия таг. Създайте [клас на разширение |extending-latte#Latte Extension] (напр. `MyLatteExtension.php`) и регистрирайте тага в неговия метод `getTags()`. - -```php - Карта: 'име-на-таг' => парсваща-функция - */ - public function getTags(): array - { - return [ - 'datetime' => DatetimeNode::create(...), - // По-късно регистрирайте повече тагове тук - ]; - } -} -``` - -След това регистрирайте това разширение в Latte Engine: - -```php -$latte = new Latte\Engine; -$latte->addExtension(new App\Latte\MyLatteExtension); -``` - -Създайте шаблон: - -```latte -

    Страницата е генерирана: {datetime}

    -``` - -Очакван изход: `

    Страницата е генерирана: 2023-10-27 11:00:00

    ` - - -Резюме на тази фаза -------------------- - -Успешно създадохме основен персонализиран таг `{datetime}`. Дефинирахме неговото представяне в AST (`DatetimeNode`), обработихме неговото парсване (`create()`), специфицирахме как трябва да генерира PHP код (`print()`), осигурихме достъп до неговите деца за обхождане (`getIterator()`) и го регистрирахме в Latte. - -В следващата секция ще подобрим този таг, така че да приема аргументи, и ще покажем как да парсваме изрази и да управляваме дъщерни възли. - - -Парсване на аргументи на таг -============================ - -Нашият прост таг `{datetime}` работи, но не е много гъвкав. Нека го подобрим, така че да приема незадължителен аргумент: форматиращ низ за функцията `date()`. Изискваният синтаксис ще бъде `{datetime $format}`. - -**Цел:** Да се модифицира `{datetime}`, така че да приема незадължителен PHP израз като аргумент, който ще бъде използван като форматиращ низ за `date()`. - - -Представяне на `TagParser` --------------------------- - -Преди да модифицираме кода, е важно да разберем инструмента, който ще използваме [api:Latte\Compiler\TagParser]. Когато основният парсер на Latte (`TemplateParser`) срещне Latte таг като `{datetime ...}` или n:атрибут, той делегира парсването на съдържанието *вътре* в тага (частта между `{` и `}` или стойността на атрибута) на специализиран `TagParser`. - -Този `TagParser` работи изключително с **аргументите на тага**. Неговата задача е да обработва токените, представляващи тези аргументи. Ключово е, че **трябва да обработи цялото съдържание**, което му е предоставено. Ако вашата парсваща функция приключи, но `TagParser` не е достигнал края на аргументите (проверява се чрез `$tag->parser->isEnd()`), Latte ще хвърли изключение, тъй като това показва, че вътре в тага са останали неочаквани токени. Обратно, ако тагът *изисква* аргументи, трябва да извикате `$tag->expectArguments()` в началото на вашата парсваща функция. Този метод проверява дали има аргументи и хвърля полезно изключение, ако тагът е бил използван без никакви аргументи. - -`TagParser` предлага полезни методи за парсване на различни видове аргументи: - -- `parseExpression(): ExpressionNode`: Парсва PHP-подобен израз (променливи, литерали, оператори, извиквания на функции/методи и т.н.). Обработва синтактичната захар на Latte, като например третирането на прости буквено-цифрови низове като низове в кавички (напр. `foo` се парсва, сякаш е `'foo'`). -- `parseUnquotedStringOrExpression(): ExpressionNode`: Парсва или стандартен израз, или *низ без кавички*. Низовете без кавички са последователности, позволени от Latte без кавички, често използвани за неща като пътища до файлове (напр. `{include ../file.latte}`). Ако парсва низ без кавички, връща `StringNode`. -- `parseArguments(): ArrayNode`: Парсва аргументи, разделени със запетаи, потенциално с ключове, като `10, name: 'John', true`. -- `parseModifier(): ModifierNode`: Парсва филтри като `|upper|truncate:10`. -- `parseType(): ?SuperiorTypeNode`: Парсва PHP указания за тип като `int`, `?string`, `array|Foo`. - -За по-сложни или ниско ниво нужди от парсване, можете директно да взаимодействате с [потока от токени |api:Latte\Compiler\TokenStream] чрез `$tag->parser->stream`. Този обект предоставя методи за проверка и обработка на отделни токени: - -- `$tag->parser->stream->is(...): bool`: Проверява дали *текущият* токен съответства на някой от указаните типове (напр. `Token::Php_Variable`) или литерални стойности (напр. `'as'`) без да го консумира. Полезно за поглед напред. -- `$tag->parser->stream->consume(...): Token`: Консумира *текущия* токен и премества позицията на потока напред. Ако са предоставени очаквани типове/стойности на токени като аргументи и текущият токен не съответства, хвърля `CompileException`. Използвайте това, когато *очаквате* определен токен. -- `$tag->parser->stream->tryConsume(...): ?Token`: Опитва се да консумира *текущия* токен *само ако* съответства на един от указаните типове/стойности. Ако съответства, консумира токена и го връща. Ако не съответства, оставя позицията на потока непроменена и връща `null`. Използвайте това за незадължителни токени или когато избирате между различни синтактични пътища. - - -Актуализиране на парсващата функция `create()` ----------------------------------------------- - -С това разбиране, нека модифицираме метода `create()` в `DatetimeNode`, така че да парсва незадължителния форматиращ аргумент с помощта на `$tag->parser`. - -```php -node = new self; - - // Проверяваме дали съществуват някакви токени - if (!$tag->parser->isEnd()) { - // Парсваме аргумента като PHP-подобен израз с помощта на TagParser. - $node->format = $tag->parser->parseExpression(); - } - - return $node; - } - - // ... методите print() и getIterator() ще бъдат актуализирани по-нататък ... -} -``` - -Добавихме публично свойство `$format`. В `create()` сега използваме `$tag->parser->isEnd()`, за да проверим дали *съществуват* аргументи. Ако да, `$tag->parser->parseExpression()` обработва токените за израза. Тъй като `TagParser` трябва да обработи всички входни токени, Latte автоматично ще хвърли грешка, ако потребителят напише нещо неочаквано след израза за формат (напр. `{datetime 'Y-m-d', unexpected}`). - - -Актуализиране на метода `print()` ---------------------------------- - -Сега нека модифицираме метода `print()`, така че да използва парсвания израз за формат, съхранен в `$this->format`. Ако не е предоставен формат (`$this->format` е `null`), трябва да използваме форматиращ низ по подразбиране, например `'Y-m-d H:i:s'`. - -```php - public function print(PrintContext $context): string - { - $formatNode = $this->format ?? new StringNode('Y-m-d H:i:s'); - - // %node отпечатва PHP кодовото представяне на $formatNode. - return $context->format( - 'echo date(%node) %line;', - $formatNode, - $this->position - ); - } -``` - -В променливата `$formatNode` съхраняваме възела на AST, представляващ форматиращия низ за PHP функцията `date()`. Използваме тук оператора за нулево сливане (`??`). Ако потребителят е предоставил аргумент в шаблона (напр. `{datetime 'd.m.Y'}`), тогава свойството `$this->format` съдържа съответния възел (в този случай `StringNode` със стойност `'d.m.Y'`) и този възел се използва. Ако потребителят не е предоставил аргумент (написал е само `{datetime}`), свойството `$this->format` е `null` и вместо това създаваме нов `StringNode` с формат по подразбиране `'Y-m-d H:i:s'`. Това гарантира, че `$formatNode` винаги съдържа валиден възел на AST за формата. - -В маската `'echo date(%node) %line;'` се използва нов placeholder `%node`, който казва на метода `format()` да вземе първия следващ аргумент (който е нашият `$formatNode`), да извика неговия метод `print()` (който ще върне неговото PHP кодово представяне) и да вмъкне резултата на позицията на placeholder-а. - - -Имплементиране на `getIterator()` за подвъзли ---------------------------------------------- - -Нашият `DatetimeNode` сега има дъщерен възел: изразът `$format`. **Трябва** да направим този дъщерен възел достъпен за компилационните проходи, като го предоставим в метода `getIterator()`. Не забравяйте да предоставите *референция* (`&`), за да позволите на проходите потенциално да заменят възела. - -```php - public function &getIterator(): \Generator - { - if ($this->format) { - yield $this->format; - } - } -``` - -Защо е толкова важно? Представете си Sandbox проход, който трябва да провери дали аргументът `$format` не съдържа забранено извикване на функция (напр. `{datetime dangerousFunction()}`). Ако `getIterator()` не предостави `$this->format`, Sandbox проходът никога няма да види извикването на `dangerousFunction()` вътре в аргумента на нашия таг, което би създало потенциална дупка в сигурността. Като му го предоставяме, позволяваме на Sandbox (и други проходи) да проверяват и потенциално да модифицират възела на израза `$format`. - - -Използване на подобрения таг ----------------------------- - -Тагът сега правилно обработва незадължителния аргумент: - -```latte -Формат по подразбиране: {datetime} -Персонализиран формат: {datetime 'd.m.Y'} -Използване на променлива: {datetime $userDateFormatPreference} - -{* Това би причинило грешка след парсването на 'd.m.Y', тъй като ", foo" е неочаквано *} -{* {datetime 'd.m.Y', foo} *} -``` - -След това ще разгледаме създаването на сдвоени тагове, които обработват съдържанието между тях. - - -Обработка на сдвоени тагове -=========================== - -Досега нашият таг `{datetime}` беше *самозатварящ се* (концептуално). Той няма съдържание между началния и крайния таг. Много полезни тагове обаче работят с блок от съдържание на шаблона. Те се наричат **сдвоени тагове**. Примерите включват `{if}...{/if}`, `{block}...{/block}` или персонализиран таг, който сега ще създадем: `{debug}...{/debug}`. - -Този таг ще ни позволи да включим в нашите шаблони дебъг информация, която трябва да бъде видима само по време на разработка. - -**Цел:** Да се създаде сдвоен таг `{debug}`, чието съдържание се рендира само когато е активен специфичен флаг "режим на разработка". - - -Представяне на Providers ------------------------- - -Понякога вашите тагове се нуждаят от достъп до данни или услуги, които не се предават директно като параметри на шаблона. Например, определяне дали приложението е в режим на разработка, достъп до обект на потребител или получаване на конфигурационни стойности. Latte предоставя механизъм, наречен **Providers** за тази цел. - -Providers се регистрират във вашето [разширение |extending-latte#Latte Extension] с помощта на метода `getProviders()`. Този метод връща асоциативен масив, където ключовете са имената, под които providers ще бъдат достъпни в кода на шаблона по време на изпълнение, а стойностите са действителните данни или обекти. - -Вътре в PHP кода, генериран от метода `print()` на вашия таг, можете да получите достъп до тези providers чрез специалното свойство на обекта `$this->global`. Тъй като това свойство се споделя между всички разширения, добра практика е **да добавяте префикс към имената на вашите providers**, за да предотвратите потенциални конфликти на имена с ключови providers на Latte или providers от други разширения на трети страни. Често срещана конвенция е да се използва кратък, уникален префикс, свързан с вашия производител или име на разширение. За нашия пример ще използваме префикс `app` и флагът за режим на разработка ще бъде достъпен като `$this->global->appDevMode`. - - -Ключовата дума `yield` за парсване на съдържание ------------------------------------------------- - -Как казваме на парсера на Latte да обработи съдържанието *между* `{debug}` и `{/debug}`? Тук влиза в игра ключовата дума `yield`. - -Когато `yield` се използва във функцията `create()`, функцията се превръща в [PHP генератор |https://www.php.net/manual/en/language.generators.overview.php]. Нейното изпълнение се спира и контролът се връща към основния `TemplateParser`. След това `TemplateParser` продължава да парсва съдържанието на шаблона, *докато* не срещне съответния затварящ таг (`{/debug}` в нашия случай). - -След като бъде намерен затварящият таг, `TemplateParser` възобновява изпълнението на нашата функция `create()` точно след инструкцията `yield`. Стойността, *върната* от инструкцията `yield`, е масив, съдържащ два елемента: - -1. `AreaNode`, представляващ парсваното съдържание между началния и крайния таг. -2. Обект `Tag`, представляващ затварящия таг (напр. `{/debug}`). - -Нека създадем клас `DebugNode` и неговия метод `create`, използващ `yield`. - -```php -node = new self; - - // Спиране на парсването, получаване на вътрешното съдържание и крайния таг, когато е намерен {/debug} - [$node->content, $endTag] = yield; - - return $node; - } - - // ... print() и getIterator() ще бъдат имплементирани по-нататък ... -} -``` - -Забележка: `$endTag` е `null`, ако тагът се използва като n:атрибут, т.е. `
    ...
    `. - - -Имплементиране на `print()` за условно рендиране ------------------------------------------------- - -Методът `print()` сега трябва да генерира PHP код, който по време на изпълнение проверява provider-а `appDevMode` и изпълнява кода за вътрешното съдържание само ако флагът е true. - -```php - public function print(PrintContext $context): string - { - // Генерира PHP инструкция 'if', която по време на изпълнение проверява provider-а - return $context->format( - <<<'XX' - if ($this->global->appDevMode) %line { - // Ако е в режим на разработка, извежда вътрешното съдържание - %node - } - - XX, - $this->position, // За %line коментар - $this->content, // Възел, съдържащ AST на вътрешното съдържание - ); - } -``` - -Това е просто. Използваме `PrintContext::format()`, за да създадем стандартна PHP инструкция `if`. Вътре в `if` поставяме placeholder `%node` за `$this->content`. Latte рекурсивно ще извика `$this->content->print($context)`, за да генерира PHP код за вътрешната част на тага, но само ако `$this->global->appDevMode` се оцени като true по време на изпълнение. - - -Имплементиране на `getIterator()` за съдържание ------------------------------------------------ - -Точно както при възела на аргумента в предишния пример, нашият `DebugNode` сега има дъщерен възел: `AreaNode $content`. Трябва да го направим достъпен, като го предоставим в `getIterator()`: - -```php - public function &getIterator(): \Generator - { - // Предоставя референция към възела на съдържанието - yield $this->content; - } -``` - -Това позволява на компилационните проходи да слязат в съдържанието на нашия таг `{debug}`, което е важно, дори ако съдържанието се рендира условно. Например, Sandbox трябва да анализира съдържанието, независимо дали `appDevMode` е true или false. - - -Регистрация и използване ------------------------- - -Регистрирайте тага и provider-а във вашето разширение: - -```php -class MyLatteExtension extends Extension -{ - // Предполагаме, че $isDevelopmentMode се определя някъде (напр. от конфигурацията) - public function __construct( - private bool $isDevelopmentMode, - ) { - } - - public function getTags(): array - { - return [ - 'datetime' => DatetimeNode::create(...), - 'debug' => DebugNode::create(...), // Регистрация на новия таг - ]; - } - - public function getProviders(): array - { - return [ - 'appDevMode' => $this->isDevelopmentMode, // Регистрация на provider-а - ]; - } -} - -// При регистрация на разширението: -$isDev = true; // Определете това въз основа на средата на вашето приложение -$latte->addExtension(new App\Latte\MyLatteExtension($isDev)); -``` - -И неговото използване в шаблона: - -```latte -

    Обикновено съдържание, видимо винаги.

    - -{debug} -
    - ID на текущия потребител: {$user->id} - Време на заявката: {=time()} -
    -{/debug} - -

    Друго обикновено съдържание.

    -``` - - -Интеграция на n:атрибути ------------------------- - -Latte предлага удобен съкратен запис за много сдвоени тагове: [n:атрибути |syntax#n:атрибути]. Ако имате сдвоен таг като `{tag}...{/tag}` и искате неговият ефект да се приложи директно към един HTML елемент, често можете да го запишете по-икономично като атрибут `n:tag` на този елемент. - -За повечето стандартни сдвоени тагове, които дефинирате (като нашия `{debug}`), Latte автоматично ще позволи съответната версия на `n:` атрибута. По време на регистрацията не е необходимо да правите нищо допълнително: - -```latte -{* Стандартно използване на сдвоен таг *} -{debug}
    Информация за дебъгване
    {/debug} - -{* Еквивалентно използване с n:атрибут *} -
    Информация за дебъгване
    -``` - -И двете версии ще рендират `
    ` само ако `$this->global->appDevMode` е true. Префиксите `inner-` и `tag-` също работят според очакванията. - -Понякога логиката на вашия таг може да се нуждае от леко различно поведение в зависимост от това дали се използва като стандартен сдвоен таг или като n:атрибут, или дали се използва префикс като `n:inner-tag` или `n:tag-tag`. Обектът `Latte\Compiler\Tag`, предаден на вашата парсваща функция `create()`, предоставя тази информация: - -- `$tag->isNAttribute(): bool`: Връща `true`, ако тагът се парсва като n:атрибут -- `$tag->prefix: ?string`: Връща префикса, използван с n:атрибута, който може да бъде `null` (не е n:атрибут), `Tag::PrefixNone`, `Tag::PrefixInner` или `Tag::PrefixTag` - -Сега, когато разбираме простите тагове, парсването на аргументи, сдвоените тагове, providers и n:атрибутите, нека се заемем с по-сложен сценарий, включващ тагове, вложени в други тагове, използвайки нашия таг `{debug}` като отправна точка. - - -Междинни тагове -=============== - -Някои сдвоени тагове позволяват или дори изискват други тагове да се появят *вътре* в тях преди крайния затварящ таг. Те се наричат **междинни тагове**. Класически примери включват `{if}...{elseif}...{else}...{/if}` или `{switch}...{case}...{default}...{/switch}`. - -Нека разширим нашия таг `{debug}` с поддръжка на незадължителна клауза `{else}`, която ще бъде рендирана, когато приложението *не е* в режим на разработка. - -**Цел:** Да се модифицира `{debug}`, така че да поддържа незадължителен междинен таг `{else}`. Крайният синтаксис трябва да бъде `{debug} ... {else} ... {/debug}`. - - -Парсване на междинни тагове с помощта на `yield` ------------------------------------------------- - -Вече знаем, че `yield` спира парсващата функция `create()` и връща парсваното съдържание заедно с крайния таг. `yield` обаче предлага повече контрол: можете да му предоставите масив от *имена на междинни тагове*. Когато парсерът срещне някой от тези указани тагове **на същото ниво на влагане** (т.е. като преки деца на родителския таг, не вътре в други блокове или тагове вътре в него), той също спира парсването. - -Когато парсването спре поради междинен таг, то спира парсването на съдържанието, възобновява генератора `create()` и предава обратно частично парсваното съдържание и **междинния таг** сам по себе си (вместо крайния затварящ таг). Нашата функция `create()` след това може да обработи този междинен таг (напр. да парсва неговите аргументи, ако има такива) и отново да използва `yield`, за да парсва *следващата* част от съдържанието до *крайния* затварящ таг или друг очакван междинен таг. - -Нека модифицираме `DebugNode::create()`, така че да очаква `{else}`: - -```php -node = new self; - - // yield и очакваме или {/debug} или {else} - [$node->thenContent, $nextTag] = yield ['else']; - - // Проверяваме дали тагът, при който сме спрели, е бил {else} - if ($nextTag?->name === 'else') { - // Yield отново за парсване на съдържанието между {else} и {/debug} - [$node->elseContent, $endTag] = yield; - } - - return $node; - } - - // ... print() и getIterator() ще бъдат актуализирани по-нататък ... -} -``` - -Сега `yield ['else']` казва на Latte да спре парсването не само за `{/debug}`, но и за `{else}`. Ако `{else}` бъде намерен, `$nextTag` ще съдържа обект `Tag` за `{else}`. След това отново използваме `yield` без аргументи, което означава, че сега очакваме само крайния таг `{/debug}`, и съхраняваме резултата в `$node->elseContent`. Ако `{else}` не е бил намерен, `$nextTag` би бил `Tag` за `{/debug}` (или `null`, ако се използва като n:атрибут) и `$node->elseContent` би останал `null`. - - -Имплементиране на `print()` с `{else}` --------------------------------------- - -Методът `print()` трябва да отразява новата структура. Той трябва да генерира PHP инструкция `if/else`, базирана на provider-а `appDevMode`. - -```php - public function print(PrintContext $context): string - { - return $context->format( - <<<'XX' - if ($this->global->appDevMode) %line { - %node // Код за клона 'then' (съдържание на {debug}) - } else { - %node // Код за клона 'else' (съдържание на {else}) - } - - XX, - $this->position, // Номер на ред за условието 'if' - $this->thenContent, // Първи placeholder %node - $this->elseContent ?? new NopNode, // Втори placeholder %node - ); - } -``` - -Това е стандартна PHP структура `if/else`. Използваме `%node` два пъти; `format()` замества предоставените възли последователно. Използваме `?? new NopNode`, за да избегнем грешки, ако `$this->elseContent` е `null` – `NopNode` просто не отпечатва нищо. - - -Имплементиране на `getIterator()` за двете съдържания ------------------------------------------------------ - -Сега имаме потенциално два дъщерни възела на съдържание (`$thenContent` и `$elseContent`). Трябва да предоставим и двата, ако съществуват: - -```php - public function &getIterator(): \Generator - { - yield $this->thenContent; - if ($this->elseContent) { - yield $this->elseContent; - } - } -``` - - -Използване на подобрения таг ----------------------------- - -Тагът сега може да бъде използван с незадължителна клауза `{else}`: - -```latte -{debug} -

    Показване на дебъг информация, защото devMode е ВКЛЮЧЕНО.

    -{else} -

    Дебъг информацията е скрита, защото devMode е ИЗКЛЮЧЕНО.

    -{/debug} -``` - - -Обработка на състояние и влагане -================================ - -Нашите предишни примери (`{datetime}`, `{debug}`) бяха относително без състояние в рамките на своите методи `print()`. Те или директно извеждаха съдържание, или извършваха проста условна проверка, базирана на глобален provider. Много тагове обаче трябва да управляват някаква форма на **състояние** по време на рендиране или включват оценка на потребителски изрази, които трябва да бъдат изпълнени само веднъж поради производителност или коректност. Освен това трябва да обмислим какво се случва, когато нашите персонализирани тагове са **вложени**. - -Нека илюстрираме тези концепции, като създадем таг `{repeat $count}...{/repeat}`. Този таг ще повтори своето вътрешно съдържание `$count` пъти. - -**Цел:** Имплементиране на `{repeat $count}`, който повтаря своето съдържание указан брой пъти. - - -Нуждата от временни и уникални променливи ------------------------------------------ - -Представете си, че потребителят напише: - -```latte -{repeat rand(1, 5)} Съдържание {/repeat} -``` - -Ако наивно генерираме PHP `for` цикъл по този начин в нашия метод `print()`: - -```php -// Опростен, НЕПРАВИЛЕН генериран код -for ($i = 0; $i < rand(1, 5); $i++) { - // извеждане на съдържание -} -``` -Това би било грешно! Изразът `rand(1, 5)` би бил **преизчислен при всяка итерация на цикъла**, което би довело до непредсказуем брой повторения. Трябва да оценим израза `$count` *веднъж* преди началото на цикъла и да съхраним резултата му. - -Ще генерираме PHP код, който първо оценява израза за броя и го съхранява във **временна променлива по време на изпълнение**. За да предотвратим конфликти с променливи, дефинирани от потребителя на шаблона, *и* вътрешни променливи на Latte (като `$ʟ_...`), ще използваме конвенцията за префикс **`$__` (двойно долно тире)** за нашите временни променливи. - -Генерираният код тогава би изглеждал така: - -```php -$__count = rand(1, 5); -for ($__i = 0; $__i < $__count; $__i++) { - // извеждане на съдържание -} -``` - -Сега да разгледаме влагането: - -```latte -{repeat $countA} {* Външен цикъл *} - {repeat $countB} {* Вътрешен цикъл *} - ... - {/repeat} -{/repeat} -``` - -Ако както външният, така и вътрешният таг `{repeat}` генерират код, използващ *едни и същи* имена на временни променливи (напр. `$__count` и `$__i`), вътрешният цикъл би презаписал променливите на външния цикъл, което би нарушило логиката. - -Трябва да гарантираме, че временните променливи, генерирани за всяка инстанция на тага `{repeat}`, са **уникални**. Постигаме това с помощта на `PrintContext::generateId()`. Този метод връща уникално цяло число по време на фазата на компилация. Можем да добавим това ID към имената на нашите временни променливи. - -Така че вместо `$__count`, ще генерираме `$__count_1` за първия таг repeat, `$__count_2` за втория и т.н. Подобно за брояча на цикъла ще използваме `$__i_1`, `$__i_2` и т.н. - - -Имплементиране на `RepeatNode` ------------------------------- - -Нека създадем класа на възела. - -```php -expectArguments(); // уверява се, че $count е предоставен - $node = $tag->node = new self; - // Парсва израза за броя - $node->count = $tag->parser->parseExpression(); - // Получаване на вътрешното съдържание - [$node->content] = yield; - return $node; - } - - /** - * Генерира PHP 'for' цикъл с уникални имена на променливи. - */ - public function print(PrintContext $context): string - { - // Генериране на уникални имена на променливи - $id = $context->generateId(); - $countVar = '$__count_' . $id; // напр. $__count_1, $__count_2, и т.н. - $iteratorVar = '$__i_' . $id; // напр. $__i_1, $__i_2, и т.н. - - return $context->format( - <<<'XX' - // Оценка на израза за броя *веднъж* и съхраняване - %raw = (int) (%node); - // Цикъл с използване на съхранения брой и уникална итерационна променлива - for (%raw = 0; %2.raw < %0.raw; %2.raw++) %line { - %node // Рендиране на вътрешното съдържание - } - - XX, - $countVar, // %0 - Променлива за съхраняване на броя - $this->count, // %1 - Възел на израза за броя - $iteratorVar, // %2 - Име на итерационната променлива на цикъла - $this->position, // %3 - Коментар с номер на ред за самия цикъл - $this->content // %4 - Възел на вътрешното съдържание - ); - } - - /** - * Предоставя дъщерните възли (израз за броя и съдържание). - */ - public function &getIterator(): \Generator - { - yield $this->count; - yield $this->content; - } -} -``` - -Методът `create()` парсва изисквания израз `$count` с помощта на `parseExpression()`. Първо се извиква `$tag->expectArguments()`. Това гарантира, че потребителят е предоставил *нещо* след `{repeat}`. Докато `$tag->parser->parseExpression()` би се провалило, ако нищо не е предоставено, съобщението за грешка може да бъде за неочакван синтаксис. Използването на `expectArguments()` предоставя много по-ясна грешка, конкретно посочваща, че липсват аргументи за тага `{repeat}`. - -Методът `print()` генерира PHP код, отговорен за изпълнението на логиката на повторение по време на изпълнение. Започва с генериране на уникални имена за временните PHP променливи, които ще са му нужни. - -Методът `$context->format()` се извиква с нов placeholder `%raw`, който вмъква *суровия низ*, предоставен като съответен аргумент. Тук той вмъква уникалното име на променлива, съхранено в `$countVar` (напр. `$__count_1`). А какво да кажем за `%0.raw` и `%2.raw`? Това демонстрира **позиционни placeholders**. Вместо просто `%raw`, който взема *следващия* наличен суров аргумент, `%2.raw` изрично взема аргумента на индекс 2 (който е `$iteratorVar`) и вмъква неговата сурова низова стойност. Това ни позволява да използваме повторно низа `$iteratorVar`, без да го предаваме многократно в списъка с аргументи за `format()`. - -Това внимателно конструирано извикване на `format()` генерира ефективен и безопасен PHP цикъл, който правилно обработва израза за броя и избягва конфликти на имена на променливи, дори когато таговете `{repeat}` са вложени. - - -Регистрация и използване ------------------------- - -Регистрирайте тага във вашето разширение: - -```php -use App\Latte\RepeatNode; - -class MyLatteExtension extends Extension -{ - public function getTags(): array - { - return [ - 'datetime' => DatetimeNode::create(...), - 'debug' => DebugNode::create(...), - 'repeat' => RepeatNode::create(...), // Регистрация на тага repeat - ]; - } -} -``` - -Използвайте го в шаблона, включително влагане: - -```latte -{var $rows = rand(5, 7)} -{var $cols = rand(3, 5)} - -{repeat $rows} - - {repeat $cols} - Вътрешен цикъл - {/repeat} - -{/repeat} -``` - -Този пример демонстрира как да се обработва състояние (броячи на цикли) и потенциални проблеми с влагането с помощта на временни променливи с префикс `$__` и уникални с ID от `PrintContext::generateId()`. - - -Чисти n:атрибути ----------------- - -Докато много `n:атрибути` като `n:if` или `n:foreach` служат като удобни съкращения за техните двойници в сдвоени тагове (`{if}...{/if}`, `{foreach}...{/foreach}`), Latte също позволява дефинирането на тагове, които *съществуват само* под формата на n:атрибут. Те често се използват за модифициране на атрибути или поведение на HTML елемента, към който са прикрепени. - -Стандартните примери, вградени в Latte, включват [`n:class` |tags#n:class], който помага за динамичното изграждане на атрибута `class`, и [`n:attr` |tags#n:attr], който може да зададе множество произволни атрибути. - -Нека създадем наш собствен чист n:атрибут: `n:confirm`, който добавя JavaScript диалогов прозорец за потвърждение преди извършване на действие (като следване на връзка или изпращане на формуляр). - -**Цел:** Имплементиране на `n:confirm="'Сигурни ли сте?'"`, който добавя обработчик `onclick` за предотвратяване на действието по подразбиране, ако потребителят отмени диалоговия прозорец за потвърждение. - - -Имплементиране на `ConfirmNode` -------------------------------- - -Нуждаем се от клас Node и парсваща функция. - -```php -expectArguments(); - $node = $tag->node = new self; - $node->message = $tag->parser->parseExpression(); - return $node; - } - - /** - * Генерира код на атрибута 'onclick' с правилно екраниране. - */ - public function print(PrintContext $context): string - { - // Гарантира правилно екраниране за контекстите на JavaScript и HTML атрибут. - return $context->format( - <<<'XX' - echo ' onclick="', LR\Filters::escapeHtmlAttr('return confirm(' . LR\Filters::escapeJs(%node) . ')'), '"' %line; - XX, - $this->message, - $this->position, - ); - } - - public function &getIterator(): \Generator - { - yield $this->message; - } -} -``` - -Методът `print()` генерира PHP код, който в крайна сметка по време на рендиране на шаблона извежда HTML атрибута `onclick="..."`. Обработката на вложени контексти (JavaScript вътре в HTML атрибут) изисква внимателно екраниране. Филтърът `LR\Filters::escapeJs(%node)` се извиква по време на изпълнение и екранира съобщението правилно за използване вътре в JavaScript (изходът би бил като `"Sure?"`). След това филтърът `LR\Filters::escapeHtmlAttr(...)` екранира знаците, които са специални в HTML атрибутите, така че това би променило изхода на `return confirm("Sure?")`. Това двустепенно екраниране по време на изпълнение гарантира, че съобщението е безопасно за JavaScript и резултатният JavaScript код е безопасен за вмъкване в HTML атрибута `onclick`. - - -Регистрация и използване ------------------------- - -Регистрирайте n:атрибута във вашето разширение. Не забравяйте префикса `n:` в ключа: - -```php -class MyLatteExtension extends Extension -{ - public function getTags(): array - { - return [ - 'datetime' => DatetimeNode::create(...), - 'debug' => DebugNode::create(...), - 'repeat' => RepeatNode::create(...), - 'n:confirm' => ConfirmNode::create(...), // Регистрация на n:confirm - ]; - } -} -``` - -Сега можете да използвате `n:confirm` върху връзки, бутони или елементи на формуляр: - -```latte -Изтриване -``` - -Генериран HTML: - -```html -Изтриване -``` - -Когато потребителят кликне върху връзката, браузърът изпълнява кода `onclick`, показва диалоговия прозорец за потвърждение и преминава към `delete.php` само ако потребителят кликне върху "OK". - -Този пример демонстрира как може да се създаде чист n:атрибут за модифициране на поведението или атрибутите на своя хост HTML елемент чрез генериране на подходящ PHP код в неговия метод `print()`. Не забравяйте за двойното екраниране, което често се изисква: веднъж за целевия контекст (JavaScript в този случай) и отново за контекста на HTML атрибута. - - -Напреднали теми -=============== - -Докато предишните секции покриват основните концепции, тук са няколко по-напреднали теми, на които може да попаднете при създаването на персонализирани Latte тагове. - - -Режими на изход на тагове -------------------------- - -Обектът `Tag`, предаден на вашата функция `create()`, има свойство `outputMode`. Това свойство влияе върху това как Latte третира околните празни пространства и индентация, особено когато тагът се използва на собствен ред. Можете да модифицирате това свойство във вашата функция `create()`. - -- `Tag::OutputKeepIndentation` (По подразбиране за повечето тагове като `{=...}`): Latte се опитва да запази индентацията преди тага. Новите редове *след* тага обикновено се запазват. Това е подходящо за тагове, които извеждат съдържание в реда. -- `Tag::OutputRemoveIndentation` (По подразбиране за блокови тагове като `{if}`, `{foreach}`): Latte премахва водещата индентация и потенциално един следващ нов ред. Това помага да се поддържа генерираният PHP код по-чист и предотвратява допълнителни празни редове в HTML изхода, причинени от самия таг. Използвайте това за тагове, които представляват контролни структури или блокове, които сами по себе си не трябва да добавят празни пространства. -- `Tag::OutputNone` (Използва се от тагове като `{var}`, `{default}`): Подобно на `RemoveIndentation`, но сигнализира по-силно, че самият таг не произвежда директен изход, потенциално влияейки върху обработката на празни пространства около него още по-агресивно. Подходящо за декларативни или задаващи тагове. - -Изберете режима, който най-добре отговаря на целта на вашия таг. За повечето структурни или контролни тагове обикновено е подходящ `OutputRemoveIndentation`. - - -Достъп до родителски/най-близки тагове --------------------------------------- - -Понякога поведението на тага трябва да зависи от контекста, в който се използва, конкретно в кой родителски таг(ове) се намира. Обектът `Tag`, предаден на вашата функция `create()`, предоставя метода `closestTag(array $classes, ?callable $condition = null): ?Tag` точно за тази цел. - -Този метод търси нагоре в йерархията на текущо отворените тагове (включително HTML елементи, представени вътрешно по време на парсване) и връща обекта `Tag` на най-близкия предшественик, който отговаря на специфични критерии. Ако не бъде намерен съответстващ предшественик, връща `null`. - -Масивът `$classes` указва какъв вид предшествени тагове търсите. Проверява дали асоциираният възел на предшествения таг (`$ancestorTag->node`) е инстанция на този клас. - -```php -function create(Tag $tag) -{ - // Търсене на най-близкия предшествен таг, чийто възел е инстанция на ForeachNode - $foreachTag = $tag->closestTag([ForeachNode::class]); - if ($foreachTag) { - // Можем да получим достъп до самата инстанция на ForeachNode: - $foreachNode = $foreachTag->node; - } -} -``` - -Забележете `$foreachTag->node`: Това работи само защото е конвенция в разработката на Latte тагове незабавно да се присвои създаденият възел на `$tag->node` в рамките на метода `create()`, както винаги сме правили. - -Понякога само сравнението на типа на възела не е достатъчно. Може да се наложи да проверите специфично свойство на потенциалния предшествен таг или неговия възел. Незадължителният втори аргумент за `closestTag()` е callable, който приема потенциалния предшествен обект `Tag` и трябва да връща дали е валидно съвпадение. - -```php -function create(Tag $tag) -{ - $dynamicBlockTag = $tag->closestTag( - [BlockNode::class], - // Условие: блокът трябва да е динамичен - fn(Tag $blockTag) => $blockTag->node->block->isDynamic(), - ); -} -``` - -Използването на `closestTag()` позволява създаването на тагове, които са контекстно осъзнати и налагат правилно използване в рамките на структурата на вашия шаблон, което води до по-здрави и разбираеми шаблони. - - -Placeholders на `PrintContext::format()` ----------------------------------------- - -Често сме използвали `PrintContext::format()`, за да генерираме PHP код в методите `print()` на нашите възли. Той приема низ-маска и следващи аргументи, които заместват placeholders в маската. Ето резюме на наличните placeholders: - -- **`%node`**: Аргументът трябва да бъде инстанция на `Node`. Извиква метода `print()` на възела и вмъква резултантния низ от PHP код. -- **`%dump`**: Аргументът е всяка PHP стойност. Експортира стойността в валиден PHP код. Подходящо за скалари, масиви, null. - - `$context->format('echo %dump;', 'Hello')` -> `echo 'Hello';` - - `$context->format('$arr = %dump;', [1, 2])` -> `$arr = [1, 2];` -- **`%raw`**: Вмъква аргумента директно в изходния PHP код без никакво екраниране или модификация. **Използвайте с повишено внимание**, предимно за вмъкване на предварително генерирани фрагменти от PHP код или имена на променливи. - - `$context->format('%raw = 1;', '$variableName')` -> `$variableName = 1;` -- **`%args`**: Аргументът трябва да бъде `Expression\ArrayNode`. Извежда елементите на масива, форматирани като аргументи за извикване на функция или метод (разделени със запетаи, обработва именувани аргументи, ако присъстват). - - `$argsNode = new ArrayNode([...]);` - - `$context->format('myFunc(%args);', $argsNode)` -> `myFunc(1, name: 'Joe');` -- **`%line`**: Аргументът трябва да бъде обект `Position` (обикновено `$this->position`). Вмъква PHP коментар `/* line X */`, указващ номера на реда на източника. - - `$context->format('echo "Hi" %line;', $this->position)` -> `echo "Hi" /* line 42 */;` -- **`%escape(...)`**: Генерира PHP код, който *по време на изпълнение* екранира вътрешния израз, използвайки текущите контекстно осъзнати правила за екраниране. - - `$context->format('echo %escape(%node);', $variableNode)` -- **`%modify(...)`**: Аргументът трябва да бъде `ModifierNode`. Генерира PHP код, който прилага филтрите, указани в `ModifierNode`, към вътрешното съдържание, включително контекстно осъзнато екраниране, ако не е забранено с `|noescape`. - - `$context->format('%modify(%node);', $modifierNode, $variableNode)` -- **`%modifyContent(...)`**: Подобно на `%modify`, но предназначено за модифициране на блокове от уловено съдържание (често HTML). - -Можете изрично да се позовавате на аргументи по техния индекс (от нула): `%0.node`, `%1.dump`, `%2.raw` и т.н. Това позволява повторното използване на аргумент няколко пъти в маската, без да го предавате многократно на `format()`. Вижте примера с тага `{repeat}`, където бяха използвани `%0.raw` и `%2.raw`. - - -Пример за комплексно парсване на аргументи ------------------------------------------- - -Докато `parseExpression()`, `parseArguments()` и т.н., покриват много случаи, понякога се нуждаете от по-сложна логика за парсване, използваща по-ниско ниво `TokenStream`, достъпно чрез `$tag->parser->stream`. - -**Цел:** Да се създаде таг `{embedYoutube $videoID, width: 640, height: 480}`. Искаме да парсваме изискваното ID на видеото (низ или променлива), последвано от незадължителни двойки ключ-стойност за размерите. - -```php -expectArguments(); - $node = $tag->node = new self; - // Парсване на изискваното ID на видеото - $node->videoId = $tag->parser->parseExpression(); - - // Парсване на незадължителни двойки ключ-стойност - $stream = $tag->parser->stream; // Получаване на потока от токени - while ($stream->tryConsume(',')) { // Изисква разделяне със запетая - // Очакване на идентификатор 'width' или 'height' - $keyToken = $stream->consume(Token::Php_Identifier); - $key = strtolower($keyToken->text); - - $stream->consume(':'); // Очакване на разделител двоеточие - - $value = $tag->parser->parseExpression(); // Парсване на израза за стойност - - if ($key === 'width') { - $node->width = $value; - } elseif ($key === 'height') { - $node->height = $value; - } else { - throw new CompileException("Неизвестен аргумент '$key'. Очаквано 'width' или 'height'.", $keyToken->position); - } - } - - return $node; - } -} -``` - -Това ниво на контрол ви позволява да дефинирате много специфични и комплексни синтаксиси за вашите персонализирани тагове чрез директно взаимодействие с потока от токени. - - -Използване на `AuxiliaryNode` ------------------------------ - -Latte предоставя общи "спомагателни" възли за специални ситуации по време на генериране на код или в рамките на компилационни проходи. Това са `AuxiliaryNode` и `Php\Expression\AuxiliaryNode`. - -Считайте `AuxiliaryNode` за гъвкав контейнерен възел, който делегира своите основни функционалности - генериране на код и излагане на дъщерни възли - на аргументите, предоставени в неговия конструктор: - -- Делегиране на `print()`: Първият аргумент на конструктора е PHP **closure**. Когато Latte извиква метода `print()` на `AuxiliaryNode`, той изпълнява тази предоставена closure. Closure приема `PrintContext` и всички възли, предадени във втория аргумент на конструктора, което ви позволява да дефинирате напълно персонализирана логика за генериране на PHP код по време на изпълнение. -- Делегиране на `getIterator()`: Вторият аргумент на конструктора е **масив от обекти `Node`**. Когато Latte трябва да обходи децата на `AuxiliaryNode` (напр. по време на компилационни проходи), неговият метод `getIterator()` просто предоставя възлите, изброени в този масив. - -Пример: - -```php -$node = new AuxiliaryNode( - // 1. Тази closure става тялото на print() - fn(PrintContext $context, $arg1, $arg2) => $context->format('...%node...%node...', $arg1, $arg2), - - // 2. Тези възли се предоставят от метода getIterator() и се предават на closure по-горе - [$argumentNode1, $argumentNode2] -); -``` - -Latte предоставя два различни типа въз основа на това къде трябва да вмъкнете генерирания код: - -- `Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode`: Използвайте това, когато трябва да генерирате част от PHP код, която представлява **израз** -- `Latte\Compiler\Nodes\AuxiliaryNode`: Използвайте това за по-общи цели, когато трябва да вмъкнете блок от PHP код, представляващ една или повече **инструкции** - -Важна причина да използвате `AuxiliaryNode` вместо стандартни възли (като `StaticMethodCallNode`) в рамките на вашия метод `print()` или компилационен проход е **контролът на видимостта за последващи компилационни проходи**, особено тези, свързани със сигурността, като Sandbox. - -Разгледайте сценарий: Вашият компилационен проход трябва да обвие предоставен от потребителя израз (`$userExpr`) с извикване на специфична, доверена помощна функция `myInternalSanitize($userExpr)`. Ако създадете стандартен възел `new FunctionCallNode('myInternalSanitize', [$userExpr])`, той ще бъде напълно видим за обхождането на AST. Ако Sandbox проходът се изпълни по-късно и `myInternalSanitize` *не е* в неговия списък с разрешени, Sandbox може да *блокира* или модифицира това извикване, потенциално нарушавайки вътрешната логика на вашия таг, дори ако *вие*, авторът на тага, знаете, че това специфично извикване е безопасно и необходимо. Можете следователно да генерирате извикването директно в рамките на closure на `AuxiliaryNode`. - -```php -use Latte\Compiler\Nodes\Php\Expression\AuxiliaryNode; - -// ... вътре в print() или компилационен проход ... -$wrappedNode = new AuxiliaryNode( - fn(PrintContext $context, $userExpr) => $context->format( - 'myInternalSanitize(%node)', // Директно генериране на PHP код - $userExpr, - ), - // ВАЖНО: Все още предайте оригиналния възел на потребителския израз тук! - [$userExpr], -); -``` - -В този случай Sandbox проходът вижда `AuxiliaryNode`, но **не анализира PHP кода, генериран от неговата closure**. Той не може директно да блокира извикването на `myInternalSanitize`, генерирано *вътре* в closure. - -Докато самият генериран PHP код е скрит от проходите, *входовете* към този код (възлите, представляващи потребителски данни или изрази) **трябва все още да бъдат обходими**. Затова вторият аргумент на конструктора на `AuxiliaryNode` е от съществено значение. **Трябва** да предадете масив, съдържащ всички оригинални възли (като `$userExpr` в примера по-горе), които вашата closure използва. `getIterator()` на `AuxiliaryNode` **ще предостави тези възли**, позволявайки на компилационни проходи като Sandbox да ги анализират за потенциални проблеми. - - -Добри практики -============== - -- **Ясна цел:** Уверете се, че вашият таг има ясна и необходима цел. Не създавайте тагове за задачи, които могат лесно да бъдат решени с помощта на [филтри |custom-filters] или [функции |custom-functions]. -- **Правилно имплементирайте `getIterator()`:** Винаги имплементирайте `getIterator()` и предоставяйте *референции* (`&`) към *всички* дъщерни възли (аргументи, съдържание), които са били парсвани от шаблона. Това е необходимо за компилационните проходи, сигурността (Sandbox) и потенциални бъдещи оптимизации. -- **Публични свойства за възли:** Направете свойствата, съдържащи дъщерни възли, публични, за да могат компилационните проходи да ги модифицират при необходимост. -- **Използвайте `PrintContext::format()`:** Използвайте метода `format()` за генериране на PHP код. Той обработва кавички, правилно екранира placeholders и добавя коментари с номер на ред автоматично. -- **Временни променливи (`$__`):** При генериране на PHP код по време на изпълнение, който се нуждае от временни променливи (напр. за съхраняване на междинни суми, броячи на цикли), използвайте конвенцията за префикс `$__`, за да избегнете конфликти с потребителски променливи и вътрешни променливи на Latte `$ʟ_`. -- **Влагане и уникални ID:** Ако вашият таг може да бъде вложен или се нуждае от състояние, специфично за инстанцията по време на изпълнение, използвайте `$context->generateId()` в рамките на вашия метод `print()`, за да създадете уникални суфикси за вашите временни променливи `$__`. -- **Providers за външни данни:** Използвайте providers (регистрирани чрез `Extension::getProviders()`) за достъп до данни или услуги по време на изпълнение ($this->global->...) вместо твърдо кодиране на стойности или разчитане на глобално състояние. Използвайте префикси на производителя за имената на providers. -- **Обмислете n:атрибути:** Ако вашият сдвоен таг логически оперира върху един HTML елемент, Latte вероятно предоставя автоматична поддръжка на `n:атрибут`. Имайте това предвид за удобство на потребителя. Ако създавате таг, модифициращ атрибут, обмислете дали чист `n:атрибут` е най-подходящата форма. -- **Тестване:** Пишете тестове за вашите тагове, покриващи както парсването на различни синтактични входове, така и коректността на изхода на генерирания **PHP код**. - -Като следвате тези насоки, можете да създавате мощни, здрави и поддържаеми персонализирани тагове, които се интегрират безпроблемно с шаблонния engine на Latte. - -.[note] -Изучаването на класовете на възлите, които са част от Latte, е най-добрият начин да научите всички подробности за процеса на парсване. diff --git a/latte/bg/develop.texy b/latte/bg/develop.texy deleted file mode 100644 index 038fb280d0..0000000000 --- a/latte/bg/develop.texy +++ /dev/null @@ -1,355 +0,0 @@ -Практики за разработка -********************** - - -Инсталация -========== - -Най-добрият начин да инсталирате Latte е с помощта на Composer: - -```shell -composer require latte/latte -``` - -Поддържани версии на PHP (важи за последните минорни версии на Latte): - -| версия | съвместима с PHP -|-----------------|------------------- -| Latte 3.0 | PHP 8.0 – 8.2 - - -Как да рендираме шаблон -======================= - -Как да рендираме шаблон? Достатъчен е този прост код: - -```php -$latte = new Latte\Engine; -// директория за кеша -$latte->setTempDirectory('/path/to/tempdir'); - -$params = [ /* променливи на шаблона */ ]; -// или $params = new TemplateParameters(/* ... */); - -// рендиране към изхода -$latte->render('template.latte', $params); -// рендиране в променлива -$output = $latte->renderToString('template.latte', $params); -``` - -Параметрите могат да бъдат масив или още по-добре [обект |#Параметри като клас], който ще осигури проверка на типовете и подсказване в редакторите. - -.[note] -Примери за употреба ще намерите също в хранилището [Latte examples |https://github.com/nette-examples/latte]. - - -Производителност и кеш -====================== - -Шаблоните в Latte са изключително бързи, Latte ги компилира директно в PHP код и ги съхранява в кеш на диска. По този начин те нямат никакви допълнителни разходи в сравнение с шаблони, написани на чист PHP. - -Кешът се регенерира автоматично всеки път, когато промените изходния файл. По време на разработката можете удобно да редактирате шаблоните в Latte и веднага да виждате промените в браузъра. Тази функция може да бъде изключена в продукционна среда, за да се спести малко производителност: - -```php -$latte->setAutoRefresh(false); -``` - -При разгръщане на продукционен сървър първоначалното генериране на кеша, особено при по-големи приложения, може разбира се да отнеме малко време. Latte има вградена превенция срещу "cache stampede":https://en.wikipedia.org/wiki/Cache_stampede. Това е ситуация, при която се събират по-голям брой едновременни заявки, които стартират Latte, и тъй като кешът все още не съществува, всички биха започнали да го генерират едновременно. Което би натоварило неимоверно сървъра. Latte е умен и при повече едновременни заявки генерира кеша само първата нишка, останалите чакат и след това го използват. - - -Параметри като клас -=================== - -По-добре от предаването на променливи към шаблона като масив е да създадете клас. Така ще получите [типово безопасен запис|type-system], [приятно подсказване в IDE |recipes#Редактори и IDE] и път за [регистрация на филтри |custom-filters#Филтри използващи клас с атрибути] и [функции |custom-functions#Функции използващи клас с атрибути]. - -```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, -)); -``` - - -Изключване на автоматичното екраниране на променлива -==================================================== - -Ако променливата съдържа низ в HTML, можете да я маркирате така, че Latte да не я екранира автоматично (и следователно двойно). Така ще избегнете нуждата да посочвате в шаблона `|noescape`. - -Най-лесният начин е да обвиете низа в обект `Latte\Runtime\Html`: - -```php -$params = [ - 'articleBody' => new Latte\Runtime\Html($article->htmlBody), -]; -``` - -Latte освен това не екранира всички обекти, които имплементират интерфейса `Latte\HtmlStringable`. Можете така да създадете собствен клас, чийто метод `__toString()` ще връща HTML код, който няма да се екранира автоматично: - -```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] -Методът `__toString` трябва да връща коректен HTML и да осигури екраниране на параметрите, иначе може да възникне уязвимост XSS! - - -Как да разширим Latte с филтри, тагове и т.н. -============================================= - -Как да добавим към Latte собствен филтър, функция, таг и т.н.? За това се говори в главата [разширяваме Latte |extending-latte]. Ако искате да използвате повторно своите модификации в различни проекти или да ги споделите с други, трябва да [създадете разширение |extending-latte#Latte Extension]. - - -Произволен код в шаблона `{php ...}` .{toc: RawPhpExtension} -============================================================ - -Вътре в тага [`{do}` |tags#do] могат да се записват само PHP изрази, не можете например да вмъкнете конструкции като `if ... else` или стейтмънти, завършващи с точка и запетая. - -Можете обаче да регистрирате разширението `RawPhpExtension`, което добавя тага `{php ...}`. С помощта на него може да се вмъква всякакъв PHP код. За него не важат никакви правила на sandbox режима, използването му е отговорност на автора на шаблона. - -```php -$latte->addExtension(new Latte\Essential\RawPhpExtension); -``` - - -Проверка на генерирания код .{data-version:3.0.7} -================================================= - -Latte компилира шаблоните в PHP код. Разбира се, той се грижи генерираният код да бъде синтактично валиден. Въпреки това, при използване на разширения от трети страни или `RawPhpExtension`, Latte не може да гарантира коректността на генерирания файл. Също така в PHP може да се запише код, който макар и синтактично правилен, е забранен (например присвояване на стойност на променливата `$this`) и причинява PHP Compile Error. Ако запишете такава операция в шаблона, тя ще попадне и в генерирания PHP код. Тъй като в PHP съществуват около двеста различни забранени операции, Latte няма амбицията да ги открива. За тях ще предупреди едва самият PHP при рендиране, което обикновено не пречи на нищо. - -Има обаче ситуации, когато искате да знаете още по време на компилацията на шаблона, че той не съдържа никакъв PHP Compile Error. Особено тогава, когато шаблоните могат да бъдат редактирани от потребители, или използвате [Sandbox]. В такъв случай оставете шаблоните да се проверяват още по време на компилацията. Тази функционалност се включва с метода `Engine::enablePhpLint()`. Тъй като за проверката е необходимо да се извика бинарният файл на PHP, предайте пътя до него като параметър: - -```php -$latte = new Latte\Engine; -$latte->enablePhpLinter('/path/to/php'); - -try { - $latte->compile('home.latte'); -} catch (Latte\CompileException $e) { - // улавя грешки в Latte, както и Compile Error в PHP - echo 'Error: ' . $e->getMessage(); -} -``` - - -Национална среда .{data-version:3.0.18}{toc: Locale} -==================================================== - -Latte позволява да се настрои национална среда, която влияе на форматирането на числа, дати и сортирането. Настройва се с помощта на метода `setLocale()`. Идентификаторът на средата се ръководи от стандарта IETF language tag, който използва разширението на PHP `intl`. Състои се от кода на езика и евентуално кода на страната, напр. `en_US` за английски в Съединените щати, `de_DE` за немски в Германия и т.н. - -```php -$latte = new Latte\Engine; -$latte->setLocale('bg_BG'); -``` - -Настройката на средата влияе на филтрите [localDate |filters#localDate], [sort |filters#sort], [number |filters#number] и [bytes |filters#bytes]. - -.[note] -Изисква PHP разширението `intl`. Настройката в Latte не влияе на глобалната настройка на locale в PHP. - - -Стриктен режим .{data-version:3.0.8} -==================================== - -В стриктен режим на парсиране Latte контролира дали не липсват затварящи HTML тагове и също забранява използването на променливата `$this`. Включвате го така: - -```php -$latte = new Latte\Engine; -$latte->setStrictParsing(); -``` - -Генерирането на шаблони с хедър `declare(strict_types=1)` включвате така: - -```php -$latte = new Latte\Engine; -$latte->setStrictTypes(); -``` - - -Превод в шаблони .{toc: TranslatorExtension} -============================================ - -С помощта на разширението `TranslatorExtension` добавяте към шаблона тагове [`{_...}` |tags#], [`{translate}` |tags#translate] и филтър [`translate` |filters#translate]. Служат за превод на стойности или части от шаблона на други езици. Като параметър посочваме метод (PHP callable), извършващ превода: - -```php -class MyTranslator -{ - public function __construct(private string $lang) - {} - - public function translate(string $original): string - { - // от $original създаваме $translated според $this->lang - return $translated; - } -} - -$translator = new MyTranslator($lang); -$extension = new Latte\Essential\TranslatorExtension( - $translator->translate(...), // [$translator, 'translate'] в PHP 8.0 -); -$latte->addExtension($extension); -``` - -Преводачът се извиква по време на изпълнение при рендиране на шаблона. Latte обаче може да превежда всички статични текстове още по време на компилацията на шаблона. Така се пести производителност, тъй като всеки низ се превежда само веднъж и резултатният превод се записва в компилираната форма. В директорията с кеша така възникват повече компилирани версии на шаблона, по една за всеки език. За това е достатъчно само да се посочи езикът като втори параметър: - -```php -$extension = new Latte\Essential\TranslatorExtension( - $translator->translate(...), - $lang, -); -``` - -Статичен текст означава например `{_'hello'}` или `{translate}hello{/translate}`. Нестатичните текстове, като например `{_$foo}`, ще продължат да се превеждат по време на изпълнение. - -На преводача могат от шаблона да се предават и допълнителни параметри с помощта на `{_$original, foo: bar}` или `{translate foo: bar}`, които той получава като масив `$params`: - -```php -public function translate(string $original, ...$params): string -{ - // $params['foo'] === 'bar' -} -``` - - -Дебъгване и Tracy -================= - -Latte се опитва да ви улесни разработката колкото е възможно повече. Директно за целите на дебъгването съществуват три тага [`{dump}` |tags#dump], [`{debugbreak}` |tags#debugbreak] и [`{trace}` |tags#trace]. - -Най-голям комфорт ще получите, ако още си инсталирате страхотния [инструмент за отстраняване на грешки Tracy|tracy:] и активирате добавката за Latte: - -```php -// включва Tracy -Tracy\Debugger::enable(); - -$latte = new Latte\Engine; -// активира разширението за Tracy -$latte->addExtension(new Latte\Bridges\Tracy\TracyExtension); -``` - -Сега всички грешки ще се показват в прегледен червен екран, включително грешките в шаблоните с подчертаване на ред и колона ([видео|https://github.com/nette/tracy/releases/tag/v2.9.0]). Същевременно в долния десен ъгъл в т.нар. Tracy Bar ще се появи раздел за Latte, където са прегледно показани всички рендирани шаблони и техните взаимни връзки (включително възможността да се кликне към шаблона или компилирания код) и също променливите: - -[* latte-debugging.webp *] - -Тъй като Latte компилира шаблоните в прегледен PHP код, можете удобно да ги стъпвате във вашето IDE. - - -Linter: валидиране на синтаксиса на шаблони .{toc: Linter} -========================================================== - -Да преминете през всички шаблони и да проверите дали не съдържат синтактични грешки, ще ви помогне инструментът Linter. Стартира се от конзолата: - -```shell -vendor/bin/latte-lint <път> -``` - -С параметъра `--strict` активирате [#стриктен режим]. - -Ако използвате собствени тагове, създайте си и собствена версия на Linter, напр. `custom-latte-lint`: - -```php -#!/usr/bin/env php -getEngine(); -// тук добавете вашите индивидуални разширения -$latte->addExtension(/* ... */); - -$ok = $linter->scanDirectory($path); -exit($ok ? 0 : 1); -``` - -Алтернативно можете да предадете собствен обект `Latte\Engine` на Linter: - -```php -$latte = new Latte\Engine; -// тук конфигурираме обекта $latte -$linter = new Latte\Tools\Linter(engine: $latte); -``` - - -Зареждане на шаблони от низ -=========================== - -Трябва ли ви да зареждате шаблони от низове вместо от файлове, например за целите на тестване? Ще ви помогне [StringLoader |loaders#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 -================= - -Можете да дефинирате собствен обслужващ handler за очаквани изключения. Ще му бъдат предадени изключенията, възникнали вътре в [`{try}` |tags#try] и в [sandbox|sandbox]. - -```php -$loggingHandler = function (Throwable $e, Latte\Runtime\Template $template) use ($logger) { - $logger->log($e); -}; - -$latte = new Latte\Engine; -$latte->setExceptionHandler($loggingHandler); -``` - - -Автоматично намиране на лейаут -============================== - -С помощта на тага [`{layout}` |template-inheritance#Наследяване на лейаут] шаблонът определя своя родителски шаблон. Възможно е също да се остави автоматичното намиране на лейаута, което ще опрости писането на шаблони, тъй като в тях няма да е необходимо да се посочва тагът `{layout}`. - -Това се постига по следния начин: - -```php -$finder = function (Latte\Runtime\Template $template) { - if (!$template->getReferenceType()) { - // връща пътя до файла с лейаута - return 'automatic.layout.latte'; - } -}; - -$latte = new Latte\Engine; -$latte->addProvider('coreParentFinder', $finder); -``` - -Ако шаблонът не трябва да има лейаут, той го обявява с тага `{layout none}`. diff --git a/latte/bg/extending-latte.texy b/latte/bg/extending-latte.texy deleted file mode 100644 index 4e1766f892..0000000000 --- a/latte/bg/extending-latte.texy +++ /dev/null @@ -1,227 +0,0 @@ -Разширяване на Latte -******************** - -.[perex] -Latte е проектиран с мисъл за разширяемост. Въпреки че стандартният му набор от тагове, филтри и функции покрива много случаи на употреба, често се налага да добавяте собствена специфична логика или помощни инструменти. Тази страница предоставя преглед на начините за разширяване на Latte, така че да отговаря перфектно на изискванията на вашия проект - от прости помощници до сложен нов синтаксис. - - -Начини за разширяване на Latte -============================== - -Ето бърз преглед на основните начини, по които можете да персонализирате и разширите Latte: - -- **[Потребителски филтри |Custom Filters]:** За форматиране или трансформиране на данни директно в изхода на шаблона (напр. `{$var|myFilter}`). Идеални за задачи като форматиране на дати, редактиране на текст или прилагане на специфично екраниране. Можете също да ги използвате за модифициране на по-големи блокове HTML съдържание, като обвиете съдържанието в анонимен [`{block}` |tags#block] и приложите към него потребителски филтър. -- **[Потребителски функции |Custom Functions]:** За добавяне на преизползваема логика, която може да бъде извикана в рамките на изрази в шаблона (напр. `{myFunction($arg1, $arg2)}`). Полезни за изчисления, достъп до помощни функции на приложението или генериране на малки части от съдържанието. -- **[Потребителски тагове |Custom Tags]:** За създаване на напълно нови езикови конструкции (`{mytag}...{/mytag}` или `n:mytag`). Таговете предлагат най-много възможности, позволяват дефиниране на собствени структури, контрол върху парсването на шаблона и имплементиране на сложна логика за рендиране. -- **[Компилационни преминавания |Compiler Passes]:** Функции, които модифицират абстрактното синтактично дърво (AST) на шаблона след парсване, но преди генериране на PHP код. Използват се за напреднали оптимизации, проверки за сигурност (като Sandbox) или автоматични модификации на кода. -- **[Потребителски зареждащи устройства |loaders]:** За промяна на начина, по който Latte търси и зарежда файлове с шаблони (напр. зареждане от база данни, криптирано хранилище и т.н.). - -Изборът на правилния метод за разширение е ключов. Преди да създадете сложен таг, помислете дали по-прост филтър или функция не биха били достатъчни. Нека го илюстрираме с пример: имплементиране на генератор *Lorem ipsum*, който приема като аргумент броя на думите за генериране. - -- **Като таг?** `{lipsum 40}` - Възможно е, но таговете са по-подходящи за контролни структури или генериране на сложни тагове. Таговете не могат да се използват директно в изрази. -- **Като филтър?** `{=40|lipsum}` - Технически работи, но филтрите са предназначени за *трансформиране* на входната стойност. Тук `40` е *аргумент*, а не стойност, която се трансформира. Това изглежда семантично неправилно. -- **Като функция?** `{lipsum(40)}` - Това е най-естественото решение! Функциите приемат аргументи и връщат стойности, което е идеално за използване във всеки израз: `{var $text = lipsum(40)}`. - -**Обща препоръка:** Използвайте функции за изчисления/генериране, филтри за трансформация и тагове за нови езикови конструкции или сложни тагове. Използвайте преминавания за манипулиране на AST и зареждащи устройства за извличане на шаблони. - - -Директна регистрация -==================== - -За помощни инструменти, специфични за проекта, или бързи разширения, Latte позволява директна регистрация на филтри и функции в обекта `Latte\Engine`. - -За да регистрирате филтър, използвайте метода `addFilter()`. Първият аргумент на вашата филтърна функция ще бъде стойността преди знака `|`, а следващите аргументи са тези, които се предават след двоеточието `:`. - -```php -$latte = new Latte\Engine; - -// Дефиниция на филтъра (извикващ се обект: функция, статичен метод и т.н.) -$myTruncate = fn(string $s, int $length = 50) => mb_substr($s, 0, $length); - -// Регистрация -$latte->addFilter('truncate', $myTruncate); - -// Използване в шаблона: {$text|truncate} или {$text|truncate:100} -``` - -Можете също да регистрирате **Filter Loader**, функция, която динамично предоставя извикващи се обекти на филтри според изискваното име: - -```php -$latte->addFilterLoader(fn(string $name) => /* връща извикващ се обект или null */); -``` - - -За да регистрирате функция, използваема в изрази на шаблона, използвайте `addFunction()`. - -```php -$latte = new Latte\Engine; - -// Дефиниция на функцията -$isWeekend = fn(DateTimeInterface $date) => $date->format('N') >= 6; - -// Регистрация -$latte->addFunction('isWeekend', $isWeekend); - -// Използване в шаблона: {if isWeekend($myDate)}Уикенд!{/if} -``` - -Повече информация ще намерите в секциите [Създаване на потребителски филтри |custom-filters] и [Функции |custom-functions]. - - -Надежден начин: Latte Extension .{toc: Latte Extension} -======================================================= - -Докато директната регистрация е проста, стандартният и препоръчителен начин за пакетиране и разпространение на разширения на Latte е чрез класове **Extension**. Extension служи като централна конфигурационна точка за регистрация на множество тагове, филтри, функции, компилационни преминавания и други елементи. - -Защо да използвате Extensions? - -- **Организация:** Поддържа свързаните разширения (тагове, филтри и т.н. за конкретна функция) заедно в един клас. -- **Преизползваемост и споделяне:** Лесно пакетирайте вашите разширения за използване в други проекти или за споделяне с общността (напр. чрез Composer). -- **Пълна мощ:** Потребителските тагове и компилационните преминавания *могат да се регистрират само* чрез Extensions. - - -Регистрация на Extension ------------------------- - -Extension се регистрира в Latte с помощта на метода `addExtension()` (или чрез [конфигурационен файл |application:configuration#Шаблони Latte]): - -```php -$latte = new Latte\Engine; -$latte->addExtension(new MyProjectExtension); -``` - -Ако регистрирате множество разширения и те дефинират тагове, филтри или функции с еднакви имена, предимство има последно добавеното разширение. Това също означава, че вашите разширения могат да презапишат нативните тагове/филтри/функции. - -Всеки път, когато направите промяна в класа и автоматичното обновяване не е изключено, Latte автоматично ще прекомпилира вашите шаблони. - - -Създаване на Extension ----------------------- - -За да създадете собствено разширение, трябва да създадете клас, който наследява от [api:Latte\Extension]. За да добиете представа как изглежда такова разширение, разгледайте вграденото "CoreExtension":https://github.com/nette/latte/blob/master/src/Latte/Essential/CoreExtension.php. - -Нека разгледаме методите, които можете да имплементирате: - - -beforeCompile(Latte\Engine $engine): void .[method] ---------------------------------------------------- - -Извиква се преди компилацията на шаблона. Методът може да се използва например за инициализации, свързани с компилацията. - - -getTags(): array .[method] --------------------------- - -Извиква се при компилация на шаблона. Връща асоциативен масив *име на таг => извикващ се обект*, което са функции за парсване на тагове. [Повече информация |custom-tags]. - -```php -public function getTags(): array -{ - return [ - 'foo' => FooNode::create(...), - 'bar' => BarNode::create(...), - 'n:baz' => NBazNode::create(...), - // ... - ]; -} -``` - -Тагът `n:baz` представлява чист [n:атрибут |syntax#n:атрибути], т.е. таг, който може да бъде записан само като атрибут. - -При таговете `foo` и `bar`, Latte автоматично разпознава дали са двойни тагове и ако да, могат автоматично да се записват с помощта на n:атрибути, включително варианти с префикси `n:inner-foo` и `n:tag-foo`. - -Редът на изпълнение на такива n:атрибути се определя от реда им в масива, върнат от метода `getTags()`. Така `n:foo` винаги се изпълнява преди `n:bar`, дори ако атрибутите в HTML тага са изброени в обратен ред като `
    `. - -Ако трябва да определите реда на n:атрибутите между няколко разширения, използвайте помощния метод `order()`, където параметърът `before` xor `after` определя кои тагове се сортират преди или след тага. - -```php -public function getTags(): array -{ - return [ - 'foo' => self::order(FooNode::create(...), before: 'bar')] - 'bar' => self::order(BarNode::create(...), after: ['block', 'snippet'])] - ]; -} -``` - - -getPasses(): array .[method] ----------------------------- - -Извиква се при компилация на шаблона. Връща асоциативен масив *име на преминаване => извикващ се обект*, което са функции, представляващи т.нар. [компилационни преминавания |compiler-passes], които преминават и модифицират AST. - -Тук също може да се използва помощният метод `order()`. Стойността на параметрите `before` или `after` може да бъде `*` със значение преди/след всички. - -```php -public function getPasses(): array -{ - return [ - 'optimize' => Passes::optimizePass(...), - 'sandbox' => self::order($this->sandboxPass(...), before: '*'), - // ... - ]; -} -``` - - -beforeRender(Latte\Engine $engine): void .[method] --------------------------------------------------- - -Извиква се преди всяко рендиране на шаблона. Методът може да се използва например за инициализиране на променливи, използвани по време на рендирането. - - -getFilters(): array .[method] ------------------------------ - -Извиква се преди рендиране на шаблона. Връща филтри като асоциативен масив *име на филтър => извикващ се обект*. [Повече информация |custom-filters]. - -```php -public function getFilters(): array -{ - return [ - 'batch' => $this->batchFilter(...), - 'trim' => $this->trimFilter(...), - // ... - ]; -} -``` - - -getFunctions(): array .[method] -------------------------------- - -Извиква се преди рендиране на шаблона. Връща функции като асоциативен масив *име на функция => извикващ се обект*. [Повече информация |custom-functions]. - -```php -public function getFunctions(): array -{ - return [ - 'clamp' => $this->clampFunction(...), - 'divisibleBy' => $this->divisibleByFunction(...), - // ... - ]; -} -``` - - -getProviders(): array .[method] -------------------------------- - -Извиква се преди рендиране на шаблона. Връща масив от providers, които обикновено са обекти, използвани от таговете по време на изпълнение. Достъпват се чрез `$this->global->...`. [Повече информация |custom-tags#Представяне на Providers]. - -```php -public function getProviders(): array -{ - return [ - 'myFoo' => $this->foo, - 'myBar' => $this->bar, - // ... - ]; -} -``` - - -getCacheKey(Latte\Engine $engine): mixed .[method] --------------------------------------------------- - -Извиква се преди рендиране на шаблона. Върнатата стойност става част от ключа, чийто хеш се съдържа в името на файла на компилирания шаблон. Следователно за различни върнати стойности Latte ще генерира различни кеш файлове. diff --git a/latte/bg/filters.texy b/latte/bg/filters.texy deleted file mode 100644 index 00971a7e45..0000000000 --- a/latte/bg/filters.texy +++ /dev/null @@ -1,873 +0,0 @@ -Latte филтри -************ - -.[perex] -В шаблоните можем да използваме функции, които помагат за модифициране или преформатиране на данните в окончателния им вид. Наричаме ги *филтри*. - -.[table-latte-filters] -|## Трансформация -| `batch` | [извеждане на линейни данни в таблица |#batch] -| `breakLines` | [Добавя HTML нов ред преди края на реда |#breakLines] -| `bytes` | [форматира размер в байтове |#bytes] -| `clamp` | [ограничава стойността в даден диапазон |#clamp] -| `dataStream` | [конверсия за Data URI протокол |#dataStream] -| `date` | [форматира дата и час |#date] -| `explode` | [разделя низ на масив по разделител |#explode] -| `first` | [връща първия елемент на масив или знак от низ |#first] -| `group` | [групира данни по различни критерии |#group] -| `implode` | [свързва масив в низ |#implode] -| `indent` | [индентира текст отляво с даден брой табулации |#indent] -| `join` | [свързва масив в низ |#implode] -| `last` | [връща последния елемент на масив или знак от низ |#last] -| `length` | [връща дължината на низ в знаци или масив |#length] -| `localDate` | [форматира дата и час според локализацията |#localDate] -| `number` | [форматира число |#number] -| `padLeft` | [допълва низ отляво до желаната дължина |#padLeft] -| `padRight` | [допълва низ отдясно до желаната дължина |#padRight] -| `random` | [връща случаен елемент от масив или знак от низ |#random] -| `repeat` | [повторение на низ |#repeat] -| `replace` | [заменя срещанията на търсения низ |#replace] -| `replaceRE` | [заменя срещанията според регулярен израз |#replaceRE] -| `reverse` | [обръща UTF‑8 низ или масив |#reverse] -| `slice` | [извлича част от масив или низ |#slice] -| `sort` | [сортира масив |#sort] -| `spaceless` | [премахва празно пространство |#spaceless], подобно на тага [spaceless |tags] -| `split` | [разделя низ на масив по разделител |#explode] -| `strip` | [премахва празно пространство |#spaceless] -| `stripHtml` | [премахва HTML тагове и преобразува HTML ентити в знаци |#stripHtml] -| `substr` | [връща част от низ |#substr] -| `trim` | [премахва начални и крайни интервали или други знаци |#trim] -| `translate` | [превод на други езици |#translate] -| `truncate` | [скъсява дължината със запазване на думи |#truncate] -| `webalize` | [модифицира UTF‑8 низ във форма, използвана в URL |#webalize] - -.[table-latte-filters] -|## Регистър на буквите -| `capitalize` | [малки букви, първата буква на думите е главна |#capitalize] -| `firstUpper` | [преобразува първата буква в главна |#firstUpper] -| `lower` | [преобразува в малки букви |#lower] -| `upper` | [преобразува в главни букви |#upper] - -.[table-latte-filters] -|## Закръгляване -| `ceil` | [закръгля число нагоре до дадена точност |#ceil] -| `floor` | [закръгля число надолу до дадена точност |#floor] -| `round` | [закръгля число до дадена точност |#round] - -.[table-latte-filters] -|## Екраниране -| `escapeUrl` | [екранира параметър в URL |#escapeUrl] -| `noescape` | [извежда променлива без екраниране |#noescape] -| `query` | [генерира query string в URL |#query] - -Освен това съществуват филтри за екраниране за HTML (`escapeHtml` и `escapeHtmlComment`), XML (`escapeXml`), JavaScript (`escapeJs`), CSS (`escapeCss`) и iCalendar (`escapeICal`), които Latte използва самостоятелно благодарение на [контекстно-чувствително екраниране |safety-first#Контекстно-чувствително екраниране] и не е необходимо да ги записвате. - -.[table-latte-filters] -|## Сигурност -| `checkUrl` | [обработва URL адрес срещу опасни входове |#checkUrl] -| `nocheck` | [предотвратява автоматичната обработка на URL адреса |#nocheck] - -Latte атрибутите `src` и `href` [проверява автоматично |safety-first#Проверка на връзки], така че филтърът `checkUrl` почти не е необходимо да се използва. - - -.[note] -Всички филтри по подразбиране са предназначени за низове в кодировка UTF‑8. - - -Използване -========== - -Филтрите се записват след вертикална черта (може да има интервал преди нея): - -```latte -

    {$heading|upper}

    -``` - -Филтрите (в по-стари версии помощници) могат да бъдат верижно свързани и след това се прилагат в реда отляво надясно: - -```latte -

    {$heading|lower|capitalize}

    -``` - -Параметрите се задават след името на филтъра, разделени с двоеточия или запетаи: - -```latte -

    {$heading|truncate:20,''}

    -``` - -Филтрите могат да се прилагат и към израз: - -```latte -{var $name = ($title|upper) . ($subtitle|lower)} -``` - -[Потребителски филтри|custom-filters] могат да се регистрират по следния начин: - -```php -$latte = new Latte\Engine; -$latte->addFilter('shortify', fn(string $s, int $len = 10) => mb_substr($s, 0, $len)); -``` - -В шаблона след това се извиква така: - -```latte -

    {$text|shortify}

    -

    {$text|shortify:100}

    -``` - - -Филтри -====== - - -batch(int $length, mixed $item): array .[filter] ------------------------------------------------- -Филтър, който опростява извеждането на линейни данни във вид на таблица. Връща масив от масиви със зададения брой елементи. Ако зададете втори параметър, той ще се използва за допълване на липсващите елементи на последния ред. - -```latte -{var $items = ['a', 'b', 'c', 'd', 'e']} - -{foreach ($items|batch: 3, 'No item') as $row} - - {foreach $row as $column} - - {/foreach} - -{/foreach} -
    {$column}
    -``` - -Извежда: - -```latte - - - - - - - - - - - -
    abc
    deNo item
    -``` - -Вижте също [#group] и тага [iterateWhile |tags#iterateWhile]. - - -breakLines .[filter] --------------------- -Добавя HTML таг `
    ` преди всеки знак за нов ред. - -```latte -{var $s = "Text & with \n newline"} -{$s|breakLines} {* извежда "Text & with
    \n newline" *} -``` - - -bytes(int $precision=2) .[filter] ---------------------------------- -Форматира размера в байтове в четим за човека вид. Ако е зададена [локализация |develop#Locale], ще се използват съответните разделители за десетични знаци и хиляди. - -```latte -{$size|bytes} {* 0 B, 1.25 GB, … *} -{$size|bytes:0} {* 10 B, 1 GB, … *} -``` - - -ceil(int $precision=0) .[filter] --------------------------------- -Закръгля число нагоре до дадена точност. - -```latte -{=3.4|ceil} {* извежда 4 *} -{=135.22|ceil:1} {* извежда 135.3 *} -{=135.22|ceil:3} {* извежда 135.22 *} -``` - -Вижте също [#floor], [#round]. - - -capitalize .[filter] --------------------- -Думите ще започват с главни букви, всички останали знаци ще бъдат малки. Изисква PHP разширението `mbstring`. - -```latte -{='i like LATTE'|capitalize} {* извежда 'I Like Latte' *} -``` - -Вижте също [#firstUpper], [#lower], [#upper]. - - -checkUrl .[filter] ------------------- -Принуждава обработката на URL адреса. Проверява дали променливата съдържа уеб URL (т.е. протокол HTTP/HTTPS) и предотвратява извеждането на връзки, които могат да представляват риск за сигурността. - -```latte -{var $link = 'javascript:window.close()'} -контролирано -неконтролирано -``` - -Извежда: - -```latte -контролирано -неконтролирано -``` - -Вижте също [#nocheck]. - - -clamp(int|float $min, int|float $max) .[filter] ------------------------------------------------ -Ограничава стойността в дадения инклузивен диапазон min и max. - -```latte -{$level|clamp: 0, 255} -``` - -Съществува и като [функция |functions#clamp]. - - -dataStream(string $mimetype=detect) .[filter] ---------------------------------------------- -Конвертира съдържанието в data URI scheme. С негова помощ могат да се вмъкват изображения в HTML или CSS без необходимост от свързване на външни файлове. - -Нека имаме изображение в променливата `$img = Image::fromFile('obrazek.gif')`, тогава - -```latte - -``` - -Извежда например: - -```latte - -``` - -.[caution] -Изисква PHP разширението `fileinfo`. - - -date(string $format) .[filter] ------------------------------- -Форматира дата и час според маската, използвана от PHP функцията [php:date]. Филтърът приема дата във формат UNIX timestamp, като низ или обект от тип `DateTimeInterface`. - -```latte -{$today|date:'j. n. Y'} -``` - -Вижте също [#localDate]. - - -escapeUrl .[filter] -------------------- -Екранира променлива за използване като параметър в URL. - -```latte -{$name} -``` - -Вижте също [#query]. - - -explode(string $separator='') .[filter] ---------------------------------------- -Разделя низ на масив по разделител. Псевдоним за `split`. - -```latte -{='one,two,three'|explode:','} {* връща ['one', 'two', 'three'] *} -``` - -Ако разделителят е празен низ (стойност по подразбиране), входът ще бъде разделен на отделни знаци: - -```latte -{='123'|explode} {* връща ['1', '2', '3'] *} -``` - -Можете също да използвате псевдонима `split`: - -```latte -{='1,2,3'|split:','} {* връща ['1', '2', '3'] *} -``` - -Вижте също [#implode]. - - -first .[filter] ---------------- -Връща първия елемент на масив или знак от низ: - -```latte -{=[1, 2, 3, 4]|first} {* извежда 1 *} -{='abcd'|first} {* извежда 'a' *} -``` - -Вижте също [#last], [#random]. - - -floor(int $precision=0) .[filter] ---------------------------------- -Закръгля число надолу до дадена точност. - -```latte -{=3.5|floor} {* извежда 3 *} -{=135.79|floor:1} {* извежда 135.7 *} -{=135.79|floor:3} {* извежда 135.79 *} -``` - -Вижте също [#ceil], [#round]. - - -firstUpper .[filter] --------------------- -Преобразува първата буква в главна. Изисква PHP разширението `mbstring`. - -```latte -{='the latte'|firstUpper} {* извежда 'The latte' *} -``` - -Вижте също [#capitalize], [#lower], [#upper]. - - -group(string|int|\Closure $by): array .[filter]{data-version:3.0.16} --------------------------------------------------------------------- -Филтърът групира данни по различни критерии. - -В този пример редовете в таблицата се групират по колона `categoryId`. Изходът е масив от масиви, където ключът е стойността в колоната `categoryId`. [Прочетете подробно ръководство|cookbook/grouping]. - -```latte -{foreach ($items|group: categoryId) as $categoryId => $categoryItems} -
      - {foreach $categoryItems as $item} -
    • {$item->name}
    • - {/foreach} -
    -{/foreach} -``` - -Вижте също [#batch], функцията [group |functions#group] и тага [iterateWhile |tags#iterateWhile]. - - -implode(string $glue='') .[filter] ----------------------------------- -Връща низ, който е конкатенация на елементите на последователност. Псевдоним за `join`. - -```latte -{=[1, 2, 3]|implode} {* извежда '123' *} -{=[1, 2, 3]|implode:'|'} {* извежда '1|2|3' *} -``` - -Можете също да използвате псевдонима `join`: - -```latte -{=[1, 2, 3]|join} {* извежда '123' *} -``` - - -indent(int $level=1, string $char="\t") .[filter] -------------------------------------------------- -Индентира текст отляво с даден брой табулации или други знаци, които можем да посочим във втория аргумент. Празните редове не се индентират. - -```latte -
    -{block |indent} -

    Hello

    -{/block} -
    -``` - -Извежда: - -```latte -
    -

    Hello

    -
    -``` - - -last .[filter] --------------- -Връща последния елемент на масив или знак от низ: - -```latte -{=[1, 2, 3, 4]|last} {* извежда 4 *} -{='abcd'|last} {* извежда 'd' *} -``` - -Вижте също [#first], [#random]. - - -length .[filter] ----------------- -Връща дължината на низ или масив. - -- за низове връща дължината в UTF‑8 знаци -- за масиви връща броя на елементите -- за обекти, които имплементират интерфейса `Countable`, използва върнатата стойност на метода `count()` -- за обекти, които имплементират интерфейса `IteratorAggregate`, използва върнатата стойност на функцията `iterator_count()` - - -```latte -{if ($users|length) > 10} - ... -{/if} -``` - - -localDate(?string $format=null, ?string $date=null, ?string $time=null) .[filter] ---------------------------------------------------------------------------------- -Форматира дата и час според [локализация |develop#Locale], което осигурява последователно и локализирано показване на времеви данни в различни езици и региони. Филтърът приема дата като UNIX timestamp, низ или обект от тип `DateTimeInterface`. - -```latte -{$date|localDate} {* 15 април 2024 *} -{$date|localDate: format: yM} {* 4/2024 *} -{$date|localDate: date: medium} {* 15.4.2024 *} -``` - -Ако използвате филтъра без параметри, датата ще се изведе на ниво `long`, вижте по-нататък. - -**а) използване на формат** - -Параметърът `format` описва кои времеви компоненти да се покажат. За тях се използват буквени кодове, чийто брой повторения влияе на ширината на изхода: - -| година | `y` / `yy` / `yyyy` | `2024` / `24` / `2024` -| месец | `M` / `MM` / `MMM` / `MMMM` | `8` / `08` / `авг` / `август` -| ден | `d` / `dd` / `E` / `EEEE` | `1` / `01` / `нд` / `неделя` -| час | `j` / `H` / `h` | предпочитан / 24-часов / 12-часов -| минута | `m` / `mm` | `5` / `05` (2 цифри в комбинация със секунди) -| секунда | `s` / `ss` | `8` / `08` (2 цифри в комбинация с минути) - -Редът на кодовете във формата няма значение, тъй като редът на компонентите се извежда според обичаите на локализацията. Следователно форматът е независим от нея. Например форматът `yyyyMMMMd` в среда `en_US` ще изведе `April 15, 2024`, докато в среда `bg_BG` ще изведе `15 април 2024`: - -| locale: | bg_BG | en_US -|--- -| `format: 'dMy'` | 10.8.2024 г. | 8/10/2024 -| `format: 'yM'` | 8.2024 г. | 8/2024 -| `format: 'yyyyMMMM'` | август 2024 г. | August 2024 -| `format: 'MMMM'` | август | August -| `format: 'jm'` | 17:22 | 5:22 PM -| `format: 'Hm'` | 17:22 | 17:22 -| `format: 'hm'` | 5:22 сл. об. | 5:22 PM - - -**б) използване на предварително зададени стилове** - -Параметрите `date` и `time` определят колко подробно да се изведат датата и часът. Можете да избирате от няколко нива: `full`, `long`, `medium`, `short`. Може да се изведе само датата, само часът или и двете: - -| locale: | bg_BG | en_US -|--- -| `date: short` | 23.01.78 г. | 1/23/78 -| `date: medium` | 23.01.1978 г. | Jan 23, 1978 -| `date: long` | 23 януари 1978 г. | January 23, 1978 -| `date: full` | понеделник, 23 януари 1978 г. | Monday, January 23, 1978 -| `time: short` | 8:30 | 8:30 AM -| `time: medium` | 8:30:59 | 8:30:59 AM -| `time: long` | 8:30:59 Гринуич+1 | 8:30:59 AM GMT+1 -| `date: short, time: short` | 23.01.78 г., 8:30 | 1/23/78, 8:30 AM -| `date: medium, time: short` | 23.01.1978 г., 8:30 | Jan 23, 1978, 8:30 AM -| `date: long, time: short` | 23 януари 1978 г. в 8:30 | January 23, 1978 at 8:30 AM - -При датата може допълнително да се използва префикс `relative-` (напр. `relative-short`), който за дати, близки до настоящия момент, ще покаже `вчера`, `днес` или `утре`, иначе ще се изведе по стандартния начин. - -```latte -{$date|localDate: date: relative-short} {* вчера *} -``` - -Вижте също [#date]. - - -lower .[filter] ---------------- -Преобразува низ в малки букви. Изисква PHP разширението `mbstring`. - -```latte -{='LATTE'|lower} {* извежда 'latte' *} -``` - -Вижте също [#capitalize], [#firstUpper], [#upper]. - - -nocheck .[filter] ------------------ -Предотвратява автоматичната обработка на URL адреса. Latte [автоматично проверява |safety-first#Проверка на връзки], дали променливата съдържа уеб URL (т.е. протокол HTTP/HTTPS) и предотвратява извеждането на връзки, които могат да представляват риск за сигурността. - -Ако връзката използва друга схема, напр. `javascript:` или `data:`, и сте сигурни в съдържанието й, можете да изключите проверката с помощта на `|nocheck`. - -```latte -{var $link = 'javascript:window.close()'} - -контролирано -неконтролирано -``` - -Извежда: - -```latte -контролирано -неконтролирано -``` - -Вижте също [#checkUrl]. - - -noescape .[filter] ------------------- -Забранява автоматичното екраниране. - -```latte -{var $trustedHtmlString = 'hello'} -Екранирано: {$trustedHtmlString} -Неекранирано: {$trustedHtmlString|noescape} -``` - -Извежда: - -```latte -Екранирано: <b>hello</b> -Неекранирано: hello -``` - -.[warning] -Неправилното използване на филтъра `noescape` може да доведе до уязвимост XSS! Никога не го използвайте, ако не сте **напълно сигурни** какво правите и че извежданият низ идва от надежден източник. - - -number(int $decimals=0, string $decPoint='.', string $thousandsSep=',') .[filter] ---------------------------------------------------------------------------------- -Форматира число до определен брой десетични знаци. Ако е зададена [локализация |develop#Locale], ще се използват съответните разделители за десетични знаци и хиляди. - -```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 *} -``` - - -number(string $format) .[filter] --------------------------------- -Параметърът `format` позволява да дефинирате вида на числата точно според вашите нужди. За това е необходимо да имате настроена [локализация |develop#Locale]. Форматът се състои от няколко специални знака, чието пълно описание ще намерите в документацията "DecimalFormat":https://unicode.org/reports/tr35/tr35-numbers.html#Number_Format_Patterns: - -- `0` задължителна цифра, винаги се показва, дори ако е нула -- `#` незадължителна цифра, показва се само ако на това място числото действително съществува -- `@` значеща цифра, помага да се покаже число с определен брой значещи цифри -- `.` показва къде трябва да бъде десетичната запетая (или точка, според държавата) -- `,` служи за разделяне на групи цифри, най-често хиляди -- `%` числото се умножава по 100× и се добавя знак за процент - -Нека разгледаме примери. В първия пример два десетични знака са задължителни, във втория - незадължителни. Третият пример показва допълване с нули отляво и отдясно, четвъртият показва само съществуващите цифри: - -```latte -{1234.5|number: '#,##0.00'} {* 1,234.50 *} -{1234.5|number: '#,##0.##'} {* 1,234.5 *} -{1.23 |number: '000.000'} {* 001.230 *} -{1.2 |number: '##.##'} {* 1.2 *} -``` - -Значещите цифри определят колко цифри, независимо от десетичната запетая, трябва да бъдат показани, като се закръгля: - -```latte -{1234|number: '@@'} {* 1200 *} -{1234|number: '@@@'} {* 1230 *} -{1234|number: '@@@#'} {* 1234 *} -{1.2345|number: '@@@'} {* 1.23 *} -{0.00123|number: '@@'} {* 0.0012 *} -``` - -Лесен начин да покажете число като процент. Числото се умножава по 100× и се добавя знак `%`: - -```latte -{0.1234|number: '#.##%'} {* 12.34% *} -``` - -Можем да дефинираме различен формат за положителни и отрицателни числа, разделени със знака `;`. По този начин може например да се настрои положителните числа да се показват със знак `+`: - -```latte -{42|number: '#.##;(#.##)'} {* 42 *} -{-42|number: '#.##;(#.##)'} {* (42) *} -{42|number: '+#.##;-#.##'} {* +42 *} -{-42|number: '+#.##;-#.##'} {* -42 *} -``` - -Помнете, че действителният вид на числата може да се различава според настройките на държавата. Например в някои държави се използва запетая вместо точка като разделител на десетичните знаци. Този филтър автоматично взема това предвид и не е нужно да се притеснявате за нищо. - - -padLeft(int $length, string $pad=' ') .[filter] ------------------------------------------------ -Допълва низ до определена дължина с друг низ отляво. - -```latte -{='hello'|padLeft: 10, '123'} {* извежда '12312hello' *} -``` - - -padRight(int $length, string $pad=' ') .[filter] ------------------------------------------------- -Допълва низ до определена дължина с друг низ отдясно. - -```latte -{='hello'|padRight: 10, '123'} {* извежда 'hello12312' *} -``` - - -query .[filter] ---------------- -Динамично генерира query string в URL: - -```latte -click -search -``` - -Извежда: - -```latte -click -search -``` - -Ключове със стойност `null` се пропускат. - -Вижте също [#escapeUrl]. - - -random .[filter] ----------------- -Връща случаен елемент от масив или знак от низ: - -```latte -{=[1, 2, 3, 4]|random} {* извежда напр.: 3 *} -{='abcd'|random} {* извежда напр.: 'b' *} -``` - -Вижте също [#first], [#last]. - - -repeat(int $count) .[filter] ----------------------------- -Повтаря низ x пъти. - -```latte -{='hello'|repeat: 3} {* извежда 'hellohellohello' *} -``` - - -replace(string|array $search, string $replace='') .[filter] ------------------------------------------------------------ -Заменя всички срещания на търсения низ със заместващ низ. - -```latte -{='hello world'|replace: 'world', 'friend'} {* извежда 'hello friend' *} -``` - -Могат да се извършат и няколко замени едновременно: - -```latte -{='hello world'|replace: [h => l, l => h]} {* извежда 'lehho worhd' *} -``` - - -replaceRE(string $pattern, string $replace='') .[filter] --------------------------------------------------------- -Извършва търсене с регулярни изрази със замяна. - -```latte -{='hello world'|replaceRE: '/l.*/', 'l'} {* извежда 'hel' *} -``` - - -reverse .[filter] ------------------ -Обръща дадения низ или масив. - -```latte -{var $s = 'Nette'} -{$s|reverse} {* извежда 'etteN' *} -{var $a = ['N', 'e', 't', 't', 'e']} -{$a|reverse} {* връща ['e', 't', 't', 'e', 'N'] *} -``` - - -round(int $precision=0) .[filter] ---------------------------------- -Закръгля число до дадена точност. - -```latte -{=3.4|round} {* извежда 3 *} -{=3.5|round} {* извежда 4 *} -{=135.79|round:1} {* извежда 135.8 *} -{=135.79|round:3} {* извежда 135.79 *} -``` - -Вижте също [#ceil], [#floor]. - - -slice(int $start, ?int $length=null, bool $preserveKeys=false) .[filter] ------------------------------------------------------------------------- -Извлича част от масив или низ. - -```latte -{='hello'|slice: 1, 2} {* извежда 'el' *} -{=['a', 'b', 'c']|slice: 1, 2} {* извежда ['b', 'c'] *} -``` - -Филтърът работи като PHP функцията `array_slice` за масиви или `mb_substr` за низове с резервен вариант към функцията `iconv_substr` в режим UTF‑8. - -Ако `$start` е положителен, последователността ще започне изместена с този брой от началото на масива/низа. Ако е отрицателен, последователността ще започне изместена с толкова от края. - -Ако е зададен параметър `$length` и е положителен, последователността ще съдържа толкова елементи. Ако в тази функция се предаде отрицателен параметър `$length`, последователността ще съдържа всички елементи на оригиналния масив, започвайки от позиция `$start` и завършвайки на позиция, по-малка с `$length` елементи от края на масива. Ако не зададете този параметър, последователността ще съдържа всички елементи на оригиналния масив, започвайки от позиция `$start`. - -По подразбиране филтърът променя реда и нулира целочислените ключове на масива. Това поведение може да се промени, като се зададе `$preserveKeys` на `true`. Низовите ключове винаги се запазват, независимо от този параметър. - - -sort(?Closure $comparison, string|int|\Closure|null $by=null, string|int|\Closure|bool $byKey=false) .[filter] --------------------------------------------------------------------------------------------------------------- -Филтърът сортира елементите на масив или итератор и запазва техните асоциативни ключове. При зададена [локализация |develop#Locale] сортирането се ръководи от нейните правила, освен ако не е специфицирана собствена функция за сравнение. - -```latte -{foreach ($names|sort) as $name} - ... -{/foreach} -``` - -Сортиран масив в обратен ред: - -```latte -{foreach ($names|sort|reverse) as $name} - ... -{/foreach} -``` - -Можете да специфицирате собствена функция за сравнение за сортиране (примерът показва как да обърнете сортирането от най-голямо към най-малко): - -```latte -{var $reverted = ($names|sort: fn($a, $b) => $b <=> $a)} -``` - -Филтърът `|sort` също позволява сортиране на елементи по ключове: - -```latte -{foreach ($names|sort: byKey: true) as $name} - ... -{/foreach} -``` - -Ако трябва да сортирате таблица по конкретна колона, можете да използвате параметъра `by`. Стойността `'name'` в примера указва, че ще се сортира по `$item->name` или `$item['name']`, в зависимост от това дали `$item` е масив или обект: - -```latte -{foreach ($items|sort: by: 'name') as $item} - {$item->name} -{/foreach} -``` - -Можете също да дефинирате callback функция, която да определи стойността, по която да се сортира: - -```latte -{foreach ($items|sort: by: fn($items) => $items->category->name) as $item} - {$item->name} -{/foreach} -``` - -По същия начин може да се използва и параметърът `byKey`. - - -spaceless .[filter] -------------------- -Премахва излишното празно пространство (интервали) от изхода. Можете също да използвате псевдонима `strip`. - -```latte -{block |spaceless} -
      -
    • Hello
    • -
    -{/block} -``` - -Извежда: - -```latte -
    • Hello
    -``` - - -stripHtml .[filter] -------------------- -Преобразува HTML в чист текст. Тоест премахва от него HTML таговете и преобразува HTML ентитите в текст. - -```latte -{='

    one < two

    '|stripHtml} {* извежда 'one < two' *} -``` - -Полученият чист текст може естествено да съдържа знаци, които представляват HTML тагове, например `'<p>'|stripHtml` се преобразува в `

    `. В никакъв случай не извеждайте така получения текст с `|noescape`, защото това може да доведе до създаване на дупка в сигурността. - - -substr(int $offset, ?int $length=null) .[filter] ------------------------------------------------- -Извлича част от низ. Този филтър е заменен с филтъра [#slice]. - -```latte -{$string|substr: 1, 2} -``` - - -translate(...$args) .[filter] ------------------------------ -Превежда изрази на други езици. За да бъде филтърът наличен, е необходимо да [настроите преводач |develop#TranslatorExtension]. Можете също да използвате [тагове за превод |tags#Преводи]. - -```latte -{='Кошница'|translate} -{$item|translate} -``` - - -trim(string $charlist=" \t\n\r\0\x0B\u{A0}") .[filter] ------------------------------------------------------- -Премахва празни знаци (или други знаци) от началото и края на низа. - -```latte -{=' I like Latte. '|trim} {* извежда 'I like Latte.' *} -{=' I like Latte.'|trim: '.'} {* извежда ' I like Latte' *} -``` - - -truncate(int $length, string $append='…') .[filter] ---------------------------------------------------- -Подрязва низ до посочената максимална дължина, като се опитва да запази цели думи. Ако низът бъде скъсен, накрая добавя три точки (може да се промени с втория параметър). - -```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] ---------------- -Преобразува низ в главни букви. Изисква PHP разширението `mbstring`. - -```latte -{='latte'|upper} {* извежда 'LATTE' *} -``` - -Вижте също [#capitalize], [#firstUpper], [#lower]. - - -webalize .[filter] ------------------- -Модифицира UTF‑8 низ във форма, използвана в URL. - -Преобразува се в ASCII. Преобразува интервалите в тирета. Премахва знаци, които не са буквено-цифрови, долни черти или тирета. Преобразува в малки букви. Също така премахва начални и крайни интервали. - -```latte -{var $s = 'Нашият 10-ти продукт'} -{$s|webalize} {* извежда 'nashiyat-10-ti-produkt' *} -``` - -.[caution] -Изисква библиотеката [nette/utils|utils:]. diff --git a/latte/bg/functions.texy b/latte/bg/functions.texy deleted file mode 100644 index aff39d32f7..0000000000 --- a/latte/bg/functions.texy +++ /dev/null @@ -1,156 +0,0 @@ -Latte функции -************* - -.[perex] -В шаблоните, освен обикновените PHP функции, можем да използваме и следните допълнителни функции. - -.[table-latte-filters] -| `clamp` | [ограничава стойността в даден диапазон |#clamp] -| `divisibleBy`| [проверява дали променливата се дели на число |#divisibleBy] -| `even` | [проверява дали даденото число е четно |#even] -| `first` | [връща първия елемент на масив или знак от низ |#first] -| `group` | [групира данни по различни критерии |#group] -| `hasBlock` | [проверява съществуването на блок |#hasBlock] -| `last` | [връща последния елемент на масив или знак от низ |#last] -| `odd` | [проверява дали даденото число е нечетно |#odd] -| `slice` | [извлича част от масив или низ |#slice] - - -Използване -========== - -Функциите се използват по същия начин като обикновените PHP функции и могат да се използват във всички изрази: - -```latte -

    {clamp($num, 1, 100)}

    - -{if odd($num)} ... {/if} -``` - -[Потребителски функции|custom-functions] могат да се регистрират по следния начин: - -```php -$latte = new Latte\Engine; -$latte->addFunction('shortify', fn(string $s, int $len = 10) => mb_substr($s, 0, $len)); -``` - -В шаблона след това се извиква така: - -```latte -

    {shortify($text)}

    -

    {shortify($text, 100)}

    -``` - - -Функции -======= - - -clamp(int|float $value, int|float $min, int|float $max): int|float .[method] ----------------------------------------------------------------------------- -Ограничава стойността в дадения инклузивен диапазон min и max. - -```latte -{=clamp($level, 0, 255)} -``` - -Вижте също [филтър clamp |filters#clamp]. - - -divisibleBy(int $value, int $by): bool .[method] ------------------------------------------------- -Проверява дали променливата се дели на число. - -```latte -{if divisibleBy($num, 5)} ... {/if} -``` - - -even(int $value): bool .[method] --------------------------------- -Проверява дали даденото число е четно. - -```latte -{if even($num)} ... {/if} -``` - - -first(string|iterable $value): mixed .[method] ----------------------------------------------- -Връща първия елемент на масив или знак от низ: - -```latte -{=first([1, 2, 3, 4])} {* извежда 1 *} -{=first('abcd')} {* извежда 'a' *} -``` - -Вижте също [#last], [филтър first |filters#first]. - - -group(iterable $data, string|int|\Closure $by): array .[method]{data-version:3.0.16} ------------------------------------------------------------------------------------- -Функцията групира данни по различни критерии. - -В този пример редовете в таблицата се групират по колона `categoryId`. Изходът е масив от масиви, където ключът е стойността в колоната `categoryId`. [Прочетете подробно ръководство|cookbook/grouping]. - -```latte -{foreach group($items, categoryId) as $categoryId => $categoryItems} -
      - {foreach $categoryItems as $item} -
    • {$item->name}
    • - {/foreach} -
    -{/foreach} -``` - -Вижте също филтъра [group |filters#group]. - - -hasBlock(string $name): bool .[method]{data-version:3.0.10} ------------------------------------------------------------ -Проверява дали блок с посоченото име съществува: - -```latte -{if hasBlock(header)} ... {/if} -``` - -Вижте също [проверка за съществуване на блокове |template-inheritance#Проверка за съществуване на блокове]. - - -last(string|array $value): mixed .[method] ------------------------------------------- -Връща последния елемент на масив или знак от низ: - -```latte -{=last([1, 2, 3, 4])} {* извежда 4 *} -{=last('abcd')} {* извежда 'd' *} -``` - -Вижте също [#first], [филтър last |filters#last]. - - -odd(int $value): bool .[method] -------------------------------- -Проверява дали даденото число е нечетно. - -```latte -{if odd($num)} ... {/if} -``` - - -slice(string|array $value, int $start, ?int $length=null, bool $preserveKeys=false): string|array .[method] ------------------------------------------------------------------------------------------------------------ -Извлича част от масив или низ. - -```latte -{=slice('hello', 1, 2)} {* извежда 'el' *} -{=slice(['a', 'b', 'c'], 1, 2)} {* извежда ['b', 'c'] *} -``` - -Функцията работи като PHP функцията `array_slice` за масиви или `mb_substr` за низове с резервен вариант към функцията `iconv_substr` в режим UTF‑8. - -Ако `$start` е положителен, последователността ще започне изместена с този брой от началото на масива/низа. Ако е отрицателен, последователността ще започне изместена с толкова от края. - -Ако е зададен параметър `$length` и е положителен, последователността ще съдържа толкова елементи. Ако в тази функция се предаде отрицателен параметър `$length`, последователността ще съдържа всички елементи на оригиналния масив, започвайки от позиция `$start` и завършвайки на позиция, по-малка с `$length` елементи от края на масива. Ако не зададете този параметър, последователността ще съдържа всички елементи на оригиналния масив, започвайки от позиция `$start`. - -По подразбиране функцията променя реда и нулира целочислените ключове на масива. Това поведение може да се промени, като се зададе `$preserveKeys` на `true`. Низовите ключове винаги се запазват, независимо от този параметър. diff --git a/latte/bg/guide.texy b/latte/bg/guide.texy deleted file mode 100644 index 29de4ffdcd..0000000000 --- a/latte/bg/guide.texy +++ /dev/null @@ -1,45 +0,0 @@ -Първи стъпки с Latte -******************** - -
    - -Шаблоните подобряват организацията на кода, разделят логиката на приложението от представянето и повишават сигурността. Те предлагат много по-добри функции и изразни средства за генериране на HTML от самото PHP. - -Latte е най-сигурната система за шаблони за PHP. Ще се влюбите в интуитивния й синтаксис. Широката гама от полезни функции значително ще улесни работата ви. Предоставя върхова защита срещу [критични уязвимости|safety-first] и ви позволява да се съсредоточите върху създаването на качествени приложения без притеснения за тяхната сигурност. - - -Как да пишем шаблони с Latte? ------------------------------ - -Latte е умно проектиран и лесен за научаване от тези, които познават PHP и усвоят основните тагове. - -- Първо се запознайте със [синтаксиса на Latte|syntax] и [ИЗПРОБВАЙТЕ ГО ОНЛАЙН |https://fiddle.nette.org/latte/#9cc0cf6d89#9cc0cf6d89] -- Разгледайте основния набор от [тагове|tags] и [филтри|filters] -- Пишете шаблони в [редактор с поддръжка на Latte |recipes#Редактори и IDE] - - -Как да използваме Latte в PHP? ------------------------------- - -Внедряването на Latte във вашето ново приложение е въпрос на няколко минути: - -- Първо [инсталирайте и стартирайте Latte |develop#Инсталация] -- Позволете си да бъдете поглезени от [инструмента за дебъгване Tracy |develop#Дебъгване и Tracy] -- Разширете Latte със [собствена функционалност |extending-latte] - -Ако преобразувате стар проект, написан на чист PHP, в Latte, миграцията ще ви улесни [инструмент за преобразуване на PHP код в Latte |cookbook/migration-from-php]. Или се готвите да преминете към Latte от Twig? Имаме за вас [конвертор на шаблони от Twig към Latte |cookbook/migration-from-twig]. - - -Какво още може Latte? ---------------------- - -Получавате Latte в пълна окомплектовка, с всичко важно в основата. - -- Вашата продуктивност ще бъде подсилена от [механизми за наследяване |template-inheritance], благодарение на които повтарящите се елементи и структури се използват повторно -- Бронираният бункер [sandbox] изолира шаблони от ненадеждни източници, които например се редактират от самите потребители -- За допълнително вдъхновение са тук [съвети и трикове |recipes] - -
    - - -{{description: Latte е най-сигурната система за шаблони за PHP. Предотвратява много уязвимости в сигурността. Ще оцените интуитивния му синтаксис и ще оцените много полезни функции.}} diff --git a/latte/bg/loaders.texy b/latte/bg/loaders.texy deleted file mode 100644 index 01ac7a7666..0000000000 --- a/latte/bg/loaders.texy +++ /dev/null @@ -1,198 +0,0 @@ -Loaders -******* - -.[perex] -Loaders са механизмът, който Latte използва за получаване на изходния код на вашите шаблони. Най-често шаблоните се съхраняват като файлове на диска, но благодарение на гъвкавата система на loaders, можете да ги зареждате практически отвсякъде или дори да ги генерирате динамично. - - -Какво е Loader? -=============== - -Когато работите с шаблони, обикновено си представяте файлове `.latte`, разположени в структурата на директориите на вашия проект. За това се грижи [#FileLoader] по подразбиране в Latte. Връзката между името на шаблона (като `'main.latte'` или `'components/card.latte'`) и неговия действителен изходен код обаче *не е задължително* да бъде директно съпоставяне с път до файл. - -Точно тук влизат в игра loaders. Loader е обект, който има за задача да вземе името на шаблона (идентифициращ низ) и да предостави на Latte неговия изходен код. Latte напълно разчита на конфигурирания loader за тази задача. Това важи не само за първоначалния шаблон, изискан с помощта на `$latte->render('main.latte')`, но и за **всеки шаблон, рефериран вътре** с помощта на тагове като `{include ...}`, `{layout ...}`, `{embed ...}` или `{import ...}`. - -Защо да използвате персонализиран loader? - -- **Зареждане от алтернативни източници:** Получаване на шаблони, съхранени в база данни, в кеш (като Redis или Memcached), в система за управление на версии (като Git, въз основа на конкретен commit) или динамично генерирани. -- **Имплементиране на персонализирани конвенции за именуване:** Може да искате да използвате по-кратки псевдоними за шаблони или да имплементирате специфична логика за пътища за търсене (напр. първо търсене в директорията на темата, след това връщане към директорията по подразбиране). -- **Добавяне на сигурност или контрол на достъпа:** Персонализиран loader може да провери потребителските права преди зареждане на определени шаблони. -- **Предварителна обработка:** Въпреки че това обикновено не се препоръчва ([компилационните проходи |compiler-passes] са по-добри), loader *би* могъл теоретично да извърши предварителна обработка на съдържанието на шаблона, преди да го предаде на Latte. - -Loader за инстанция на `Latte\Engine` се задава с помощта на метода `setLoader()`: - -```php -$latte = new Latte\Engine; - -// Използване на FileLoader по подразбиране за файлове в '/path/to/templates' -$loader = new Latte\Loaders\FileLoader('/path/to/templates'); -$latte->setLoader($loader); -``` - -Loader трябва да имплементира интерфейса `Latte\Loader`. - - -Вградени Loaders -================ - -Latte предлага няколко стандартни loaders: - - -FileLoader ----------- - -Това е **loader-ът по подразбиране**, използван от класа `Latte\Engine`, ако не е указан друг. Той зарежда шаблони директно от файловата система. - -По желание можете да зададете коренна директория за ограничаване на достъпа: - -```php -use Latte\Loaders\FileLoader; - -// Следното ще позволи зареждане на шаблони само от директорията /var/www/html/templates -$loader = new FileLoader('/var/www/html/templates'); -$latte->setLoader($loader); - -// $latte->render('../../../etc/passwd'); // Това би хвърлило изключение - -// Рендиране на шаблон, разположен на /var/www/html/templates/pages/contact.latte -$latte->render('pages/contact.latte'); -``` - -При използване на тагове като `{include}` или `{layout}` решава имената на шаблоните относително спрямо текущия шаблон, ако не е зададен абсолютен път. - - -StringLoader ------------- - -Този loader получава съдържанието на шаблона от асоциативен масив, където ключовете са имената на шаблоните (идентификатори), а стойностите са низове с изходния код на шаблона. Той е особено полезен за тестване или малки приложения, където шаблоните могат да бъдат съхранени директно в PHP кода. - -```php -use Latte\Loaders\StringLoader; - -$loader = new StringLoader([ - 'main.latte' => 'Hello {$name}, include is below:{include helper.latte}', - 'helper.latte' => '{var $x = 10}Included content: {$x}', - // Добавете още шаблони според нуждите -]); - -$latte->setLoader($loader); - -$latte->render('main.latte', ['name' => 'World']); -// Изход: Hello World, include is below:Included content: 10 -``` - -Ако трябва да рендирате само един шаблон директно от низ, без нужда от включване или наследяване, рефериращи към други именувани низови шаблони, можете да предадете низа директно на метода `render()` или `renderToString()`, когато използвате `StringLoader` без масив: - -```php -$loader = new StringLoader; -$latte->setLoader($loader); - -$templateString = 'Hello {$name}!'; -$output = $latte->renderToString($templateString, ['name' => 'Alice']); -// $output съдържа 'Hello Alice!' -``` - - -Създаване на персонализиран Loader -================================== - -За да създадете персонализиран loader (напр. за зареждане на шаблони от база данни, кеш, система за управление на версии или друг източник), трябва да създадете клас, който имплементира интерфейса [api:Latte\Loader]. - -Нека разгледаме какво трябва да прави всеки метод. - - -getContent(string $name): string .[method] ------------------------------------------- -Това е основният метод на loader-а. Неговата задача е да получи и върне пълния изходен код на шаблона, идентифициран чрез `$name` (както е предадено на метода `$latte->render()` или върнато от метода [#getReferredName()]). - -Ако шаблонът не може да бъде намерен или достъпен, този метод **трябва да хвърли изключение `Latte\RuntimeException`**. - -```php -public function getContent(string $name): string -{ - // Пример: Зареждане от хипотетично вътрешно хранилище - $content = $this->storage->read($name); - if ($content === null) { - throw new Latte\RuntimeException("Template '$name' cannot be loaded."); - } - return $content; -} -``` - - -getReferredName(string $name, string $referringName): string .[method] ----------------------------------------------------------------------- -Този метод решава превода на имената на шаблоните, използвани в рамките на тагове като `{include}`, `{layout}` и т.н. Когато Latte срещне например `{include 'partial.latte'}` вътре в `main.latte`, той извиква този метод с `$name = 'partial.latte'` и `$referringName = 'main.latte'`. - -Задачата на метода е да преведе `$name` на каноничен идентификатор (напр. абсолютен път, уникален ключ на база данни), който ще бъде използван при извикване на други методи на loader-а, въз основа на контекста, предоставен в `$referringName`. - -```php -public function getReferredName(string $name, string $referringName): string -{ - return ...; -} -``` - - -getUniqueId(string $name): string .[method] -------------------------------------------- -Latte използва кеш на компилирани шаблони за подобряване на производителността. Всеки компилиран файл на шаблон се нуждае от уникално име, получено от идентификатора на изходния шаблон. Този метод предоставя низ, който **еднозначно идентифицира** шаблона `$name`. - -За шаблони, базирани на файлове, може да послужи абсолютният път. За шаблони в база данни е обичайна комбинация от префикс и ID на базата данни. - -```php -public function getUniqueId(string $name): string -{ - return ...; -} -``` - - -Пример: Прост Loader за база данни ----------------------------------- - -Този пример показва основната структура на loader, който зарежда шаблони, съхранени в таблица на база данни, наречена `templates` с колони `name` (уникален идентификатор), `content` и `updated_at`. - -```php -use Latte; - -class DatabaseLoader implements Latte\Loader -{ - public function __construct( - private \PDO $db, - ) { - } - - public function getContent(string $name): string - { - $stmt = $this->db->prepare('SELECT content FROM templates WHERE name = ?'); - $stmt->execute([$name]); - $content = $stmt->fetchColumn(); - if ($content === false) { - throw new Latte\RuntimeException("Шаблон '$name' не е намерен в базата данни."); - } - return $content; - } - - // Този прост пример предполага, че имената на шаблоните ('homepage', 'article', и т.н.) - // са уникални ID и шаблоните не се реферират един към друг относително. - public function getReferredName(string $name, string $referringName): string - { - return $name; - } - - public function getUniqueId(string $name): string - { - // Използването на префикс и самото име тук е уникално и достатъчно - return 'db_' . $name; - } -} - -// Използване: -$pdo = new \PDO(/* детайли за връзка */); -$loader = new DatabaseLoader($pdo); -$latte->setLoader($loader); -$latte->render('homepage'); // Зарежда шаблон с име 'homepage' от БД -``` - -Персонализираните loaders ви дават пълен контрол върху това откъде идват вашите Latte шаблони, което позволява интеграция с различни системи за съхранение и работни процеси. diff --git a/latte/bg/recipes.texy b/latte/bg/recipes.texy deleted file mode 100644 index 2c843a386d..0000000000 --- a/latte/bg/recipes.texy +++ /dev/null @@ -1,162 +0,0 @@ -Съвети и трикове -**************** - - -Редактори и IDE -=============== - -Пишете шаблони в редактор или IDE, който има поддръжка за Latte. Ще бъде много по-приятно. - -- PhpStorm: инсталирайте в `Settings > Plugins > Marketplace` [плъгин Latte|https://plugins.jetbrains.com/plugin/7457-latte] -- VS Code: инсталирайте [Nette Latte + Neon|https://marketplace.visualstudio.com/items?itemName=Kasik96.latte], [Nette Latte templates|https://marketplace.visualstudio.com/items?itemName=smuuf.latte-lang] или най-новия [Nette for VS Code |https://marketplace.visualstudio.com/items?itemName=franken-ui.nette-for-vscode] плъгин -- NetBeans IDE: нативната поддръжка на Latte е част от инсталацията -- Sublime Text 3: в Package Control намерете и инсталирайте пакета `Nette` и изберете Latte в `View > Syntax` -- в стари редактори използвайте за файлове .latte подчертаване на Smarty - -Плъгинът за PhpStorm е много напреднал и може отлично да подсказва PHP код. За да работи оптимално, използвайте [типизирани шаблони|type-system]. - -[* latte-phpstorm-plugin.webp *] - -Поддръжка за Latte ще намерите също и в уеб подчертавача на код [Prism.js|https://prismjs.com/#supported-languages] и редактора [Ace|https://ace.c9.io]. - - -Latte в JavaScript или CSS -========================== - -Latte може много удобно да се използва и в JavaScript или CSS. Но как да избегнем ситуация, в която Latte погрешно би счело JavaScript код или CSS стил за Latte таг? - -```latte - - - -``` - -**Вариант 1** - -Избягвайте ситуация, в която след `{` веднага следва буква, например като вмъкнете интервал, нов ред или кавичка преди нея: - -```latte - - - -``` - -**Вариант 2** - -Напълно изключете обработката на Latte тагове вътре в елемента с помощта на [n:syntax |tags#syntax]: - -```latte - -``` - -**Вариант 3** - -Превключете синтаксиса на Latte таговете вътре в елемента на двойни къдрави скоби: - -```latte - -``` - -В JavaScript [не се пишат кавички около променливата |tags#Извеждане в JavaScript]. - - -Замяна на `use` клауза в Latte -============================== - -Как в Latte да заменим клаузите `use`, които се използват в PHP, за да не се налага да пишем namespace при достъп до клас? Пример в PHP: - -```php -use Pets\Model\Dog; - -if ($dog->status === Dog::StatusHungry) { - // ... -} -``` - -**Вариант 1** - -Вместо клауза `use`, ще запазим името на класа в променлива и след това вместо `Dog` ще използваме `$Dog`: - -```latte -{var $Dog = Pets\Model\Dog::class} - -
    - {if $dog->status === $Dog::StatusHungry} - ... - {/if} -
    -``` - -**Вариант 2** - -Ако обектът `$dog` е инстанция на `Pets\Model\Dog`, тогава може да се използва `{if $dog->status === $dog::StatusHungry}`. - - -Генериране на XML в Latte -========================= - -Latte може да генерира всякакъв текстов формат (HTML, XML, CSV, iCal и т.н.), но за да екранира правилно извежданите данни, трябва да му кажем какъв формат генерираме. За това служи тагът [`{contentType}` |tags#contentType]. - -```latte -{contentType application/xml} - -... -``` - -След това можем например да генерираме sitemap по подобен начин: - -```latte -{contentType application/xml} - - - - {$url->loc} - {$url->lastmod->format('Y-m-d')} - {$url->frequency} - {$url->priority} - - -``` - - -Предаване на данни от включен шаблон -==================================== - -Променливите, които създаваме с помощта на `{var}` или `{default}` във включения шаблон, съществуват само в него и не са достъпни във включващия шаблон. Ако искаме да предадем данни от включения шаблон обратно към включващия, една от възможностите е да предадем обект на шаблона и да вмъкнем данните в него. - -Основен шаблон: - -```latte -{* създава празен обект $vars *} -{var $vars = (object) null} - -{include 'included.latte', vars: $vars} - -{* сега съдържа свойството foo *} -{$vars->foo} -``` - -Включен шаблон `included.latte`: - -```latte -{* записваме данни в свойството foo *} -{var $vars->foo = 123} -``` diff --git a/latte/bg/safety-first.texy b/latte/bg/safety-first.texy deleted file mode 100644 index 56a3b9fd91..0000000000 --- a/latte/bg/safety-first.texy +++ /dev/null @@ -1,383 +0,0 @@ -Latte е синоним на сигурност -**************************** - -
    - -Latte е единствената система за шаблони за PHP с ефективна защита срещу критичната уязвимост Cross-site Scripting (XSS). И това е благодарение на т.нар. контекстно-чувствително екраниране. Ще си поговорим за: - -- какъв е принципът на уязвимостта XSS и защо е толкова опасна -- защо Latte е толкова ефективен в защитата срещу XSS -- как в шаблоните на Twig, Blade и други подобни може лесно да се направи дупка в сигурността - -
    - - -Cross-site Scripting (XSS) -========================== - -Cross-site Scripting (съкратено XSS) е една от най-често срещаните уязвимости на уеб страниците и същевременно много опасна. Тя позволява на нападателя да вмъкне в чужда страница зловреден скрипт (т.нар. malware), който се стартира в браузъра на нищо неподозиращия потребител. - -Какво всичко може да направи такъв скрипт? Може например да изпрати на нападателя всякакво съдържание от нападнатата страница, включително чувствителни данни, показани след влизане. Може да промени страницата или да извършва други заявки от името на потребителя. Ако например става въпрос за уебмейл, може да прочете чувствителни съобщения, да промени показваното съдържание или да пренастрои конфигурацията, напр. да включи препращане на копия на всички съобщения към адреса на нападателя, за да получи достъп и до бъдещи имейли. - -Затова XSS фигурира на водещи места в класациите на най-опасните уязвимости. Ако на уеб страница се появи уязвимост, е необходимо тя да бъде отстранена възможно най-скоро, за да се предотврати злоупотреба. - - -Как възниква уязвимостта? -------------------------- - -Грешката възниква на мястото, където се генерира уеб страницата и се извеждат променливи. Представете си, че създавате страница с търсене, и в началото ще има параграф с търсения израз във вида: - -```php -echo '

    Резултати от търсенето за ' . $search . '

    '; -``` - -Нападателят може в полето за търсене и съответно в променливата `$search` да запише произволен низ, т.е. и HTML код като ``. Тъй като изходът не е обработен по никакъв начин, той става част от показаната страница: - -```html -

    Резултати от търсенето за

    -``` - -Браузърът, вместо да изпише търсения низ, стартира JavaScript. И така нападателят поема контрола над страницата. - -Можете да възразите, че вмъкването на код в променлива наистина ще доведе до стартиране на JavaScript, но само в браузъра на нападателя. Как ще стигне до жертвата? От тази гледна точка разграничаваме няколко типа XSS. В нашия пример с търсенето говорим за *reflected XSS*. Тук е необходимо още да се насочи жертвата да кликне върху връзка, която ще съдържа зловреден код в параметъра: - -``` -https://example.com/?search= -``` - -Насочването на потребителя към връзката наистина изисква известно социално инженерство, но не е нищо сложно. Потребителите кликват върху връзки, било то в имейли или в социалните мрежи, без много да мислят. А това, че в адреса има нещо подозрително, може да се маскира с помощта на съкратител на URL, потребителят тогава вижда само `bit.ly/xxx`. - -Въпреки това съществува и втора, много по-опасна форма на атака, наречена *stored XSS* или *persistent XSS*, при която нападателят успява да съхрани зловреден код на сървъра така, че той автоматично да се вмъква в някои страници. - -Пример за това са страниците, където потребителите пишат коментари. Нападателят изпраща публикация, съдържаща код, и той се съхранява на сървъра. Ако страниците не са достатъчно защитени, той ще се стартира в браузъра на всеки посетител. - -Може да изглежда, че ядрото на атаката се състои в това да се вкара в страницата низът ` - - - -

    -``` - -Два пътя и два различни начина за екраниране на данни. Вътре в елементите ` -``` - -Ако обаче искахме да го вмъкнем в HTML атрибут, трябва още да екранираме кавичките в HTML ентичности: - -```html -
    -``` - -Вложеният контекст обаче не е задължително да бъде само JS или CSS. Често това е и URL. Параметрите в URL се екранират така, че знаците със специално значение се преобразуват в последователности, започващи с `%`. Пример: - -``` -https://example.org/?a=Jazz&b=Rock%27n%27Roll -``` - -И когато този низ изведем в атрибут, ще приложим още екраниране според този контекст и ще заменим `&` с `&`: - -```html - -``` - -Ако сте прочели дотук, поздравления, беше изчерпателно. Сега вече имате добра представа какво са контексти и екраниране. И не трябва да се притеснявате, че е сложно. Latte прави това за вас автоматично. - - -Latte срещу наивни системи -========================== - -Показахме си как правилно се екранира в HTML документ и колко е важно познаването на контекста, т.е. мястото, където извеждаме данните. С други думи, как работи контекстно-чувствителното екраниране. Въпреки че това е необходима предпоставка за функционална защита срещу XSS, **Latte е единствената система за шаблони за PHP, която може това.** - -Как е възможно това, когато всички системи днес твърдят, че имат автоматично екраниране? Автоматичното екраниране без познаване на контекста е малко глупост, която **създава фалшиво усещане за сигурност**. - -Системи за шаблони като Twig, Laravel Blade и други не виждат в шаблона никаква HTML структура. Следователно не виждат и контексти. В сравнение с Latte те са слепи и наивни. Обработват само собствените си тагове, всичко останало за тях е незначителен поток от знаци: - -
    - -```twig .{file:Twig шаблон, както го вижда самият Twig} -░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░ -░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░ -``` - -```twig .{file:Twig шаблон, както го вижда дизайнерът} -- в текст: {{ foo }} -- в таг: -- в атрибут: -- в атрибут без кавички: -- в атрибут, съдържащ URL: -- в атрибут, съдържащ JavaScript: -- в атрибут, съдържащ CSS: -- в JavaScript: -- в CSS: -- в коментар: -``` - -
    - -Наивните системи само механично преобразуват знаците `< > & ' "` в HTML ентичности, което, макар и в повечето случаи на употреба да е валиден начин за екраниране, далеч не винаги е така. Те не могат да открият или предотвратят възникването на различни дупки в сигурността, както ще покажем по-нататък. - -Latte вижда шаблона по същия начин като вас. Разбира HTML, XML, разпознава тагове, атрибути и т.н. И благодарение на това разграничава отделните контексти и според тях обработва данните. Предлага така наистина ефективна защита срещу критичната уязвимост Cross-site Scripting. - -
    - -```latte .{file:Latte шаблон, както го вижда Latte} -░░░░░░░░░░░{$foo} -░░░░░░░░░░ -░░░░░░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ -░░░░░░░░░░░░░░░░░ -░░░░░░░░░ -░░░░░░░░░░░░░░░ -``` - -```latte .{file:Latte шаблон, както го вижда дизайнерът} -- в текст: {$foo} -- в таг: -- в атрибут: -- в атрибут без кавички: -- в атрибут, съдържащ URL: -- в атрибут, съдържащ JavaScript: -- в атрибут, съдържащ CSS: -- в JavaScript: -- в CSS: -- в коментар: -``` - -
    - - -Жив пример -========== - -Вляво виждате шаблон в Latte, вдясно е генерираният HTML код. Няколко пъти тук се извежда променливата `$text` и всеки път в малко по-различен контекст. И следователно и малко по-различно екранирана. Можете сами да редактирате кода на шаблона, например да промените съдържанието на променливата и т.н. Опитайте: - -
    -
    - -``` .{file:template.latte; min-height: 14em}[fiddle-source] -{* ОПИТАЙТЕ ДА РЕДАКТИРАТЕ ТОЗИ ШАБЛОН *} -{var $text = "Rock'n'Roll"} -- {$text} -- -- -- -- -- -``` - -
    - -
    - -``` .{file:view-source:...; min-height: 14em}[fiddle-output] -- Rock'n'Roll -- -- -- -- -- -``` - -
    -
    - -Не е ли страхотно! Latte прави контекстно-чувствително екраниране автоматично, така че програмистът: - -- не трябва да мисли или да знае как се екранира къде -- не може да сгреши -- не може да забрави за екранирането - -Това дори не са всички контексти, които Latte разграничава при извеждане и за които адаптира обработката на данни. Ще разгледаме сега други интересни случаи. - - -Как да хакнем наивни системи -============================ - -На няколко практически примера ще покажем колко е важно разграничаването на контексти и защо наивните системи за шаблони не предоставят достатъчна защита срещу XSS, за разлика от Latte. Като представител на наивна система ще използваме в примерите Twig, но същото важи и за други системи. - - -Уязвимост чрез атрибут ----------------------- - -Ще се опитаме да инжектираме в страницата зловреден код с помощта на HTML атрибут, както [показахме по-горе |#Как възниква уязвимостта]. Нека имаме шаблон в Twig, изобразяващ изображение: - -```twig .{file:Twig} -{{ -``` - -Забележете, че около стойностите на атрибутите няма кавички. Кодерът може да ги е забравил, което просто се случва. Например в React кодът се пише така, без кавички, и кодер, който сменя езици, след това лесно може да забрави кавичките. - -Нападателят като описание на изображението вмъква умело съставен низ `foo onload=alert('Hacked!')`. Вече знаем, че Twig не може да разпознае дали променливата се извежда в потока на HTML текста, вътре в атрибут, HTML коментар и т.н., накратко не разграничава контексти. И само механично преобразува знаците `< > & ' "` в HTML ентичности. Така резултатният код ще изглежда така: - -```html -foo -``` - -**И възникна дупка в сигурността!** - -Част от страницата стана подправеният атрибут `onload` и браузърът веднага след изтеглянето на изображението го стартира. - -Сега ще видим как със същия шаблон ще се справи Latte: - -```latte .{file:Latte} -{$imageAlt} -``` - -Latte вижда шаблона по същия начин като вас. За разлика от Twig, разбира HTML и знае, че променливата се извежда като стойност на атрибут, който не е в кавички. Затова ги допълва. Когато нападателят вмъкне същото описание, резултатният код ще изглежда така: - -```html -foo onload=alert('Hacked!') -``` - -**Latte успешно предотврати XSS.** - - -Извеждане на променлива в JavaScript ------------------------------------- - -Благодарение на контекстно-чувствителното екраниране е напълно нативно възможно да се използват PHP променливи вътре в JavaScript. - -```latte -

    {$movie}

    - - -``` - -Ако променливата `$movie` съдържа низ `'Amarcord & 8 1/2'`, ще се генерира следният изход. Забележете, че вътре в HTML се използва различно екраниране от това вътре в JavaScript и още по-различно в атрибута `onclick`: - -```latte -

    Amarcord & 8 1/2

    - - -``` - - -Проверка на връзки ------------------- - -Latte автоматично проверява дали променливата, използвана в атрибутите `src` или `href`, съдържа уеб URL (т.е. протокол HTTP) и предотвратява извеждането на връзки, които могат да представляват риск за сигурността. - -```latte -{var $link = 'javascript:attack()'} - -кликни -``` - -Извежда: - -```latte -кликни -``` - -Проверката може да се изключи с помощта на филтъра [nocheck |filters#nocheck]. - - -Ограничения на Latte -==================== - -Latte не е напълно цялостна защита срещу XSS за цялото приложение. Не бихме искали, ако използвате Latte, да спрете да мислите за сигурността. Целта на Latte е да гарантира, че нападателят не може да промени структурата на страницата, да подправи HTML елементи или атрибути. Но не контролира коректността на съдържанието на извежданите данни. Или коректността на поведението на JavaScript. Това вече излиза извън компетенциите на системата за шаблони. Проверката на коректността на данните, особено тези, въведени от потребителя и следователно ненадеждни, е важна задача на програмиста. diff --git a/latte/bg/sandbox.texy b/latte/bg/sandbox.texy deleted file mode 100644 index a9586d1a28..0000000000 --- a/latte/bg/sandbox.texy +++ /dev/null @@ -1,56 +0,0 @@ -Sandbox -******* - -.[perex] -Sandbox предоставя слой за сигурност, който ви дава контрол върху това кои тагове, PHP функции, методи и т.н. могат да бъдат използвани в шаблоните. Благодарение на sandbox режима можете безопасно да си сътрудничите с клиента или външен кодер при създаването на шаблони, без да се налага да се притеснявате, че ще настъпи нарушаване на приложението или нежелани операции. - -Как работи? Просто дефинираме какво всичко ще позволим на шаблона. При което по подразбиране всичко е забранено и ние постепенно разрешаваме. Със следния код ще позволим на автора на шаблона да използва таговете `{block}`, `{if}`, `{else}` и `{=}`, което е таг за [извеждане на променлива или израз |tags#Извеждане] и всички филтри: - -```php -$policy = new Latte\Sandbox\SecurityPolicy; -$policy->allowTags(['block', 'if', 'else', '=']); -$policy->allowFilters($policy::All); - -$latte->setPolicy($policy); -``` - -Освен това можем да разрешим отделни функции, методи или свойства на обекти: - -```php -$policy->allowFunctions(['trim', 'strlen']); -$policy->allowMethods(Nette\Security\User::class, ['isLoggedIn', 'isAllowed']); -$policy->allowProperties(Nette\Database\Row::class, $policy::All); -``` - -Не е ли страхотно? Можете на много ниско ниво да контролирате абсолютно всичко. Ако шаблонът се опита да извика непозволена функция или да достъпи непозволен метод или свойство, това ще завърши с изключение `Latte\SecurityViolationException`. - -Създаването на политика от нулата, когато всичко е забранено, може да не е удобно, затова можете да започнете от безопасна основа: - -```php -$policy = Latte\Sandbox\SecurityPolicy::createSafePolicy(); -``` - -Безопасна основа означава, че са разрешени всички стандартни тагове с изключение на `contentType`, `debugbreak`, `dump`, `extends`, `import`, `include`, `layout`, `php`, `sandbox`, `snippet`, `snippetArea`, `templatePrint`, `varPrint`, `widget`. Разрешени са стандартните филтри с изключение на `datastream`, `noescape` и `nocheck`. И накрая е разрешен достъпът до методите и свойствата на обекта `$iterator`. - -Правилата се прилагат за шаблон, който вмъкваме с тага [`{sandbox}` |tags#Вмъкване на шаблон]. Което е нещо като аналог на `{include}`, но включва безопасен режим и също така не предава никакви променливи: - -```latte -{sandbox 'untrusted.latte'} -``` - -Така лейаутът и отделните страници могат необезпокоявано да използват всички тагове и променливи, само върху шаблона `untrusted.latte` ще бъдат приложени ограничения. - -Някои нарушения, като използване на забранен таг или филтър, се откриват по време на компилация. Други, като например извикване на непозволени методи на обект, чак по време на изпълнение. Шаблонът също може да съдържа всякакви други грешки. За да не може от sandbox шаблона да изскочи изключение, което да наруши цялото рендиране, може да се дефинира собствен [персонализиран обработчик на изключения |develop#Exception handler], който например да го логва. - -Ако искаме да включим sandbox режим директно за всички шаблони, това става лесно: - -```php -$latte->setSandboxMode(); -``` - -За да сте сигурни, че потребителят няма да вмъкне в страницата PHP код, който макар и синтактично правилен, е забранен и ще причини PHP Compile Error, препоръчваме [шаблоните да се проверяват с PHP linter |develop#Проверка на генерирания код]. Тази функционалност се включва с метода `Engine::enablePhpLint()`. Тъй като за проверката е необходимо да се извика PHP бинарният файл, предайте пътя до него като параметър: - -```php -$latte = new Latte\Engine; -$latte->enablePhpLinter('/path/to/php'); -``` diff --git a/latte/bg/syntax.texy b/latte/bg/syntax.texy deleted file mode 100644 index 1cbeba309b..0000000000 --- a/latte/bg/syntax.texy +++ /dev/null @@ -1,276 +0,0 @@ -Синтаксис -********* - -.[perex] -Синтаксисът на Latte произлиза от практическите изисквания на уеб дизайнерите. Търсихме най-удобния синтаксис, с който елегантно да запишете дори конструкции, които иначе представляват истинско предизвикателство. Същевременно всички изрази се пишат по абсолютно същия начин като в PHP, така че не е необходимо да учите нов език. Просто използвате това, което вече знаете. - -По-долу е представен минимален шаблон, който илюстрира няколко основни елемента: тагове, n:атрибути, коментари и филтри. - -```latte -{* това е коментар *} -
      {* n:if е n:атрибут *} -{foreach $items as $item} {* таг, представляващ цикъл foreach *} -
    • {$item|capitalize}
    • {* таг, извеждащ променлива с филтър *} -{/foreach} {* край на цикъла *} -
    -``` - -Нека разгледаме по-отблизо тези важни елементи и как те могат да ви помогнат да създадете страхотен шаблон. - - -Тагове -====== - -Шаблонът съдържа тагове, които управляват логиката на шаблона (например цикли *foreach*) или извеждат изрази. И за двете се използва единствен разделител `{ ... }`, така че не е необходимо да мислите кой разделител в коя ситуация да използвате, както е при други системи. Ако след знака `{` следва кавичка или интервал, Latte не го счита за начало на таг, благодарение на което можете в шаблоните безпроблемно да използвате и JavaScript конструкции, JSON или правила в CSS. - -Разгледайте [преглед на всички тагове|tags]. Освен това можете да създавате и [собствени тагове|custom tags]. - - -Latte разбира PHP -================= - -Вътре в таговете можете да използвате PHP изрази, които добре познавате: - -- променливи -- низове (включително HEREDOC и NOWDOC), масиви, числа и др. -- [оператори |https://www.php.net/manual/en/language.operators.php] -- извиквания на функции и методи (които могат да бъдат ограничени чрез [sandbox|sandbox]) -- [match |https://www.php.net/manual/en/control-structures.match.php] -- [анонимни функции |https://www.php.net/manual/en/functions.arrow.php] -- [callback функции |https://www.php.net/manual/en/functions.first_class_callable_syntax.php] -- многоредови коментари `/* ... */` -- и т.н.… - -Освен това Latte допълва синтаксиса на PHP с няколко [приятни разширения |#Синтактичен захар]. - - -n:атрибути -========== - -Всички двойни тагове, например `{if} … {/if}`, опериращи над един HTML елемент, могат да бъдат преписани във вид на n:атрибути. Така би могло да се запише например и `{foreach}` в началния пример: - -```latte -
      -
    • {$item|capitalize}
    • -
    -``` - -Функционалността тогава се отнася към HTML елемента, в който е поставена: - -```latte -{var $items = ['I', '♥', 'Latte']} - -

    {$item}

    -``` - -извежда: - -```latte -

    I

    -

    -

    Latte

    -``` - -С помощта на префикса `inner-` можем да променим поведението така, че да се отнася само към вътрешната част на елемента: - -```latte -
    -

    {$item}

    -
    -
    -``` - -Ще се изведе: - -```latte -
    -

    I

    -
    -

    -
    -

    Latte

    -
    -
    -``` - -Или с помощта на префикса `tag-` прилагаме функционалността само към самите HTML тагове: - -```latte -

    Title

    -``` - -Което ще изведе в зависимост от променливата `$url`: - -```latte -{* когато $url е празно *} -

    Title

    - -{* когато $url съдържа 'https://nette.org' *} -

    Title

    -``` - -Въпреки това, n:атрибутите не са само съкращение за двойни тагове. Съществуват и чисти n:атрибути, като например [n:href |application:creating-links#В шаблона на презентера] или изключително удобния помощник за кодера [n:class |tags#n:class]. - - -Филтри -====== - -Разгледайте прегледа на [стандартни филтри |filters]. - -Филтрите се записват след вертикална черта (може да има интервал преди нея): - -```latte -

    {$heading|upper}

    -``` - -Филтрите могат да бъдат верижно свързани и след това се прилагат в реда отляво надясно: - -```latte -

    {$heading|lower|capitalize}

    -``` - -Параметрите се задават след името на филтъра, разделени с двоеточия или запетаи: - -```latte -

    {$heading|truncate:20,''}

    -``` - -Филтрите могат да се прилагат и към израз: - -```latte -{var $name = ($title|upper) . ($subtitle|lower)} -``` - -Към блок: - -```latte -

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

    -``` - -Или директно към стойност (в комбинация с тага [`{=expr}` |tags#Извеждане]): -```latte -

    {=' Hello world '|trim}

    -``` - - -Динамични HTML тагове .{data-version:3.0.9} -=========================================== - -Latte поддържа динамични HTML тагове, които са полезни, когато се нуждаете от гъвкавост в имената на таговете: - -```latte -Heading -``` - -Горният код може например да генерира `

    Heading

    ` или `

    Heading

    ` в зависимост от стойността на променливата `$level`. Динамичните HTML тагове в Latte трябва винаги да бъдат двойни. Тяхната алтернатива е [n:tag |tags#n:tag]. - -Тъй като Latte е сигурна система за шаблони, тя проверява дали резултатното име на тага е валидно и не съдържа никакви нежелани или вредни стойности. Освен това гарантира, че името на затварящия таг винаги ще бъде същото като името на отварящия таг. - - -Коментари -========= - -Коментарите се записват по този начин и не попадат в изхода: - -```latte -{* това е коментар в Latte *} -``` - -Вътре в таговете работят PHP коментари: - -```latte -{include 'file.info', /* value: 123 */} -``` - - -Синтактичен захар -================= - - -Низове без кавички ------------------- - -При прости низове могат да се пропуснат кавичките: - -```latte -като в PHP: {var $arr = ['hello', 'btn--default', '€']} - -съкратено: {var $arr = [hello, btn--default, €]} -``` - -Прости низове са тези, които са съставени само от букви, цифри, долни черти, тирета и точки. Не трябва да започват с цифра и не трябва да започват или завършват с тире. Не трябва да са съставени само от главни букви и долни черти, защото тогава се считат за константа (напр. `PHP_VERSION`). И не трябва да колидират с ключови думи: `and`, `array`, `clone`, `default`, `false`, `in`, `instanceof`, `new`, `null`, `or`, `return`, `true`, `xor`. - - -Константи ---------- - -Тъй като при прости низове могат да се пропускат кавичките, препоръчваме за разграничение да се записват глобални константи с наклонена черта в началото: - -```latte -{if \PROJECT_ID === 1} ... {/if} -``` - -Този запис е напълно валиден в самото PHP, наклонената черта казва, че константата е в глобалното пространство от имена. - - -Съкратен тернарен оператор --------------------------- - -Ако третата стойност на тернарния оператор е празна, може да се пропусне: - -```latte -като в PHP: {$stock ? 'Налично' : ''} - -съкратено: {$stock ? 'Налично'} -``` - - -Модерен запис на ключове в масив --------------------------------- - -Ключовете в масив могат да се записват подобно на именуваните параметри при извикване на функции: - -```latte -като в PHP: {var $arr = ['one' => 'item 1', 'two' => 'item 2']} - -модерно: {var $arr = [one: 'item 1', two: 'item 2']} -``` - - -Филтри ------- - -Филтрите могат да се използват за всякакви изрази, достатъчно е цялото да се затвори в скоби: - -```latte -{var $content = ($text|truncate: 30|upper)} -``` - - -Оператор `in` -------------- - -С оператора `in` може да се замени функцията `in_array()`. Сравнението винаги е стриктно: - -```latte -{* аналог на in_array($item, $items, true) *} -{if $item in $items} - ... -{/if} -``` - - -Исторически преглед -------------------- - -Latte през своята история е въвеждал цяла редица синтактични захари, които след няколко години са се появявали в самото PHP. Например в Latte беше възможно да се пишат масиви като `[1, 2, 3]` вместо `array(1, 2, 3)` или да се използва nullsafe операторът `$obj?->foo` много преди това да стане възможно в самото PHP. Latte също въведе оператор за разгръщане на масив `(expand) $arr`, който е еквивалент на днешния оператор `...$arr` от PHP. - -Undefined-safe операторът `??->`, който е аналог на nullsafe оператора `?->`, но не предизвиква грешка, ако променливата не съществува, възникна по исторически причини и днес препоръчваме да се използва стандартният PHP оператор `?->`. - - -Ограничения на PHP в Latte -========================== - -В Latte могат да се записват само PHP изрази. Тоест не могат да се използват стейтмънти, завършващи с точка и запетая. Не могат да се декларират класове или да се използват [контролни структури |https://www.php.net/manual/en/language.control-structures.php], напр. `if`, `foreach`, `switch`, `return`, `try`, `throw` и други, вместо които Latte предлага свои [тагове|tags]. Също така не могат да се използват [атрибути |https://www.php.net/manual/en/language.attributes.php], [обратни кавички |https://www.php.net/manual/en/language.operators.execution.php] или някои [магически константи |https://www.php.net/manual/en/language.constants.magic.php]. Не могат да се използват и `unset`, `echo`, `include`, `require`, `exit`, `eval`, защото това не са функции, а специални езикови конструкции на PHP и следователно не са изрази. Коментарите се поддържат само многоредови `/* ... */`. - -Тези ограничения обаче могат да бъдат заобиколени, като активирате разширението [RawPhpExtension |develop#RawPhpExtension], благодарение на което след това може да се използва в тага `{php ...}` всякакъв PHP код на отговорност на автора на шаблона. diff --git a/latte/bg/tags.texy b/latte/bg/tags.texy deleted file mode 100644 index 05257823ee..0000000000 --- a/latte/bg/tags.texy +++ /dev/null @@ -1,1079 +0,0 @@ -Latte тагове -************ - -.[perex] -Преглед и описание на всички тагове на системата за шаблони Latte, които са ви стандартно достъпни. - -.[table-latte-tags language-latte] -|## Извеждане -| `{$var}`, `{...}` или `{=...}` | [извежда екранирана променлива или израз |#Извеждане] -| `{$var\|filter}` | [извежда с използване на филтри |#Филтри] -| `{l}` или `{r}` | извежда знак `{` или `}` - -.[table-latte-tags language-latte] -|## Условия -| `{if}` … `{elseif}` … `{else}` … `{/if}` | [условие if |#if elseif else] -| `{ifset}` … `{elseifset}` … `{/ifset}` | [условие ifset |#ifset elseifset] -| `{ifchanged}` … `{/ifchanged}` | [проверка дали е настъпила промяна |#ifchanged] -| `{switch}` `{case}` `{default}` `{/switch}` | [условие switch |#switch case default] -| `n:else` | [алтернативно съдържание за условия |#n:else] - -.[table-latte-tags language-latte] -|## Цикли -| `{foreach}` … `{/foreach}` | [#foreach] -| `{for}` … `{/for}` | [#for] -| `{while}` … `{/while}` | [#while] -| `{continueIf $cond}` | [продължаване със следващата итерация |#continueIf skipIf breakIf] -| `{skipIf $cond}` | [пропускане на итерация |#continueIf skipIf breakIf] -| `{breakIf $cond}` | [прекъсване на цикъл |#continueIf skipIf breakIf] -| `{exitIf $cond}` | [ранно прекратяване |#exitIf] -| `{first}` … `{/first}` | [това първото преминаване ли е? |#first last sep] -| `{last}` … `{/last}` | [това последното преминаване ли е? |#first last sep] -| `{sep}` … `{/sep}` | [ще последва ли още преминаване? |#first last sep] -| `{iterateWhile}` … `{/iterateWhile}` | [структуриран foreach |#iterateWhile] -| `$iterator` | [специална променлива вътре в foreach |#iterator] - -.[table-latte-tags language-latte] -|## Вмъкване на други шаблони -| `{include 'file.latte'}` | [зарежда шаблон от друг файл |#include] -| `{sandbox 'file.latte'}` | [зарежда шаблон в sandbox режим |#sandbox] - -.[table-latte-tags language-latte] -|## Блокове, лейаути, наследяване на шаблони -| `{block}` | [анонимен блок |#block] -| `{block blockname}` | [дефинира блок |template-inheritance#Блокове] -| `{define blockname}` | [дефинира блок за по-късна употреба |template-inheritance#Дефиниции] -| `{include blockname}` | [рендиране на блок |template-inheritance#Рендиране на блокове] -| `{include blockname from 'file.latte'}` | [рендира блок от файл |template-inheritance#Рендиране на блокове] -| `{import 'file.latte'}` | [зарежда блокове от шаблон |template-inheritance#Хоризонтално повторно използване] -| `{layout 'file.latte'}` / `{extends}` | [определя файла с лейаута |template-inheritance#Наследяване на лейаут] -| `{embed}` … `{/embed}` | [зарежда шаблон или блок и позволява презаписване на блокове |template-inheritance#Единично наследяване] -| `{ifset blockname}` … `{/ifset}` | [условие дали съществува блок |template-inheritance#Проверка за съществуване на блокове] - -.[table-latte-tags language-latte] -|## Управление на изключения -| `{try}` … `{else}` … `{/try}` | [прихващане на изключения |#try] -| `{rollback}` | [отхвърляне на try блок |#rollback] - -.[table-latte-tags language-latte] -|## Променливи -| `{var $foo = value}` | [създава променлива |#var default] -| `{default $foo = value}` | [създава променлива, ако не съществува |#var default] -| `{parameters}` | [декларира променливи, типове и стойности по подразбиране |#parameters] -| `{capture}` … `{/capture}` | [улавя блок в променлива |#capture] - -.[table-latte-tags language-latte] -|## Типове -| `{varType}` | [декларира типа на променливата |type-system#varType] -| `{varPrint}` | [предлага типове променливи |type-system#varPrint] -| `{templateType}` | [декларира типове променливи според класа |type-system#templateType] -| `{templatePrint}` | [предлага клас с типове променливи |type-system#templatePrint] - -.[table-latte-tags language-latte] -|## Преводи -| `{_...}` | [извежда превод |#Преводи] -| `{translate}` … `{/translate}` | [превежда съдържание |#Преводи] - -.[table-latte-tags language-latte] -|## Други -| `{contentType}` | [превключва екранирането и изпраща HTTP хедър |#contentType] -| `{debugbreak}` | [поставя breakpoint в кода |#debugbreak] -| `{do}` | [изпълнява код, но не извежда нищо |#do] -| `{dump}` | [дъмпва променливи в Tracy Bar |#dump] -| `{php}` | [изпълнява всякакъв PHP код |#php] -| `{spaceless}` … `{/spaceless}` | [премахва излишните интервали |#spaceless] -| `{syntax}` | [промяна на синтаксиса по време на изпълнение |#syntax] -| `{trace}` | [показва stack trace |#trace] - -.[table-latte-tags language-latte] -|## Помощници за HTML кодера -| `n:class` | [динамичен запис на HTML атрибут class |#n:class] -| `n:attr` | [динамичен запис на всякакви HTML атрибути |#n:attr] -| `n:tag` | [динамичен запис на името на HTML елемент |#n:tag] -| `n:ifcontent` | [пропуска празен HTML таг |#n:ifcontent] - -.[table-latte-tags language-latte] -|## Достъпни само в Nette Framework -| `n:href` | [връзка, използвана в HTML елементи `` |application:creating-links#В шаблона на презентера] -| `{link}` | [извежда връзка |application:creating-links#В шаблона на презентера] -| `{plink}` | [извежда връзка към presenter |application:creating-links#В шаблона на презентера] -| `{control}` | [рендира компонент |application:components#Рендиране] -| `{snippet}` … `{/snippet}` | [фрагмент, който може да бъде изпратен чрез AJAX |application:ajax#Снипети в Latte] -| `{snippetArea}` | [обвивка за фрагменти |application:ajax#Области на снипети] -| `{cache}` … `{/cache}` | [кешира част от шаблона |caching:#Кеширане в Latte] - -.[table-latte-tags language-latte] -|## Достъпни само с Nette Forms -| `{form}` … `{/form}` | [рендира тагове на формата |forms:rendering#form] -| `{label}` … `{/label}` | [рендира етикет на елемент от формата |forms:rendering#label input] -| `{input}` | [рендира елемент от формата |forms:rendering#label input] -| `{inputError}` | [извежда съобщение за грешка на елемент от формата |forms:rendering#inputError] -| `n:name` | [активира елемент от формата |forms:rendering#n:name] -| `{formContainer}` … `{/formContainer}` | [рендиране на контейнер на формата |forms:rendering#Специални случаи] - -.[table-latte-tags language-latte] -|## Налично само с Nette Assets -| `{asset}` | [визуализира актив като HTML елемент или URL адрес |assets:#asset] -| `{preload}` | [генерира подсказки за предварително зареждане за оптимизиране на производителността |assets:#preload] -| `n:asset` | [добавя атрибути на активи към HTML елементи |assets:#n:asset] - - -Извеждане -========= - - -`{$var}` `{...}` `{=...}` -------------------------- - -В Latte се използва тагът `{=...}` за извеждане на всякакъв израз на изхода. Latte се грижи за вашето удобство, така че ако изразът започва с променлива или извикване на функция, не е необходимо да пишете знака за равенство. Което на практика означава, че почти никога не е необходимо да го пишете: - -```latte -Име: {$name} {$surname}
    -Възраст: {date('Y') - $birth}
    -``` - -Като израз можете да запишете всичко, което познавате от PHP. Просто не е нужно да учите нов език. Така например: - - -```latte -{='0' . ($num ?? $num * 3) . ', ' . PHP_VERSION} -``` - -Моля, не търсете никакъв смисъл в предишния пример, но ако намерите такъв, пишете ни :-) - - -Екраниране на изхода --------------------- - -Коя е най-важната задача на системата за шаблони? Да предотврати дупки в сигурността. И точно това прави Latte винаги, когато извеждате нещо. Автоматично го екранира: - -```latte -

    {='one < two'}

    {* извежда: '

    one < two

    ' *} -``` - -За да бъдем точни, Latte използва контекстно-чувствително екраниране, което е толкова важно и уникално нещо, че му посветихме [отделна глава |safety-first#Контекстно-чувствително екраниране]. - -А какво ако извеждате съдържание, кодирано в HTML от надежден източник? Тогава лесно може да се изключи екранирането: - -```latte -{$trustedHtmlString|noescape} -``` - -.[warning] -Неправилното използване на филтъра `noescape` може да доведе до уязвимост XSS! Никога не го използвайте, ако не сте **напълно сигурни** какво правите и че извежданият низ идва от надежден източник. - - -Извеждане в JavaScript ----------------------- - -Благодарение на контекстно-чувствителното екраниране е изключително лесно да се извеждат променливи вътре в JavaScript, а правилното екраниране се осигурява от Latte. - -Променливата не е задължително да бъде низ, поддържа се всеки тип данни, който след това се кодира като JSON: - -```latte -{var $foo = ['hello', true, 1]} - -``` - -Генерира: - -```latte - -``` - -Това е и причината, поради която около променливата **не се пишат кавички**: Latte ги добавя само при низове. А ако искате да вмъкнете низова променлива в друг низ, просто ги свържете: - -```latte - -``` - - -Филтри ------- - -Извежданият израз може да бъде модифициран с [филтър |syntax#Филтри]. Така например низът се преобразува в главни букви и се скъсява до максимум 30 знака: - -```latte -{$string|upper|truncate:30} -``` - -Филтрите могат да се използват и върху части от израза по следния начин: - -```latte -{$left . ($middle|upper) . $right} -``` - - -Условия -======= - - -`{if}` `{elseif}` `{else}` --------------------------- - -Условията се държат по същия начин като техните аналози в PHP. Можете да използвате в тях същите изрази, които познавате от PHP, не е необходимо да учите нов език. - -```latte -{if $product->inStock > Stock::Minimum} - Налично -{elseif $product->isOnWay()} - На път -{else} - Не е налично -{/if} -``` - -Както всеки двоен таг, така и двойката `{if} ... {/if}` може да се записва и във вид на [n:атрибут |syntax#n:атрибути], например: - -```latte -

    Налични {$count} броя

    -``` - -Знаете ли, че към n:атрибутите можете да добавите префикс `tag-`? Тогава условието ще се отнася само до извеждането на HTML таговете, а съдържанието между тях ще се изведе винаги: - -```latte -
    Hello - -{* извежда 'Hello', когато $clickable е невярно *} -{* извежда 'Hello', когато $clickable е вярно *} -``` - -Страхотно. - - -`n:else` .{data-version:3.0.11} -------------------------------- - -Ако условието `{if} ... {/if}` запишете във вид на [n:атрибут |syntax#n:атрибути], имате възможност да посочите и алтернативен клон с помощта на `n:else`: - -```latte -Налични {$count} броя - -не е налично -``` - -Атрибутът `n:else` може да се използва също и в двойка с [`n:ifset` |#ifset elseifset], [`n:foreach` |#foreach], [`n:try` |#try], [#`n:ifcontent`] и [`n:ifchanged` |#ifchanged]. - - -`{/if $cond}` -------------- - -Може би ще ви изненада, че изразът в условието `{if}` може да се посочи и в затварящия таг. Това е полезно в ситуации, когато при отваряне на условието все още не знаем стойността му. Нека го наречем отложено решение. - -Например започваме да извеждаме таблица със записи от база данни и едва след завършване на извеждането осъзнаваме, че в базата данни не е имало нито един запис. Тогава поставяме условие за това в затварящия таг `{/if}` и ако няма нито един запис, нищо от това няма да се изведе: - -```latte -{if} -

    Извеждане на редове от базата данни

    - - - {foreach $resultSet as $row} - ... - {/foreach} -
    -{/if isset($row)} -``` - -Умно, нали? - -В отложеното условие може да се използва и `{else}`, но не и `{elseif}`. - - -`{ifset}` `{elseifset}` ------------------------ - -.[note] -Вижте също [`{ifset block}` |template-inheritance#Проверка за съществуване на блокове] - -С помощта на условието `{ifset $var}` установяваме дали променливата (или няколко променливи) съществува и има стойност, различна от *null*. Всъщност това е същото като `if (isset($var))` в PHP. Както всеки двоен таг, тя може да се записва и във вид на [n:атрибут |syntax#n:атрибути], така че нека го покажем като пример: - -```latte - -``` - - -`{ifchanged}` -------------- - -`{ifchanged}` проверява дали стойността на променливата се е променила от последната итерация в цикъла (foreach, for или while). - -Ако в тага посочим една или повече променливи, ще се проверява дали някоя от тях се е променила и според това ще се изведе съдържанието. Например следващият пример ще изведе първата буква на името като заглавие всеки път, когато при извеждане на имената тя се промени: - -```latte -{foreach ($names|sort) as $name} - {ifchanged $name[0]}

    {$name[0]}

    {/ifchanged} - -

    {$name}

    -{/foreach} -``` - -Ако обаче не посочим никакъв аргумент, ще се проверява рендираното съдържание спрямо предишното му състояние. Това означава, че в предишния пример можем спокойно да пропуснем аргумента в тага. И разбира се, можем да използваме и [n:атрибут |syntax#n:атрибути]: - -```latte -{foreach ($names|sort) as $name} -

    {$name[0]}

    - -

    {$name}

    -{/foreach} -``` - -Вътре в `{ifchanged}` може също да се посочи клауза `{else}`. - - -`{switch}` `{case}` `{default}` -------------------------------- -Сравнява стойност с няколко възможности. Това е аналог на условния оператор `switch`, който познавате от PHP. Въпреки това Latte го подобрява: - -- използва стриктно сравнение (`===`) -- не се нуждае от `break` - -Това е точен еквивалент на структурата `match`, която идва с PHP 8.0. - -```latte -{switch $transport} - {case train} - С влак - {case plane} - Със самолет - {default} - Друго -{/switch} -``` - -Клаузата `{case}` може да съдържа няколко стойности, разделени със запетаи: - -```latte -{switch $status} -{case $status::New}нова позиция -{case $status::Sold, $status::Unknown}не е налична -{/switch} -``` - - -Цикли -===== - -В Latte ще намерите всички цикли, които познавате от PHP: foreach, for и while. - - -`{foreach}` ------------ - -Цикълът се записва по абсолютно същия начин като в PHP: - -```latte -{foreach $langs as $code => $lang} - {$lang} -{/foreach} -``` - -Освен това има няколко удобни трика, за които ще поговорим сега. - -Например Latte проверява дали създадените променливи случайно не презаписват глобални променливи със същото име. Това спасява ситуации, когато разчитате, че в `$lang` е текущият език на страницата, и не осъзнавате, че `foreach $langs as $lang` ви е презаписало тази променлива. - -Цикълът foreach може също много елегантно и икономично да се запише с помощта на [n:атрибут |syntax#n:атрибути]: - -```latte -
      -
    • {$item->name}
    • -
    -``` - -Знаете ли, че към n:атрибутите можете да добавите префикс `inner-`? Тогава в цикъла ще се повтаря само вътрешността на елемента: - -```latte -
    -

    {$item->title}

    -

    {$item->description}

    -
    -``` - -Така ще се изведе нещо като: - -```latte -
    -

    Foo

    -

    Lorem ipsum.

    -

    Bar

    -

    Sit dolor.

    -
    -``` - - -`{else}` .{toc: foreach-else} ------------------------------ - -Вътре в цикъла `foreach` може да се посочи клауза `{else}`, чието съдържание се показва, ако цикълът е празен: - -```latte -
      - {foreach $people as $person} -
    • {$person->name}
    • - {else} -
    • Съжаляваме, в този списък няма потребители
    • - {/foreach} -
    -``` - - -`$iterator` ------------ - -Вътре в цикъла `foreach` Latte създава променливата `$iterator`, с помощта на която можем да установяваме полезна информация за протичащия цикъл: - -- `$iterator->first` - това първото преминаване през цикъла ли е? -- `$iterator->last` - това последното преминаване ли е? -- `$iterator->counter` - кое по ред е това преминаване, броейки от едно? -- `$iterator->counter0` - кое по ред е това преминаване, броейки от нула? -- `$iterator->odd` - това нечетно преминаване ли е? -- `$iterator->even` - това четно преминаване ли е? -- `$iterator->parent` - итераторът, обгръщащ текущия -- `$iterator->nextValue` - следващият елемент в цикъла -- `$iterator->nextKey` - ключът на следващия елемент в цикъла - - -```latte -{foreach $rows as $row} - {if $iterator->first}{/if} - - - - - - - {if $iterator->last}
    {$row->name}{$row->email}
    {/if} -{/foreach} -``` - -Latte е умно и `$iterator->last` работи не само при масиви, но и когато цикълът преминава през общ итератор, където броят на елементите не е известен предварително. - - -`{first}` `{last}` `{sep}` --------------------------- - -Тези тагове могат да се използват вътре в цикъла `{foreach}`. Съдържанието на `{first}` се рендира, ако това е първото преминаване. Съдържанието на `{last}` се рендира… дали ще познаете? Да, ако това е последното преминаване. Всъщност това са съкращения за `{if $iterator->first}` и `{if $iterator->last}`. - -Таговете могат също елегантно да се използват като [n:атрибут |syntax#n:атрибути]: - -```latte -{foreach $rows as $row} - {first}

    Списък с имена

    {/first} - -

    {$row->name}

    - -
    -{/foreach} -``` - -Съдържанието на тага `{sep}` се рендира, ако преминаването не е последно, така че е подходящо за рендиране на разделители, например запетаи между извежданите елементи: - -```latte -{foreach $items as $item} {$item} {sep}, {/sep} {/foreach} -``` - -Това е доста практично, нали? - - -`{iterateWhile}` ----------------- - -Опростява групирането на линейни данни по време на итерация в цикъл foreach, като извършва итерацията във вложен цикъл, докато условието е изпълнено. [Прочетете подробно ръководство|cookbook/grouping]. - -Може също елегантно да замени `{first}` и `{last}` в примера по-горе: - -```latte -{foreach $rows as $row} - - - {iterateWhile} - - - - - {/iterateWhile true} - -
    {$row->name}{$row->email}
    -{/foreach} -``` - -Вижте също филтрите [batch |filters#batch] и [group |filters#group]. - - -`{for}` -------- - -Цикълът се записва по абсолютно същия начин като в PHP: - -```latte -{for $i = 0; $i < 10; $i++} - Елемент {$i} -{/for} -``` - -Тагът може също да се използва като [n:атрибут |syntax#n:атрибути]: - -```latte -

    {$i}

    -``` - - -`{while}` ---------- - -Цикълът отново се записва по абсолютно същия начин като в PHP: - -```latte -{while $row = $result->fetch()} - {$row->title} -{/while} -``` - -Или като [n:атрибут |syntax#n:атрибути]: - -```latte - - {$row->title} - -``` - -Възможен е и вариант с условие в затварящия таг, който съответства на PHP цикъла do-while: - -```latte -{while} - {$item->title} -{/while $item = $item->getNext()} -``` - - -`{continueIf}` `{skipIf}` `{breakIf}` -------------------------------------- - -За управление на всеки цикъл могат да се използват таговете `{continueIf ?}` и `{breakIf ?}`, които преминават към следващия елемент съответно прекратяват цикъла при изпълнение на условието: - -```latte -{foreach $rows as $row} - {continueIf $row->date < $now} - {breakIf $row->parent === null} - ... -{/foreach} -``` - - -Тагът `{skipIf}` е много подобен на `{continueIf}`, но не увеличава брояча `$iterator->counter`, така че ако го извеждаме и същевременно пропуснем някои елементи, няма да има дупки в номерирането. Също така клаузата `{else}` се рендира, когато пропуснем всички елементи. - -```latte -
      - {foreach $people as $person} - {skipIf $person->age < 18} -
    • {$iterator->counter}. {$person->name}
    • - {else} -
    • Съжаляваме, в този списък няма възрастни
    • - {/foreach} -
    -``` - - -`{exitIf}` .{data-version:3.0.5} --------------------------------- - -Прекратява рендирането на шаблона или блока при изпълнение на условието (т.нар. "early exit"). - -```latte -{exitIf !$messages} - -

    Съобщения

    -
    - {$message} -
    -``` - - -Вмъкване на шаблон -================== - - -`{include 'file.latte'}` .{toc: include} ----------------------------------------- - -.[note] -Вижте също [`{include block}` |template-inheritance#Рендиране на блокове] - -Тагът `{include}` зарежда и рендира посочения шаблон. Ако говорим на езика на нашия любим език PHP, това е нещо като: - -```php - -``` - -Вмъкнатите шаблони нямат достъп до променливите на активния контекст, имат достъп само до глобалните променливи. - -Можете да предавате променливи към вмъкнатия шаблон по следния начин: - -```latte -{include 'template.latte', foo: 'bar', id: 123} -``` - -Името на шаблона може да бъде всякакъв израз в PHP: - -```latte -{include $someVar} -{include $ajax ? 'ajax.latte' : 'not-ajax.latte'} -``` - -Вмъкнатото съдържание може да бъде модифицирано с помощта на [филтри |syntax#Филтри]. Следващият пример премахва целия HTML и променя регистъра на буквите: - -```latte -{include 'heading.latte' |stripHtml|capitalize} -``` - -По подразбиране [наследяването на шаблони|template-inheritance] в този случай не играе никаква роля. Въпреки че във включения шаблон можем да използваме блокове, не се извършва замяна на съответните блокове в шаблона, в който се включва. Мислете за включените шаблони като за самостоятелни изолирани части от страници или модули. Това поведение може да се промени с помощта на модификатора `with blocks`: - -```latte -{include 'template.latte' with blocks} -``` - -Връзката между името на файла, посочено в тага, и файла на диска е въпрос на [loader|loaders]. - - -`{sandbox}` ------------ - -При вмъкване на шаблон, създаден от краен потребител, трябва да обмислите sandbox режим (повече информация в [документация за sandbox |sandbox]): - -```latte -{sandbox 'untrusted.latte', level: 3, data: $menu} -``` - - -`{block}` -========= - -.[note] -Вижте също [`{block name}` |template-inheritance#Блокове] - -Блоковете без име служат като начин за прилагане на [филтри |syntax#Филтри] към част от шаблона. Например така може да се приложи филтърът [strip |filters#spaceless], който премахва излишните интервали: - -```latte -{block|strip} -
      -
    • Hello World
    • -
    -{/block} -``` - - -Управление на изключения -======================== - - -`{try}` -------- - -Благодарение на този таг е изключително лесно да се създават здрави шаблони. - -Ако при рендиране на блока `{try}` възникне изключение, целият блок се отхвърля и рендирането ще продължи след него: - -```latte -{try} -
      - {foreach $twitter->loadTweets() as $tweet} -
    • {$tweet->text}
    • - {/foreach} -
    -{/try} -``` - -Съдържанието в незадължителната клауза `{else}` се рендира само когато възникне изключение: - -```latte -{try} -
      - {foreach $twitter->loadTweets() as $tweet} -
    • {$tweet->text}
    • - {/foreach} -
    - {else} -

    Съжаляваме, не успяхме да заредим туитовете.

    -{/try} -``` - -Тагът може също да се използва като [n:атрибут |syntax#n:атрибути]: - -```latte -
      - ... -
    -``` - -Възможно е също да се дефинира собствен [персонализиран обработчик на изключения |develop#Exception handler], например за логване. - - -`{rollback}` ------------- - -Блокът `{try}` може да бъде спрян и пропуснат също и ръчно с помощта на `{rollback}`. Благодарение на това не е необходимо предварително да проверявате всички входни данни и чак по време на рендирането можете да решите, че изобщо не искате да рендирате обекта: - -```latte -{try} -
      - {foreach $people as $person} - {skipIf $person->age < 18} -
    • {$person->name}
    • - {else} - {rollback} - {/foreach} -
    -{/try} -``` - - -Променливи -========== - - -`{var}` `{default}` -------------------- - -Нови променливи създаваме в шаблона с тага `{var}`: - -```latte -{var $name = 'John Smith'} -{var $age = 27} - -{* Множествена декларация *} -{var $name = 'John Smith', $age = 27} -``` - -Тагът `{default}` работи подобно, но създава променливи само тогава, когато те не съществуват. Ако променливата вече съществува и съдържа стойност `null`, тя няма да бъде презаписана: - -```latte -{default $lang = 'bg'} -``` - -Можете да посочвате и [типове променливи|type-system]. Засега те са информативни и Latte не ги проверява. - -```latte -{var string $name = $article->getTitle()} -{default int $id = 0} -``` - - -`{parameters}` --------------- - -Точно както функцията декларира своите параметри, така и шаблонът може в началото да декларира своите променливи: - -```latte -{parameters - $a, - ?int $b, - int|string $c = 10 -} -``` - -Променливите `$a` и `$b` без посочена стойност по подразбиране автоматично имат стойност по подразбиране `null`. Декларираните типове засега са информативни и Latte не ги проверява. - -Други променливи освен декларираните не се пренасят в шаблона. С това се различава от тага `{default}`. - - -`{capture}` ------------ - -Улавя изхода в променлива: - -```latte -{capture $var} -
      -
    • Hello World
    • -
    -{/capture} - -

    Уловено: {$var}

    -``` - -Тагът може, подобно на всеки двоен таг, да се запише и като [n:атрибут |syntax#n:атрибути]: - -```latte -
      -
    • Hello World
    • -
    -``` - -HTML изходът се записва в променливата `$var` във вид на обект `Latte\Runtime\Html`, за да [не се стигне до нежелано екраниране |develop#Изключване на автоматичното екраниране на променлива] при извеждане. - - -Други -===== - - -`{contentType}` ---------------- - -С тага указвате какъв тип съдържание представлява шаблонът. Възможностите са: - -- `html` (тип по подразбиране) -- `xml` -- `javascript` -- `css` -- `calendar` (iCal) -- `text` - -Неговото използване е важно, защото настройва [контекстно-чувствително екраниране |safety-first#Контекстно-чувствително екраниране] и само така може да екранира правилно. Например `{contentType xml}` превключва в режим XML, `{contentType text}` напълно изключва екранирането. - -Ако параметърът е пълноценен MIME тип, като например `application/xml`, тогава още допълнително изпраща HTTP хедър `Content-Type` към браузъра: - -```latte -{contentType application/xml} - - - - RSS feed - - ... - - - -``` - - -`{debugbreak}` --------------- - -Означава място, където изпълнението на програмата ще бъде спряно и ще се стартира дебъгерът, за да може програмистът да извърши инспекция на средата на изпълнение и да установи дали програмата работи според очакванията. Поддържа [Xdebug |https://xdebug.org/]. Може да се добави условие, което определя кога програмата трябва да бъде спряна. - -```latte -{debugbreak} {* спира програмата *} - -{debugbreak $counter == 1} {* спира програмата при изпълнение на условието *} -``` - - -`{do}` ------- - -Изпълнява PHP код и нищо не извежда. Както при всички други тагове, под PHP код се разбира един израз, вижте [ограничения на PHP |syntax#Ограничения на PHP в Latte]. - -```latte -{do $num++} -``` - - -`{dump}` --------- - -Извежда променлива или текущия контекст. - -```latte -{dump $name} {* Извежда променливата $name *} - -{dump} {* Извежда всички текущо дефинирани променливи *} -``` - -.[caution] -Изисква библиотеката [Tracy|tracy:]. - - -`{php}` -------- - -Позволява изпълнението на всякакъв PHP код. Тагът трябва да бъде активиран с помощта на разширението [RawPhpExtension |develop#RawPhpExtension]. - - -`{spaceless}` -------------- - -Премахва излишното празно пространство от изхода. Работи подобно на филтъра [spaceless |filters#spaceless]. - -```latte -{spaceless} -
      -
    • Hello
    • -
    -{/spaceless} -``` - -Генерира - -```latte -
    • Hello
    -``` - -Тагът може също да се запише като [n:атрибут |syntax#n:атрибути]. - - -`{syntax}` ----------- - -Latte таговете не е задължително да бъдат оградени само с единични къдрави скоби. Можем да изберем и друг разделител, и то дори по време на изпълнение. За това служи `{syntax …}`, където като параметър може да се посочи: - -- double: `{{...}}` -- off: напълно изключва обработката на Latte тагове - -С използването на n:атрибути може да се изключи Latte например само за един блок JavaScript: - -```latte - -``` - -Latte може много удобно да се използва и вътре в JavaScript, достатъчно е да се избягват конструкции като в този пример, когато след `{` веднага следва буква, вижте [Latte в JavaScript или CSS |recipes#Latte в JavaScript или CSS]. - -Ако изключите Latte с помощта на `{syntax off}` (т.е. с таг, а не с n:атрибут), той ще игнорира стриктно всички тагове до `{/syntax}` - - -{trace} -------- - -Хвърля изключение `Latte\RuntimeException`, чийто stack trace е в духа на шаблоните. Тоест вместо извиквания на функции и методи съдържа извиквания на блокове и вмъквания на шаблони. Ако използвате инструмент за прегледно показване на хвърлените изключения, като например [Tracy|tracy:], прегледно ще ви се покаже call stack, включително всички предавани аргументи. - - -Помощници за HTML кодера -======================== - - -n:class -------- - -Благодарение на `n:class` много лесно генерирате HTML атрибута `class` точно според представите. - -Пример: трябва активният елемент да има клас `active`: - -```latte -{foreach $items as $item} - ... -{/foreach} -``` - -И освен това, първият елемент да има класове `first` и `main`: - -```latte -{foreach $items as $item} - ... -{/foreach} -``` - -И всички елементи да имат клас `list-item`: - -```latte -{foreach $items as $item} - ... -{/foreach} -``` - -Удивително просто, нали? - - -n:attr ------- - -Атрибутът `n:attr` може със същата елегантност като [#n:class] да генерира всякакви HTML атрибути. - -```latte -{foreach $data as $item} - -{/foreach} -``` - -В зависимост от върнатите стойности ще изведе напр.: - -```latte - - - - - -``` - - -n:tag ------ - -Атрибутът `n:tag` може динамично да променя името на HTML елемента. - -```latte -

    {$title}

    -``` - -Ако `$heading === null`, ще се изведе без промяна тагът `

    `. Иначе името на елемента ще се промени на стойността на променливата, така че за `$heading === 'h3'` ще се изведе: - -```latte -

    ...

    -``` - -Тъй като Latte е сигурна система за шаблони, тя проверява дали новото име на тага е валидно и не съдържа никакви нежелани или вредни стойности. - - -n:ifcontent ------------ - -Предотвратява извеждането на празен HTML елемент, т.е. елемент, който не съдържа нищо освен интервали. - -```latte -
    -
    {$error}
    -
    -``` - -Извежда в зависимост от стойността на променливата `$error`: - -```latte -{* $error = '' *} -
    -
    - -{* $error = 'Required' *} -
    -
    Required
    -
    -``` - - -Преводи -======= - -За да работят таговете за превод, е необходимо да [активирате преводача |develop#TranslatorExtension]. За превод можете да използвате и филтъра [`translate` |filters#translate]. - - -`{_...}` --------- - -Превежда стойности на други езици. - -```latte -{_'Кошница'} -{_$item} -``` - -На преводача могат да се предават и други параметри: - -```latte -{_'Кошница', domain: order} -``` - - -`{translate}` -------------- - -Превежда части от шаблона: - -```latte -

    {translate}Поръчка{/translate}

    - -{translate domain: order}Lorem ipsum ...{/translate} -``` - -Тагът може също да се запише като [n:атрибут |syntax#n:атрибути], за превод на вътрешността на елемента: - -```latte -

    Поръчка

    -``` diff --git a/latte/bg/template-inheritance.texy b/latte/bg/template-inheritance.texy deleted file mode 100644 index b3a576ed80..0000000000 --- a/latte/bg/template-inheritance.texy +++ /dev/null @@ -1,748 +0,0 @@ -Наследяване и повторно използване на шаблони -******************************************** - -.[perex] -Механизмите за повторно използване и наследяване на шаблони ще повишат вашата производителност, тъй като всеки шаблон съдържа само своето уникално съдържание, а повтарящите се елементи и структури се използват повторно. Представяме три концепции: [#Наследяване на лейаут], [#Хоризонтално повторно използване] и [#Единично наследяване]. - -Концепцията за наследяване на шаблони в Latte е подобна на наследяването на класове в PHP. Вие дефинирате **родителски шаблон**, от който други **дъщерни шаблони** могат да наследят и да презапишат части от родителския шаблон. Това работи чудесно, когато елементите споделят обща структура. Звучи ли сложно? Не се притеснявайте, много е лесно. - - -Наследяване на лейаут `{layout}` .{toc: Наследяване на лейаут} -============================================================== - -Нека разгледаме наследяването на шаблон за оформление, т.е. лейаут, директно с пример. Това е родителски шаблон, който ще наречем например `layout.latte` и който дефинира скелета на HTML документ: - -```latte - - - - {block title}{/block} - - - -
    - {block content}{/block} -
    - - - -``` - -Таговете `{block}` дефинират три блока, които дъщерните шаблони могат да запълнят. Тагът block прави само това, че обявява, че това място може да бъде презаписано от дъщерен шаблон чрез дефиниране на собствен блок със същото име. - -Дъщерният шаблон може да изглежда така: - -```latte -{layout 'layout.latte'} - -{block title}My amazing blog{/block} - -{block content} -

    Welcome to my awesome homepage.

    -{/block} -``` - -Ключът тук е тагът `{layout}`. Той казва на Latte, че този шаблон "разширява" друг шаблон. Когато Latte рендира този шаблон, първо намира родителския шаблон - в този случай `layout.latte`. - -В този момент Latte забелязва трите блокови тага в `layout.latte` и заменя тези блокове със съдържанието на дъщерния шаблон. Тъй като дъщерният шаблон не е дефинирал блока *footer*, вместо това се използва съдържанието от родителския шаблон. Съдържанието в тага `{block}` в родителския шаблон винаги се използва като резервно. - -Изходът може да изглежда така: - -```latte - - - - My amazing blog - - - -
    -

    Welcome to my awesome homepage.

    -
    - - - -``` - -В дъщерния шаблон блоковете могат да бъдат поставени само на най-високо ниво или вътре в друг блок, т.е.: - -```latte -{block content} -

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

    -{/block} -``` - -Също така, блок винаги ще бъде създаден, независимо дали заобикалящото го условие `{if}` се оценява като вярно или невярно. Така че, дори да не изглежда така, този шаблон ще дефинира блока. - -```latte -{if false} - {block head} - - {/block} -{/if} -``` - -Ако искате изходът вътре в блока да се показва условно, използвайте следното вместо това: - -```latte -{block head} - {if $condition} - - {/if} -{/block} -``` - -Пространството извън блоковете в дъщерния шаблон се изпълнява преди рендирането на шаблона на лейаута, така че можете да го използвате за дефиниране на променливи като `{var $foo = bar}` и за разпространение на данни по цялата верига на наследяване: - -```latte -{layout 'layout.latte'} -{var $robots = noindex} - -... -``` - - -Многостепенно наследяване -------------------------- -Можете да използвате толкова нива на наследяване, колкото са ви необходими. Обичайният начин за използване на наследяването на лейаути е следният тристепенен подход: - -1) Създайте шаблон `layout.latte`, който съдържа основния скелет на външния вид на сайта. -2) Създайте шаблон `layout-SECTIONNAME.latte` за всяка секция на вашия сайт. Например `layout-news.latte`, `layout-blog.latte` и т.н. Всички тези шаблони разширяват `layout.latte` и включват стилове и дизайн, специфични за всяка секция. -3) Създайте индивидуални шаблони за всеки тип страница, например новинарска статия или публикация в блог. Тези шаблони разширяват съответния шаблон на секцията. - - -Динамично наследяване ---------------------- -Като име на родителския шаблон може да се използва променлива или произволен PHP израз, така че наследяването може да се държи динамично: - -```latte -{layout $standalone ? 'minimum.latte' : 'layout.latte'} -``` - -Можете също да използвате Latte API за [автоматично |develop#Автоматично намиране на лейаут] избиране на шаблон за лейаут. - - -Съвети ------- -Ето няколко съвета за работа с наследяване на лейаути: - -- Ако използвате `{layout}` в шаблон, той трябва да бъде първият таг на шаблона в този шаблон. - -- Лейаутът може да се [търси автоматично |develop#Автоматично намиране на лейаут] (както например в [презентери |application:templates#Търсене на шаблони]). В такъв случай, ако шаблонът не трябва да има лейаут, той го обявява с тага `{layout none}`. - -- Тагът `{layout}` има псевдоним `{extends}`. - -- Името на файла на лейаута зависи от [loader |loaders]. - -- Можете да имате толкова блокове, колкото искате. Помнете, че дъщерните шаблони не е необходимо да дефинират всички родителски блокове, така че можете да попълните разумни стойности по подразбиране в няколко блока и след това да дефинирате само тези, от които се нуждаете по-късно. - - -Блокове `{block}` .{toc: Блокове} -================================= - -.[note] -Вижте също анонимен [`{block}` |tags#block] - -Блокът представлява начин за промяна на начина, по който се рендира определена част от шаблона, но по никакъв начин не се намесва в логиката около него. В следващия пример ще покажем как работи блокът, но също и как не работи: - -```latte .{file: parent.latte} -{foreach $posts as $post} -{block post} -

    {$post->title}

    -

    {$post->body}

    -{/block} -{/foreach} -``` - -Ако рендирате този шаблон, резултатът ще бъде абсолютно същият със или без таговете `{block}`. Блоковете имат достъп до променливи от външни обхвати. Те просто дават възможност да бъдат презаписани от дъщерен шаблон: - -```latte .{file: child.latte} -{layout 'parent.Latte'} - -{block post} -
    -
    {$post->title}
    -
    {$post->text}
    -
    -{/block} -``` - -Сега, при рендиране на дъщерния шаблон, цикълът ще използва блока, дефиниран в дъщерния шаблон `child.Latte`, вместо блока, дефиниран в `parent.Latte`; изпълненият шаблон тогава е еквивалентен на следното: - -```latte -{foreach $posts as $post} -
    -
    {$post->title}
    -
    {$post->text}
    -
    -{/foreach} -``` - -Ако обаче създадем нова променлива вътре в именуван блок или заменим стойността на съществуваща, промяната ще бъде видима само вътре в блока: - -```latte -{var $foo = 'foo'} -{block post} - {do $foo = 'new value'} - {var $bar = 'bar'} -{/block} - -foo: {$foo} // отпечатва: foo -bar: {$bar ?? 'not defined'} // отпечатва: not defined -``` - -Съдържанието на блока може да бъде модифицирано с помощта на [филтри |syntax#Филтри]. Следният пример премахва целия HTML и променя регистъра на буквите: - -```latte -{block title|stripHtml|capitalize}...{/block} -``` - -Тагът може да бъде записан и като [n:атрибут |syntax#n:атрибути]: - -```latte -
    - ... -
    -``` - - -Локални блокове ---------------- - -Всеки блок презаписва съдържанието на родителския блок със същото име – с изключение на локалните блокове. В класовете това би било нещо като частни методи. По този начин можете да създавате шаблон, без да се притеснявате, че поради съвпадение на имената на блоковете, те ще бъдат презаписани от друг шаблон. - -```latte -{block local helper} - ... -{/block} -``` - - -Рендиране на блокове `{include}` .{toc: Рендиране на блокове} -------------------------------------------------------------- - -.[note] -Вижте също [`{include file}` |tags#include] - -За да изведете блок на определено място, използвайте тага `{include blockname}`: - -```latte -{block title}{/block} - -

    {include title}

    -``` - -Можете също да изведете блок от друг шаблон: - -```latte -{include footer from 'main.latte'} -``` - -Рендираният блок няма достъп до променливите на активния контекст, освен ако блокът не е дефиниран в същия файл, в който е вмъкнат. Въпреки това, той има достъп до глобалните променливи. - -Можете да предавате променливи към блока по този начин: - -```latte -{include footer, foo: bar, id: 123} -``` - -Като име на блока може да се използва променлива или произволен израз в PHP. В такъв случай, преди променливата добавяме ключовата дума `block`, за да може Latte още по време на компилация да знае, че става въпрос за блок, а не за [вмъкване на шаблон |tags#include], чието име също може да бъде в променлива: - -```latte -{var $name = footer} -{include block $name} -``` - -Блокът може да бъде рендиран и вътре в себе си, което е полезно например при рендиране на дървовидна структура: - -```latte -{define menu, $items} -
      - {foreach $items as $item} -
    • - {if is_array($item)} - {include menu, $item} - {else} - {$item} - {/if} -
    • - {/foreach} -
    -{/define} -``` - -Вместо `{include menu, ...}` можем да напишем `{include this, ...}`, където `this` означава текущия блок. - -Рендираният блок може да бъде модифициран с помощта на [филтри |syntax#Филтри]. Следният пример премахва целия HTML и променя регистъра на буквите: - -```latte -{include heading|stripHtml|capitalize} -``` - - -Родителски блок ---------------- - -Ако трябва да изведете съдържанието на блок от родителския шаблон, използвайте `{include parent}`. Това е полезно, ако искате само да допълните съдържанието на родителския блок, вместо да го презапишете напълно. - -```latte -{block footer} - {include parent} - GitHub - Twitter -{/block} -``` - - -Дефиниции `{define}` .{toc: Дефиниции} --------------------------------------- - -Освен блокове, в Latte съществуват и "дефиниции". В обикновените езици за програмиране бихме ги сравнили с функции. Те са полезни за повторно използване на фрагменти от шаблони, за да не се повтаряте. - -Latte се стреми да прави нещата прости, така че по същество дефинициите са същите като блоковете и **всичко, което е казано за блоковете, важи и за дефинициите**. Те се различават от блоковете по това, че: - -1) са затворени в тагове `{define}` -2) рендират се едва когато ги вмъкнете чрез `{include}` -3) могат да им се дефинират параметри, подобно на функциите в PHP - -```latte -{block foo}

    Hello

    {/block} -{* отпечатва:

    Hello

    *} - -{define bar}

    World

    {/define} -{* не отпечатва нищо *} - -{include bar} -{* отпечатва:

    World

    *} -``` - -Представете си, че имате помощен шаблон с колекция от дефиниции за това как да рисувате HTML форми. - -```latte .{file: forms.latte} -{define input, $name, $value, $type = 'text'} - -{/define} - -{define textarea, $name, $value} - -{/define} -``` - -Аргументите винаги са незадължителни със стойност по подразбиране `null`, освен ако не е посочена стойност по подразбиране (тук `'text'` е стойността по подразбиране за `$type`). Могат да се декларират и типове параметри: `{define input, string $name, ...}`. - -Шаблонът с дефиниции се зарежда с помощта на [`{import}` |#Хоризонтално повторно използване]. Самите дефиниции се рендират [по същия начин като блоковете |#Рендиране на блокове]: - -```latte -

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

    -

    {include textarea, 'comment'}

    -``` - -Дефинициите нямат достъп до променливите на активния контекст, но имат достъп до глобалните променливи. - - -Динамични имена на блокове --------------------------- - -Latte позволява голяма гъвкавост при дефинирането на блокове, тъй като името на блока може да бъде произволен PHP израз. Този пример дефинира три блока с имена `hi-Peter`, `hi-John` и `hi-Mary`: - -```latte .{file: parent.latte} -{foreach [Peter, John, Mary] as $name} - {block "hi-$name"}Hi, I am {$name}.{/block} -{/foreach} -``` - -В дъщерния шаблон тогава можем да предефинираме например само един блок: - -```latte .{file: child.latte} -{block hi-John}Hello. I am {$name}.{/block} -``` - -Така изходът ще изглежда по следния начин: - -```latte -Hi, I am Peter. -Hello. I am John. -Hi, I am Mary. -``` - - -Проверка за съществуване на блокове `{ifset}` .{toc: Проверка за съществуване на блокове} ------------------------------------------------------------------------------------------ - -.[note] -Вижте също [`{ifset $var}` |tags#ifset elseifset] - -С помощта на теста `{ifset blockname}` проверяваме дали в текущия контекст съществува блок (или повече блокове): - -```latte -{ifset footer} - ... -{/ifset} - -{ifset footer, header, main} - ... -{/ifset} -``` - -Като име на блока може да се използва променлива или произволен израз в PHP. В такъв случай, преди променливата добавяме ключовата дума `block`, за да е ясно, че не става въпрос за тест за съществуване на [променливи |tags#ifset elseifset]: - -```latte -{ifset block $name} - ... -{/ifset} -``` - -Съществуването на блокове се проверява и от функцията [`hasBlock()` |functions#hasBlock]: - -```latte -{if hasBlock(header) || hasBlock(footer)} - ... -{/if} -``` - - -Съвети ------- -Няколко съвета за работа с блокове: - -- Последният блок на най-високо ниво не е необходимо да има затварящ таг (блокът завършва с края на документа). Това опростява писането на дъщерни шаблони, които съдържат един основен блок. - -- За по-добра четимост можете да посочите името на блока в тага `{/block}`, например `{/block footer}`. Името обаче трябва да съвпада с името на блока. В по-големи шаблони тази техника ще ви помогне да видите кои тагове на блокове се затварят. - -- В един и същ шаблон не можете директно да дефинирате няколко тага на блокове със същото име. Това обаче може да се постигне с помощта на [#динамични имена на блокове]. - -- Можете да използвате [n:атрибути |syntax#n:атрибути] за дефиниране на блокове като `

    Welcome to my awesome homepage

    ` - -- Блоковете могат да се използват и без имена, само за прилагане на [филтри |syntax#Филтри]: `{block|strip} hello {/block}` - - -Хоризонтално повторно използване `{import}` .{toc: Хоризонтално повторно използване} -==================================================================================== - -Хоризонталното повторно използване е третият механизъм в Latte за повторно използване и наследяване. Той позволява зареждане на блокове от други шаблони. Това е подобно на създаването на файл с помощни функции в PHP, който след това зареждаме с помощта на `require`. - -Въпреки че наследяването на лейаута на шаблона е една от най-мощните функции на Latte, то е ограничено до просто наследяване - шаблонът може да разшири само един друг шаблон. Хоризонталното повторно използване е начин за постигане на множествено наследяване. - -Нека имаме файл с дефиниции на блокове: - -```latte .{file: blocks.latte} -{block sidebar}...{/block} - -{block menu}...{/block} -``` - -С помощта на командата `{import}` импортираме всички блокове и [#Дефиниции], дефинирани в `blocks.latte`, в друг шаблон: - -```latte .{file: child.latte} -{import 'blocks.latte'} - -{* сега могат да се използват блоковете sidebar и menu *} -``` - -Ако импортирате блокове в родителския шаблон (т.е. използвате `{import}` в `layout.latte`), блоковете ще бъдат достъпни и във всички дъщерни шаблони, което е много практично. - -Шаблонът, предназначен за импортиране (напр. `blocks.latte`), не трябва да [разширява |#Наследяване на лейаут] друг шаблон, т.е. да използва `{layout}`. Въпреки това, той може да импортира други шаблони. - -Тагът `{import}` трябва да бъде първият таг на шаблона след `{layout}`. Името на шаблона може да бъде произволен PHP израз: - -```latte -{import $ajax ? 'ajax.latte' : 'not-ajax.latte'} -``` - -В шаблона можете да използвате толкова команди `{import}`, колкото искате. Ако два импортирани шаблона дефинират един и същ блок, печели първият. Най-висок приоритет обаче има основният шаблон, който може да презапише всеки импортиран блок. - -Съдържанието на презаписаните блокове може да бъде запазено, като вмъкнем блока по същия начин, по който се вмъква [#родителски блок]: - -```latte -{layout 'layout.latte'} - -{import 'blocks.latte'} - -{block sidebar} - {include parent} -{/block} - -{block title}...{/block} -{block content}...{/block} -``` - -В този пример `{include parent}` извиква блока `sidebar` от шаблона `blocks.latte`. - - -Единично наследяване `{embed}` .{toc: Единично наследяване} -=========================================================== - -Единичното наследяване разширява идеята за наследяване на лейаути до нивото на фрагменти от съдържание. Докато наследяването на лейаути работи със "скелета на документа", който се оживява от дъщерни шаблони, единичното наследяване ви позволява да създавате скелети за по-малки единици съдържание и да ги използвате повторно навсякъде, където искате. - -При единичното наследяване ключът е тагът `{embed}`. Той комбинира поведението на `{include}` и `{layout}`. Позволява ви да вмъкнете съдържанието на друг шаблон или блок и по избор да предавате променливи, точно както при `{include}`. Също така ви позволява да презапишете всеки блок, дефиниран вътре във вмъкнатия шаблон, както при използване на `{layout}`. - -Например, нека използваме елемент акордеон. Да разгледаме скелета на елемента, съхранен в шаблона `collapsible.latte`: - -```latte -
    -

    - {block title}{/block} -

    - -
    - {block content}{/block} -
    -
    -``` - -Таговете `{block}` дефинират два блока, които дъщерните шаблони могат да запълнят. Да, както в случая с родителския шаблон при наследяването на лейаути. Виждате също променливата `$modifierClass`. - -Нека използваме нашия елемент в шаблона. Тук идва ред на `{embed}`. Това е изключително мощен таг, който ни позволява да правим всичко: да вмъкваме съдържанието на шаблона на елемента, да добавяме променливи към него и да добавяме блокове със собствен 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} -``` - -Изходът може да изглежда така: - -```latte -
    -

    - Hello World -

    - -
    -

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

    -
    -
    -``` - -Блоковете вътре във вмъкнатите тагове образуват отделен слой, независим от другите блокове. Следователно те могат да имат същото име като блок извън вмъкването и не се влияят по никакъв начин. С помощта на тага [include |#Рендиране на блокове] вътре в таговете `{embed}` можете да вмъквате блокове, създадени тук, блокове от вмъкнатия шаблон (които *не са* [локални |#Локални блокове]), както и блокове от основния шаблон, които от своя страна *са* локални. Можете също да [импортирате блокове |#Хоризонтално повторно използване] от други файлове: - -```latte -{block outer}…{/block} -{block local hello}…{/block} - -{embed 'collapsible.latte', modifierClass: my-style} - {import 'blocks.latte'} - - {block inner}…{/block} - - {block title} - {include inner} {* работи, блокът е дефиниран вътре в embed *} - {include hello} {* работи, блокът е локален в този шаблон *} - {include content} {* работи, блокът е дефиниран във вмъкнатия шаблон *} - {include aBlockDefinedInImportedTemplate} {* работи *} - {include outer} {* не работи! - блокът е във външния слой *} - {/block} -{/embed} -``` - -Вмъкнатите шаблони нямат достъп до променливите на активния контекст, но имат достъп до глобалните променливи. - -С помощта на `{embed}` могат да се вмъкват не само шаблони, но и други блокове, така че предишният пример може да бъде записан по следния начин: - -```latte -{define collapsible} -
    -

    - {block title}{/block} -

    - ... -
    -{/define} - - -{embed collapsible, modifierClass: my-style} - {block title} - Hello World - {/block} - ... -{/embed} -``` - -Ако предадем израз на `{embed}` и не е ясно дали това е име на блок или файл, добавяме ключовата дума `block` или `file`: - -```latte -{embed block $name} ... {/embed} -``` - - -Случаи на употреба -================== - -В Latte съществуват различни типове наследяване и повторно използване на код. Нека обобщим основните концепции за по-голяма яснота: - - -`{include template}` --------------------- - -**Случай на употреба**: Използване на `header.latte` и `footer.latte` вътре в `layout.latte`. - -`header.latte` - -```latte - -``` - -`footer.latte` - -```latte -
    -
    Copyright
    -
    -``` - -`layout.latte` - -```latte -{include 'header.latte'} - -
    {block main}{/block}
    - -{include 'footer.latte'} -``` - - -`{layout}` ----------- - -**Случай на употреба**: Разширяване на `layout.latte` вътре в `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}` ----------- - -**Случай на употреба**: `sidebar.latte` в `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}` ----------- - -**Случай на употреба**: Функции, на които предаваме променливи и те рендират нещо. - -`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}` ---------- - -**Случай на употреба**: Вмъкване на `pagination.latte` в `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/bg/type-system.texy b/latte/bg/type-system.texy deleted file mode 100644 index aa1be010a5..0000000000 --- a/latte/bg/type-system.texy +++ /dev/null @@ -1,73 +0,0 @@ -Типова система -************** - -
    - -Типовата система е ключова за разработването на стабилни приложения. Latte въвежда поддръжка на типове и в шаблоните. Благодарение на това, че знаем какъв тип данни или обект има във всяка променлива, може - -- IDE да подсказва правилно (вижте [интеграция |recipes#Редактори и IDE]) -- статичният анализ да открива грешки - -И двете значително повишават качеството и удобството на разработката. - -
    - -.[note] -Декларираните типове са информативни и Latte в момента не ги проверява. - -Как да започнете да използвате типове? Създайте клас на шаблона, напр. `CatalogTemplateParameters`, представляващ предаваните параметри, техните типове и евентуално стойности по подразбиране: - -```php -class CatalogTemplateParameters -{ - public function __construct( - public string $lang, - /** @var ProductEntity[] */ - public array $products, - public Address $address, - ) {} -} - -$latte->render('template.latte', new CatalogTemplateParameters( - address: $userAddress, - lang: $settings->getLanguage(), - products: $entityManager->getRepository('Product')->findAll(), -)); -``` - -След това в началото на шаблона поставете тага `{templateType}` с пълното име на класа (включително namespace). Това дефинира, че в шаблона има променливи `$lang` и `$products`, включително съответните типове. Типовете на локалните променливи можете да посочите с помощта на таговете [`{var}` |tags#var default], `{varType}`, [`{define}` |template-inheritance#Дефиниции]. - -От този момент IDE може да ви подсказва правилно. - -Как да си спестите работа? Как най-лесно да напишете клас с параметри на шаблон или тагове `{varType}`? Нека бъдат генерирани за вас. За това съществуват двойка тагове `{templatePrint}` и `{varPrint}`. Ако ги поставите в шаблона, вместо нормалното рендиране ще се покаже предложение за код на клас или съответно списък с тагове `{varType}`. След това е достатъчно да маркирате кода с едно кликване и да го копирате в проекта. - - -`{templateType}` ----------------- -Типовете на параметрите, предавани към шаблона, се декларират с помощта на клас: - -```latte -{templateType MyApp\CatalogTemplateParameters} -``` - - -`{varType}` ------------ -Как да декларираме типовете на променливите? За това служат таговете `{varType}` за съществуващи променливи или [`{var}` |tags#var default]: - -```latte -{varType Nette\Security\User $user} -{varType string $lang} -``` - - -`{templatePrint}` ------------------ -Можете също така да генерирате класа с помощта на тага `{templatePrint}`. Ако го поставите в началото на шаблона, вместо нормалното рендиране ще се покаже предложение за клас. След това е достатъчно да маркирате кода с едно кликване и да го копирате в проекта. - - -`{varPrint}` ------------- -Тагът `{varPrint}` ще ви спести време за писане. Ако го поставите в шаблона, вместо нормалното рендиране ще се покаже предложение за тагове `{varType}` за локални променливи. След това е достатъчно да маркирате кода с едно кликване и да го копирате в шаблона. - -Самото `{varPrint}` извежда само локални променливи, които не са параметри на шаблона. Ако искате да изведете всички променливи, използвайте `{varPrint all}`. diff --git a/latte/bg/why-use.texy b/latte/bg/why-use.texy deleted file mode 100644 index 5f87c55670..0000000000 --- a/latte/bg/why-use.texy +++ /dev/null @@ -1,80 +0,0 @@ -Защо да използваме шаблони? -*************************** - - -Защо трябва да използвам система за шаблони в PHP? --------------------------------------------------- - -Защо да използваме система за шаблони в PHP, когато самият PHP е език за шаблони? - -Нека първо накратко да обобщим историята на този език, която е пълна с интересни обрати. Един от първите езици за програмиране, използвани за генериране на HTML страници, беше езикът C. Скоро обаче се оказа, че използването му за тази цел е непрактично. Затова Расмус Лердорф създаде PHP, което улесни генерирането на динамичен HTML с езика C в бекенда. Така PHP първоначално е проектиран като език за шаблони, но с течение на времето придобива допълнителни функции и се превръща в пълноценен език за програмиране. - -Въпреки това той все още функционира и като език за шаблони. В PHP файл може да бъде записана HTML страница, в която с помощта на `` се извеждат променливи и т.н. - -Още в началото на историята на PHP възникна системата за шаблони Smarty, чиято цел беше стриктно да отдели външния вид (HTML/CSS) от логиката на приложението. Тоест, тя умишлено предоставяше по-ограничен език от самия PHP, така че разработчикът да не може например да изпълни заявка към базата данни от шаблона и т.н. От друга страна, тя представляваше допълнителна зависимост в проектите, увеличаваше тяхната сложност и програмистите трябваше да учат новия език Smarty. Такава полза беше спорна и за шаблони продължи да се използва обикновен PHP. - -С течение на времето системите за шаблони започнаха да стават полезни. Те въведоха концепцията за [наследяване |template-inheritance], [режим sandbox|sandbox] и редица други функции, които значително опростиха създаването на шаблони в сравнение с чистия PHP. На преден план излезе темата за сигурността, съществуването на [уязвимости като XSS|safety-first] и необходимостта от [екраниране |#Какво е екраниране]. Системите за шаблони въведоха автоматично екраниране, за да изчезне рискът програмистът да забрави за това и да възникне сериозна дупка в сигурността (след малко ще покажем, че това има известни клопки). - -Ползите от системите за шаблони днес значително надвишават разходите, свързани с тяхното внедряване. Затова има смисъл да се използват. - - -Защо Latte е по-добър от Twig или Blade? ----------------------------------------- - -Причините са няколко – някои са приятни, а други са изключително полезни. Latte е комбинация от приятното с полезното. - -*Първо приятната причина:* Latte има същия [синтаксис като PHP |syntax#Latte разбира PHP]. Различава се само записът на таговете, вместо `` предпочита по-кратките `{` и `}`. Това означава, че не е нужно да учите нов език. Разходите за обучение са минимални. И най-важното, по време на разработката не е нужно постоянно да "превключвате" между езика PHP и езика на шаблона, тъй като и двата са еднакви. За разлика от шаблоните на Twig, които използват езика Python, и програмистът трябва да превключва между два различни езика. - -*А сега изключително полезната причина*: Всички системи за шаблони, като Twig, Blade или Smarty, в хода на еволюцията си въведоха защита срещу XSS под формата на автоматично [екраниране |#Какво е екраниране]. По-точно, автоматично извикване на функцията `htmlspecialchars()`. Създателите на Latte обаче осъзнаха, че това изобщо не е правилното решение. Защото на различни места в документа екранирането се извършва по различни начини. Наивното автоматично екраниране е опасна функция, защото създава фалшиво усещане за сигурност. - -За да бъде автоматичното екраниране функционално и надеждно, то трябва да разпознава къде в документа се извеждат данните (наричаме ги контексти) и според това да избира функцията за екраниране. Тоест, трябва да бъде [контекстно-чувствително |safety-first#Контекстно-чувствително екраниране]. И точно това умее Latte. То разбира HTML. Не възприема шаблона само като низ от знаци, а разбира какво са тагове, атрибути и т.н. И затова екранира по различен начин в HTML текст, по различен начин вътре в HTML таг, по различен начин вътре в JavaScript и т.н. - -Latte е първата и единствена система за шаблони в PHP, която има контекстно-чувствително екраниране. По този начин тя представлява единствената наистина сигурна система за шаблони. - -*И още една приятна причина*: Благодарение на това, че Latte разбира HTML, той предлага други много приятни функции. Например [n:атрибути |syntax#n:атрибути]. Или способността да [проверява връзки |safety-first#Проверка на връзки]. И много други. - - -Какво е екраниране? -------------------- - -Екранирането е процес, който се състои в замяна на знаци със специално значение със съответните им последователности при вмъкване на един низ в друг, за да се предотвратят нежелани явления или грешки. Например, когато вмъкваме низ в HTML текст, в който знакът `<` има специално значение, тъй като обозначава началото на таг, го заменяме със съответната последователност, която е HTML ентитетът `<`. Благодарение на това браузърът правилно ще покаже символа `<`. - -Прост пример за екраниране директно при писане на код в PHP е вмъкването на кавичка в низ, като преди нея напишем обратна наклонена черта. - -Разглеждаме екранирането по-подробно в главата [Как да се защитим от XSS |safety-first#Как да се защитим от XSS]. - - -Възможно ли е да се изпълни заявка към базата данни от шаблон в Latte? ----------------------------------------------------------------------- - -В шаблоните може да се работи с обекти, които програмистът им предава. Следователно, ако програмистът иска, той може да предаде обект на база данни към шаблона и да изпълни заявка върху него. Ако има такова намерение, няма причина да му се пречи. - -Друга ситуация възниква, ако искате да дадете възможност на клиенти или външни кодери да редактират шаблоните. В такъв случай определено не искате те да имат достъп до базата данни. Разбира се, няма да предадете обект на база данни на шаблона, но какво ще стане, ако до нея може да се стигне чрез друг обект? Решението е [режим sandbox|sandbox], който позволява да се дефинира кои методи могат да се извикват в шаблоните. Благодарение на това не е нужно да се притеснявате за нарушаване на сигурността. - - -Какви са основните разлики между системите за шаблони като Latte, Twig и Blade? -------------------------------------------------------------------------------- - -Разликите между системите за шаблони Latte, Twig и Blade се състоят главно в синтаксиса, сигурността и начина на интеграция във фреймуърците - -- Latte: използва синтаксиса на езика PHP, което улеснява ученето и използването. Предоставя върхова защита срещу XSS атаки. -- Twig: използва синтаксиса на езика Python, който доста се различава от PHP. Екранира без разграничаване на контекста. Добре е интегриран в Symfony framework. -- Blade: използва смес от PHP и собствен синтаксис. Екранира без разграничаване на контекста. Тясно е интегриран с функциите и екосистемата на Laravel. - - -Изгодно ли е за фирмите да използват система за шаблони? --------------------------------------------------------- - -Първо, разходите, свързани с обучението, използването и общата полза, се различават значително в зависимост от системата. Системата за шаблони Latte, благодарение на това, че използва синтаксиса на PHP, значително улеснява ученето за програмисти, които вече са запознати с този език. Обикновено отнема няколко часа, докато програмистът се запознае достатъчно с Latte. По този начин намалява разходите за обучение. Същевременно ускорява усвояването на технологията и преди всичко ефективността при ежедневна употреба. - -Освен това Latte предоставя високо ниво на защита срещу уязвимостта XSS благодарение на уникалната технология за контекстно-чувствително екраниране. Тази защита е ключова за гарантиране на сигурността на уеб приложенията и минимизиране на риска от атаки, които биха могли да застрашат потребителите или фирмените данни. Защитата на сигурността на уеб приложенията е важна и за поддържането на добрата репутация на фирмата. Проблемите със сигурността могат да доведат до загуба на доверие от страна на клиентите и да навредят на репутацията на фирмата на пазара. - -Използването на Latte също така намалява общите разходи за разработка и поддръжка на приложението, като улеснява и двете. Следователно използването на система за шаблони определено си заслужава. - - -Влияе ли Latte на производителността на уеб приложенията? ---------------------------------------------------------- - -Въпреки че шаблоните на Latte се обработват бързо, този аспект всъщност няма значение. Причината е, че парсирането на файловете се извършва само веднъж при първото показване. След това те се компилират в PHP код, съхраняват се на диска и се изпълняват при всяка следваща заявка, без да е необходимо повторно компилиране. - -Това е начинът на работа в продукционна среда. По време на разработката шаблоните на Latte се прекомпилират всеки път, когато съдържанието им се промени, така че разработчикът винаги да вижда актуалната версия. diff --git a/latte/el/@home.texy b/latte/el/@home.texy deleted file mode 100644 index eb03b498aa..0000000000 --- a/latte/el/@home.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{maintitle: Latte – τα πιο ασφαλή & πραγματικά διαισθητικά templates για PHP}} -{{description: Το Latte είναι το ασφαλέστερο σύστημα templating για PHP. Αποτρέπει πολλές ευπάθειες ασφαλείας. Θα εκτιμήσετε τη διαισθητική του σύνταξη και θα εκτιμήσετε πολλά χρήσιμα χαρακτηριστικά.}} diff --git a/latte/el/@left-menu.texy b/latte/el/@left-menu.texy deleted file mode 100644 index 2d81db5e5d..0000000000 --- a/latte/el/@left-menu.texy +++ /dev/null @@ -1,24 +0,0 @@ -- [Ξεκινώντας με το Latte |guide] -- [Γιατί να χρησιμοποιήσετε templates; |why-use] -- Έννοιες ⚗️ - - [Η ασφάλεια πάνω απ' όλα |safety-first] - - [Κληρονομικότητα templates |Template Inheritance] - - [Σύστημα τύπων |type-system] - - [Sandbox] - -- Για designers 🎨 - - [Σύνταξη |syntax] - - [Tags |tags] - - [Φίλτρα |filters] - - [Συναρτήσεις |functions] - - [Συμβουλές και κόλπα |recipes] - -- Για developers 🧮 - - [Διαδικασίες developer |develop] - - [Επεκτείνοντας το Latte |extending-latte] - -- [Οδηγοί και διαδικασίες 💡|cookbook/@home] - - [Μετάβαση από το Twig |cookbook/migration-from-twig] - - [… περισσότερα |cookbook/@home] - -- "Playground .[link-external]":https://fiddle.nette.org/latte/ .{padding-top:1em} diff --git a/latte/el/@menu.texy b/latte/el/@menu.texy deleted file mode 100644 index 76aed7bbd8..0000000000 --- a/latte/el/@menu.texy +++ /dev/null @@ -1,12 +0,0 @@ -
      -- [Εισαγωγή |@home] -- [Τεκμηρίωση |guide] -- "GitHub .[link-external]":https://github.com/nette/latte - -
    diff --git a/latte/el/@meta.texy b/latte/el/@meta.texy deleted file mode 100644 index 052351480f..0000000000 --- a/latte/el/@meta.texy +++ /dev/null @@ -1 +0,0 @@ -{{sitename: Latte Τεκμηρίωση}} diff --git a/latte/el/compiler-passes.texy b/latte/el/compiler-passes.texy deleted file mode 100644 index 38c6189be9..0000000000 --- a/latte/el/compiler-passes.texy +++ /dev/null @@ -1,555 +0,0 @@ -Περάσματα Μεταγλώττισης -*********************** - -.[perex] -Τα περάσματα μεταγλώττισης παρέχουν έναν ισχυρό μηχανισμό για την ανάλυση και τροποποίηση προτύπων Latte *μετά* την ανάλυσή τους σε ένα αφηρημένο συντακτικό δέντρο (AST) και *πριν* από τη δημιουργία του τελικού κώδικα PHP. Αυτό επιτρέπει προηγμένη χειραγώγηση προτύπων, βελτιστοποιήσεις, ελέγχους ασφαλείας (όπως το Sandbox) και συλλογή πληροφοριών σχετικά με τα πρότυπα. Αυτός ο οδηγός θα σας καθοδηγήσει στη δημιουργία των δικών σας περασμάτων μεταγλώττισης. - - -Τι είναι ένα πέρασμα μεταγλώττισης; -=================================== - -Για να κατανοήσετε τον ρόλο των περασμάτων μεταγλώττισης, ρίξτε μια ματιά στη [διαδικασία μεταγλώττισης του Latte |custom-tags#Κατανόηση της διαδικασίας μεταγλώττισης]. Όπως μπορείτε να δείτε, τα περάσματα μεταγλώττισης λειτουργούν σε μια κρίσιμη φάση, επιτρέποντας βαθιά παρέμβαση μεταξύ της αρχικής ανάλυσης και της τελικής εξόδου κώδικα. - -Στον πυρήνα του, ένα πέρασμα μεταγλώττισης είναι απλά ένα PHP callable (όπως μια συνάρτηση, στατική μέθοδος ή μέθοδος στιγμιοτύπου), που δέχεται ένα όρισμα: τον ριζικό κόμβο του AST του προτύπου, ο οποίος είναι πάντα ένα στιγμιότυπο της κλάσης `Latte\Compiler\Nodes\TemplateNode`. - -Ο πρωταρχικός στόχος ενός περάσματος μεταγλώττισης είναι συνήθως ένας ή και οι δύο από τους παρακάτω: - -- Ανάλυση: Διασχίστε το AST και συλλέξτε πληροφορίες σχετικά με το πρότυπο (π.χ. βρείτε όλα τα ορισμένα μπλοκ, ελέγξτε τη χρήση συγκεκριμένων tags, διασφαλίστε την τήρηση ορισμένων περιορισμών ασφαλείας). -- Τροποποίηση: Αλλάξτε τη δομή του AST ή τα χαρακτηριστικά των κόμβων (π.χ. προσθέστε αυτόματα χαρακτηριστικά HTML, βελτιστοποιήστε ορισμένους συνδυασμούς tags, αντικαταστήστε τα παρωχημένα tags με νέα, εφαρμόστε κανόνες sandbox). - - -Καταχώριση -========== - -Τα περάσματα μεταγλώττισης καταχωρούνται χρησιμοποιώντας τη μέθοδο [`getPasses()` |extending-latte#getPasses] της [επέκτασης |extending-latte]. Αυτή η μέθοδος επιστρέφει έναν συσχετιστικό πίνακα, όπου τα κλειδιά είναι μοναδικά ονόματα περασμάτων (που χρησιμοποιούνται εσωτερικά και για ταξινόμηση) και οι τιμές είναι PHP callables που υλοποιούν τη λογική του περάσματος. - -```php -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Extension; - -class MyExtension extends Extension -{ - public function getPasses(): array - { - return [ - 'modificationPass' => $this->modifyTemplateAst(...), - // ... άλλα περάσματα ... - ]; - } - - public function modifyTemplateAst(TemplateNode $templateNode): void - { - // Υλοποίηση... - } -} -``` - -Τα περάσματα που καταχωρούνται από τις βασικές επεκτάσεις του Latte και τις δικές σας επεκτάσεις εκτελούνται διαδοχικά. Η σειρά μπορεί να είναι σημαντική, ειδικά αν ένα πέρασμα εξαρτάται από τα αποτελέσματα ή τις τροποποιήσεις ενός άλλου. Το Latte παρέχει έναν βοηθητικό μηχανισμό για τον έλεγχο αυτής της σειράς, εάν χρειάζεται· δείτε την τεκμηρίωση για το [`Extension::getPasses()` |extending-latte#getPasses] για λεπτομέρειες. - - -Παράδειγμα AST -============== - -Για καλύτερη κατανόηση του AST, παραθέτουμε ένα δείγμα. Αυτό είναι το πρότυπο προέλευσης: - -```latte -{foreach $category->getItems() as $item} -
  • {$item->name|upper}
  • - {else} - no items found -{/foreach} -``` - -Και αυτή είναι η αναπαράστασή του με τη μορφή 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') - ) - ) - ) -) -\-- - - -Διάσχιση του AST με το `NodeTraverser` -====================================== - -Η χειροκίνητη συγγραφή αναδρομικών συναρτήσεων για τη διάσχιση της πολύπλοκης δομής του AST είναι κουραστική και επιρρεπής σε σφάλματα. Το Latte παρέχει ένα ειδικό εργαλείο για αυτόν τον σκοπό: το [api:Latte\Compiler\NodeTraverser]. Αυτή η κλάση υλοποιεί το [πρότυπο σχεδίασης Visitor |https://en.wikipedia.org/wiki/Visitor_pattern], χάρη στο οποίο η διάσχιση του AST γίνεται συστηματική και εύκολα διαχειρίσιμη. - -Η βασική χρήση περιλαμβάνει τη δημιουργία ενός στιγμιότυπου του `NodeTraverser` και την κλήση της μεθόδου του `traverse()`, περνώντας τον ριζικό κόμβο του AST και ένα ή δύο "visitor" callables: - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes; - -(new NodeTraverser)->traverse( - $templateNode, - - // 'enter' visitor: Καλείται κατά την είσοδο στον κόμβο (πριν από τα παιδιά του) - enter: function (Node $node) { - echo "Είσοδος στον κόμβο τύπου: " . $node::class . "\n"; - // Εδώ μπορείτε να εξετάσετε τον κόμβο - if ($node instanceof Nodes\TextNode) { - // echo "Βρέθηκε κείμενο: " . $node->content . "\n"; - } - }, - - // 'leave' visitor: Καλείται κατά την έξοδο από τον κόμβο (μετά τα παιδιά του) - leave: function (Node $node) { - echo "Έξοδος από τον κόμβο τύπου: " . $node::class . "\n"; - // Εδώ μπορείτε να εκτελέσετε ενέργειες μετά την επεξεργασία των παιδιών - }, -); -``` - -Μπορείτε να παρέχετε μόνο τον `enter` visitor, μόνο τον `leave` visitor, ή και τους δύο, ανάλογα με τις ανάγκες σας. - -**`enter(Node $node)`:** Αυτή η συνάρτηση εκτελείται για κάθε κόμβο **πριν** ο διασχιστής (`traverser`) επισκεφθεί οποιοδήποτε από τα παιδιά αυτού του κόμβου. Είναι χρήσιμη για: - -- Συλλογή πληροφοριών κατά τη διάσχιση του δέντρου προς τα κάτω. -- Λήψη αποφάσεων *πριν* από την επεξεργασία των παιδιών (όπως η απόφαση να τα παραλείψετε, δείτε [#Βελτιστοποίηση διάσχισης]). -- Πιθανή τροποποίηση του κόμβου πριν από την επίσκεψη των παιδιών (λιγότερο συχνή). - -**`leave(Node $node)`:** Αυτή η συνάρτηση εκτελείται για κάθε κόμβο **αφού** όλα τα παιδιά του (και τα ολόκληρα υποδέντρα τους) έχουν πλήρως επισκεφθεί (τόσο η είσοδος όσο και η έξοδος). Είναι το πιο συνηθισμένο μέρος για: - -Και οι δύο visitors `enter` και `leave` μπορούν προαιρετικά να επιστρέψουν μια τιμή για να επηρεάσουν τη διαδικασία διάσχισης. Η επιστροφή `null` (ή τίποτα) συνεχίζει κανονικά τη διάσχιση, η επιστροφή ενός στιγμιότυπου `Node` αντικαθιστά τον τρέχοντα κόμβο, και η επιστροφή ειδικών σταθερών όπως `NodeTraverser::RemoveNode` ή `NodeTraverser::StopTraversal` τροποποιεί τη ροή, όπως εξηγείται στις επόμενες ενότητες. - - -Πώς λειτουργεί η διάσχιση -------------------------- - -Ο `NodeTraverser` χρησιμοποιεί εσωτερικά τη μέθοδο `getIterator()`, την οποία πρέπει να υλοποιεί κάθε κλάση `Node` (όπως συζητήθηκε στο [Δημιουργία προσαρμοσμένων tags |custom-tags#Υλοποίηση του getIterator για υποκόμβους]). Επαναλαμβάνει τα παιδιά που λαμβάνονται μέσω του `getIterator()`, καλεί αναδρομικά το `traverse()` σε αυτά και διασφαλίζει ότι οι visitors `enter` και `leave` καλούνται στη σωστή σειρά πρώτα-σε-βάθος (depth-first) για κάθε κόμβο στο δέντρο που είναι προσβάσιμος μέσω των iterators. Αυτό τονίζει ξανά γιατί η σωστά υλοποιημένη `getIterator()` στους προσαρμοσμένους κόμβους tag σας είναι απολύτως απαραίτητη για τη σωστή λειτουργία των περασμάτων μεταγλώττισης. - -Ας γράψουμε ένα απλό πέρασμα που μετρά πόσες φορές χρησιμοποιείται το tag `{do}` (που αντιπροσωπεύεται από το `Latte\Essential\Nodes\DoNode`) στο πρότυπο. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Essential\Nodes\DoNode; - -function countDoTags(TemplateNode $templateNode): void -{ - $count = 0; - (new NodeTraverser)->traverse( - $templateNode, - enter: function (Node $node) use (&$count): void { - if ($node instanceof DoNode) { - $count++; - } - }, - // Ο 'leave' visitor δεν χρειάζεται για αυτήν την εργασία - ); - - echo "Βρέθηκε το tag {do} $count φορές.\n"; -} - -$latte = new Latte\Engine; -$ast = $latte->parse($templateSource); -countDoTags($ast); -``` - -Σε αυτό το παράδειγμα, χρειαζόμασταν μόνο τον `enter` visitor για να ελέγξουμε τον τύπο κάθε κόμβου που επισκεφθήκαμε. - -Στη συνέχεια, θα εξετάσουμε πώς αυτοί οι visitors τροποποιούν πραγματικά το AST. - - -Τροποποίηση του AST -=================== - -Ένας από τους κύριους σκοπούς των περασμάτων μεταγλώττισης είναι η τροποποίηση του αφηρημένου συντακτικού δέντρου (AST). Αυτό επιτρέπει ισχυρούς μετασχηματισμούς, βελτιστοποιήσεις ή την επιβολή κανόνων απευθείας στη δομή του προτύπου πριν από τη δημιουργία κώδικα PHP. Ο `NodeTraverser` παρέχει διάφορους τρόπους για να το επιτύχετε αυτό εντός των `enter` και `leave` visitors. - -**Σημαντική σημείωση:** Η τροποποίηση του AST απαιτεί προσοχή. Λανθασμένες αλλαγές – όπως η αφαίρεση βασικών κόμβων ή η αντικατάσταση ενός κόμβου με έναν μη συμβατό τύπο – μπορεί να οδηγήσουν σε σφάλματα κατά τη δημιουργία κώδικα ή να προκαλέσουν απροσδόκητη συμπεριφορά κατά την εκτέλεση του προγράμματος. Πάντα δοκιμάζετε διεξοδικά τα περάσματα τροποποίησής σας. - - -Αλλαγή ιδιοτήτων κόμβων ------------------------ - -Ο απλούστερος τρόπος τροποποίησης του δέντρου είναι η άμεση αλλαγή των **δημόσιων ιδιοτήτων** των κόμβων που επισκέπτονται κατά τη διάσχιση. Όλοι οι κόμβοι αποθηκεύουν τα αναλυμένα ορίσματά τους, το περιεχόμενο ή τα χαρακτηριστικά τους σε δημόσιες ιδιότητες. - -**Παράδειγμα:** Ας δημιουργήσουμε ένα πέρασμα που βρίσκει όλους τους στατικούς κόμβους κειμένου (`TextNode`, που αντιπροσωπεύουν κανονικό HTML ή κείμενο εκτός των tags Latte) και μετατρέπει το περιεχόμενό τους σε κεφαλαία *απευθείας στο AST*. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Compiler\Nodes\TextNode; - -function uppercaseStaticText(TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // Μπορούμε να χρησιμοποιήσουμε το 'enter', επειδή το TextNode δεν έχει παιδιά για επεξεργασία - enter: function (Node $node) { - // Είναι αυτός ο κόμβος ένα στατικό μπλοκ κειμένου; - if ($node instanceof TextNode) { - // Ναι! Τροποποιούμε άμεσα τη δημόσια ιδιότητά του 'content'. - $node->content = mb_strtoupper(html_entity_decode($node->content)); - } - // Δεν χρειάζεται να επιστρέψουμε τίποτα· η αλλαγή εφαρμόζεται απευθείας. - }, - ); -} -``` - -Σε αυτό το παράδειγμα, ο `enter` visitor ελέγχει αν ο τρέχων `$node` είναι τύπου `TextNode`. Αν ναι, ενημερώνουμε απευθείας τη δημόσια ιδιότητά του `$content` χρησιμοποιώντας τη συνάρτηση `mb_strtoupper()`. Αυτό αλλάζει άμεσα το περιεχόμενο του στατικού κειμένου που είναι αποθηκευμένο στο AST *πριν* από τη δημιουργία κώδικα PHP. Επειδή τροποποιούμε το αντικείμενο απευθείας, δεν χρειάζεται να επιστρέψουμε τίποτα από τον visitor. - -Αποτέλεσμα: Αν το πρότυπο περιείχε `

    Hello

    {= $var }World`, μετά από αυτό το πέρασμα το AST θα αντιπροσωπεύει κάτι σαν: `

    HELLO

    {= $var }WORLD`. Αυτό ΔΕΝ ΕΠΗΡΕΑΖΕΙ το περιεχόμενο της μεταβλητής `$var`. - - -Αντικατάσταση κόμβων --------------------- - -Μια πιο ισχυρή τεχνική τροποποίησης είναι η πλήρης αντικατάσταση ενός κόμβου με έναν άλλο. Αυτό γίνεται **επιστρέφοντας ένα νέο στιγμιότυπο `Node`** από τον `enter` ή `leave` visitor. Ο `NodeTraverser` αντικαθιστά στη συνέχεια τον αρχικό κόμβο με τον επιστρεφόμενο στη δομή του γονικού κόμβου. - -**Παράδειγμα:** Ας δημιουργήσουμε ένα πέρασμα που βρίσκει όλες τις χρήσεις της σταθεράς `PHP_VERSION` (που αντιπροσωπεύεται από το `ConstantFetchNode`) και τις αντικαθιστά απευθείας με ένα string literal (`StringNode`) που περιέχει την *πραγματική* έκδοση της PHP που ανιχνεύθηκε *κατά τη μεταγλώττιση*. Αυτή είναι μια μορφή βελτιστοποίησης κατά τη μεταγλώττιση. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Compiler\Nodes\Php\Expression\ConstantFetchNode; -use Latte\Compiler\Nodes\Php\Scalar\StringNode; - -function inlinePhpVersion(TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // Το 'leave' χρησιμοποιείται συχνά για αντικατάσταση, διασφαλίζοντας ότι τα παιδιά (αν υπάρχουν) - // επεξεργάζονται πρώτα, αν και το 'enter' θα λειτουργούσε επίσης εδώ. - leave: function (Node $node) { - // Είναι αυτός ο κόμβος πρόσβαση σε σταθερά και το όνομα της σταθεράς είναι 'PHP_VERSION'; - if ($node instanceof ConstantFetchNode && (string) $node->name === 'PHP_VERSION') { - // Δημιουργούμε ένα νέο StringNode που περιέχει την τρέχουσα έκδοση της PHP - $newNode = new StringNode(PHP_VERSION); - - // Προαιρετικό, αλλά καλή πρακτική: αντιγράφουμε τις πληροφορίες θέσης - $newNode->position = $node->position; - - // Επιστρέφουμε το νέο StringNode. Ο Traverser θα αντικαταστήσει - // το αρχικό ConstantFetchNode με αυτό το $newNode. - return $newNode; - } - // Αν δεν επιστρέψουμε ένα Node, ο αρχικός $node διατηρείται. - }, - ); -} -``` - -Εδώ ο `leave` visitor αναγνωρίζει το συγκεκριμένο `ConstantFetchNode` για το `PHP_VERSION`. Στη συνέχεια, δημιουργεί ένα εντελώς νέο `StringNode` που περιέχει την τιμή της σταθεράς `PHP_VERSION` *κατά τη μεταγλώττιση*. Επιστρέφοντας αυτό το `$newNode` λέει στον traverser να αντικαταστήσει το αρχικό `ConstantFetchNode` στο AST. - -Αποτέλεσμα: Αν το πρότυπο περιείχε `{= PHP_VERSION }` και η μεταγλώττιση εκτελείται σε PHP 8.2.1, το AST μετά από αυτό το πέρασμα θα αντιπροσωπεύει ουσιαστικά `{= '8.2.1' }`. - -**Επιλογή `enter` vs. `leave` για αντικατάσταση:** - -- Χρησιμοποιήστε το `leave` εάν η δημιουργία του νέου κόμβου εξαρτάται από τα αποτελέσματα της επεξεργασίας των παιδιών του παλιού κόμβου, ή αν θέλετε απλώς να διασφαλίσετε ότι τα παιδιά επισκέπτονται πριν από την αντικατάσταση (συνήθης πρακτική). -- Χρησιμοποιήστε το `enter` εάν θέλετε να αντικαταστήσετε έναν κόμβο *πριν* τα παιδιά του επισκεφθούν καν. - - -Αφαίρεση κόμβων ---------------- - -Μπορείτε να αφαιρέσετε εντελώς έναν κόμβο από το AST επιστρέφοντας την ειδική σταθερά `NodeTraverser::RemoveNode` από τον visitor. - -**Παράδειγμα:** Ας αφαιρέσουμε όλα τα σχόλια προτύπου (`{* ... *}`), τα οποία αντιπροσωπεύονται από το `CommentNode` στο AST που παράγεται από τον πυρήνα του Latte (αν και συνήθως επεξεργάζονται νωρίτερα, αυτό χρησιμεύει ως παράδειγμα). - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Compiler\Nodes\CommentNode; - -function removeCommentNodes(TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // Το 'enter' είναι εντάξει εδώ, επειδή δεν χρειαζόμαστε πληροφορίες για τα παιδιά για να αφαιρέσουμε το σχόλιο - enter: function (Node $node) { - if ($node instanceof CommentNode) { - // Σηματοδοτούμε στον traverser να αφαιρέσει αυτόν τον κόμβο από το AST - return NodeTraverser::RemoveNode; - } - }, - ); -} -``` - -**Προειδοποίηση:** Χρησιμοποιήστε το `RemoveNode` με προσοχή. Η αφαίρεση ενός κόμβου που περιέχει βασικό περιεχόμενο ή επηρεάζει τη δομή (όπως η αφαίρεση του κόμβου περιεχομένου ενός βρόχου) μπορεί να οδηγήσει σε κατεστραμμένα πρότυπα ή άκυρο παραγόμενο κώδικα. Είναι ασφαλέστερο για κόμβους που είναι πραγματικά προαιρετικοί ή αυτόνομοι (όπως σχόλια ή tags εντοπισμού σφαλμάτων) ή για κενούς δομικούς κόμβους (π.χ. ένα κενό `FragmentNode` μπορεί να αφαιρεθεί με ασφάλεια σε ορισμένα πλαίσια από ένα πέρασμα καθαρισμού). - -Αυτές οι τρεις μέθοδοι - τροποποίηση ιδιοτήτων, αντικατάσταση κόμβων και αφαίρεση κόμβων - παρέχουν τα βασικά εργαλεία για τη χειραγώγηση του AST εντός των περασμάτων μεταγλώττισής σας. - - -Βελτιστοποίηση διάσχισης -======================== - -Το AST των προτύπων μπορεί να είναι αρκετά μεγάλο, περιέχοντας ενδεχομένως χιλιάδες κόμβους. Η διάσχιση κάθε μεμονωμένου κόμβου μπορεί να είναι περιττή και να επηρεάσει την απόδοση της μεταγλώττισης, εάν το πέρασμά σας ενδιαφέρεται μόνο για συγκεκριμένα τμήματα του δέντρου. Ο `NodeTraverser` προσφέρει τρόπους βελτιστοποίησης της διάσχισης: - - -Παράλειψη παιδιών ------------------ - -Αν γνωρίζετε ότι μόλις συναντήσετε έναν συγκεκριμένο τύπο κόμβου, κανένας από τους απογόνους του δεν μπορεί να περιέχει τους κόμβους που αναζητάτε, μπορείτε να πείτε στον traverser να παραλείψει την επίσκεψη των παιδιών του. Αυτό γίνεται επιστρέφοντας τη σταθερά `NodeTraverser::DontTraverseChildren` από τον **`enter`** visitor. Αυτό παραλείπει ολόκληρους κλάδους κατά τη διάσχιση, εξοικονομώντας δυνητικά σημαντικό χρόνο, ειδικά σε πρότυπα με πολύπλοκες εκφράσεις PHP εντός των tags. - - -Διακοπή διάσχισης ------------------ - -Αν το πέρασμά σας χρειάζεται να βρει μόνο την *πρώτη* εμφάνιση κάτι (ένα συγκεκριμένο τύπο κόμβου, την ικανοποίηση μιας συνθήκης), μπορείτε να σταματήσετε εντελώς ολόκληρη τη διαδικασία διάσχισης μόλις το βρείτε. Αυτό επιτυγχάνεται επιστρέφοντας τη σταθερά `NodeTraverser::StopTraversal` από τον `enter` ή `leave` visitor. Η μέθοδος `traverse()` σταματά να επισκέπτεται οποιουσδήποτε άλλους κόμβους. Αυτό είναι εξαιρετικά αποτελεσματικό εάν χρειάζεστε μόνο την πρώτη αντιστοίχιση σε ένα δυνητικά πολύ μεγάλο δέντρο. - - -Χρήσιμος βοηθός `NodeHelpers` -============================= - -Ενώ ο `NodeTraverser` προσφέρει λεπτομερή έλεγχο, το Latte παρέχει επίσης μια πρακτική βοηθητική κλάση, την [api:Latte\Compiler\NodeHelpers], η οποία ενσωματώνει τον `NodeTraverser` για διάφορες κοινές εργασίες αναζήτησης και ανάλυσης, απαιτώντας συχνά λιγότερο προπαρασκευαστικό κώδικα. - - -find(Node $startNode, callable $filter): array .[method] --------------------------------------------------------- - -Αυτή η στατική μέθοδος βρίσκει **όλους** τους κόμβους στο υποδέντρο που ξεκινούν από το `$startNode` (συμπεριλαμβανομένου), οι οποίοι ικανοποιούν το callback `$filter`. Επιστρέφει έναν πίνακα των αντιστοιχούντων κόμβων. - -**Παράδειγμα:** Βρείτε όλους τους κόμβους μεταβλητών (`VariableNode`) σε ολόκληρο το πρότυπο. - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\Php\Expression\VariableNode; -use Latte\Compiler\Nodes\TemplateNode; - -function findAllVariables(TemplateNode $templateNode): array -{ - return NodeHelpers::find( - $templateNode, - fn($node) => $node instanceof VariableNode, - ); -} -``` - - -findFirst(Node $startNode, callable $filter): ?Node .[method] --------------------------------------------------------------- - -Παρόμοιο με το `find`, αλλά σταματά τη διάσχιση αμέσως μόλις βρεθεί ο **πρώτος** κόμβος που ικανοποιεί το callback `$filter`. Επιστρέφει το αντικείμενο `Node` που βρέθηκε ή `null` εάν δεν βρεθεί κανένας αντιστοιχών κόμβος. Αυτό είναι ουσιαστικά ένα πρακτικό περιτύλιγμα γύρω από το `NodeTraverser::StopTraversal`. - -**Παράδειγμα:** Βρείτε τον κόμβο `{parameters}` (το ίδιο με το χειροκίνητο παράδειγμα προηγουμένως, αλλά συντομότερο). - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\TemplateNode; -use Latte\Essential\Nodes\ParametersNode; - -function findParametersNodeHelper(TemplateNode $templateNode): ?ParametersNode -{ - return NodeHelpers::findFirst( - $templateNode->head, // Αναζήτηση μόνο στην κεφαλίδα (head section) για αποτελεσματικότητα - fn($node) => $node instanceof ParametersNode, - ); -} -``` - - -toValue(ExpressionNode $node, bool $constants = false): mixed .[method] ------------------------------------------------------------------------ - -Αυτή η στατική μέθοδος προσπαθεί να αξιολογήσει ένα `ExpressionNode` **κατά τη μεταγλώττιση** και να επιστρέψει την αντίστοιχη τιμή PHP. Λειτουργεί αξιόπιστα μόνο για απλούς κόμβους literal (`StringNode`, `IntegerNode`, `FloatNode`, `BooleanNode`, `NullNode`) και στιγμιότυπα `ArrayNode` που περιέχουν μόνο τέτοια αξιολογήσιμα στοιχεία. - -Εάν το `$constants` οριστεί σε `true`, θα προσπαθήσει επίσης να επιλύσει το `ConstantFetchNode` και το `ClassConstantFetchNode` ελέγχοντας το `defined()` και χρησιμοποιώντας το `constant()`. - -Εάν ο κόμβος περιέχει μεταβλητές, κλήσεις συναρτήσεων ή άλλα δυναμικά στοιχεία, δεν μπορεί να αξιολογηθεί κατά τη μεταγλώττιση και η μέθοδος θα πετάξει `InvalidArgumentException`. - -**Περίπτωση χρήσης:** Απόκτηση της στατικής τιμής ενός ορίσματος tag κατά τη μεταγλώττιση για λήψη αποφάσεων κατά τη μεταγλώττιση. - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\Php\ExpressionNode; - -function getStaticStringArgument(ExpressionNode $argumentNode): ?string -{ - try { - $value = NodeHelpers::toValue($argumentNode); - return is_string($value) ? $value : null; - } catch (\InvalidArgumentException $e) { - // Το όρισμα δεν ήταν στατικό string literal - return null; - } -} -``` - - -toText(?Node $node): ?string .[method] --------------------------------------- - -Αυτή η στατική μέθοδος είναι χρήσιμη για την εξαγωγή απλού περιεχομένου κειμένου από απλούς κόμβους. Λειτουργεί κυρίως με: -- `TextNode`: Επιστρέφει το `$content` του. -- `FragmentNode`: Συνενώνει το αποτέλεσμα του `toText()` για όλα τα παιδιά του. Αν κάποιο παιδί δεν είναι μετατρέψιμο σε κείμενο (π.χ. περιέχει `PrintNode`), επιστρέφει `null`. -- `NopNode`: Επιστρέφει μια κενή συμβολοσειρά. -- Άλλοι τύποι κόμβων: Επιστρέφει `null`. - -**Περίπτωση χρήσης:** Απόκτηση του στατικού περιεχομένου κειμένου της τιμής ενός χαρακτηριστικού HTML ή ενός απλού στοιχείου HTML για ανάλυση κατά τη διάρκεια ενός περάσματος μεταγλώττισης. - -```php -use Latte\Compiler\NodeHelpers; -use Latte\Compiler\Nodes\Html\AttributeNode; - -function getStaticAttributeValue(AttributeNode $attr): ?string -{ - // το $attr->value είναι συνήθως AreaNode (όπως FragmentNode ή TextNode) - return NodeHelpers::toText($attr->value); -} - -// Παράδειγμα χρήσης σε πέρασμα: -// if ($node instanceof Html\ElementNode && $node->name === 'meta') { -// $nameAttrValue = getStaticAttributeValue($node->getAttributeNode('name')); -// if ($nameAttrValue === 'description') { ... } -// } -``` - -Η κλάση `NodeHelpers` μπορεί να απλοποιήσει τα περάσματα μεταγλώττισής σας παρέχοντας έτοιμες λύσεις για κοινές εργασίες διάσχισης και ανάλυσης του AST. - - -Πρακτικά παραδείγματα -===================== - -Ας εφαρμόσουμε τις έννοιες της διάσχισης και τροποποίησης του AST για να λύσουμε ορισμένα πρακτικά προβλήματα. Αυτά τα παραδείγματα δείχνουν κοινά πρότυπα που χρησιμοποιούνται στα περάσματα μεταγλώττισης. - - -Αυτόματη προσθήκη `loading="lazy"` σε `` ---------------------------------------------- - -Οι σύγχρονοι περιηγητές υποστηρίζουν εγγενή τεμπέλικη φόρτωση (lazy loading) για εικόνες χρησιμοποιώντας το χαρακτηριστικό `loading="lazy"`. Ας δημιουργήσουμε ένα πέρασμα που προσθέτει αυτόματα αυτό το χαρακτηριστικό σε όλα τα tags `` που δεν έχουν ήδη το χαρακτηριστικό `loading`. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes; -use Latte\Compiler\Nodes\Html; - -function addLazyLoading(Nodes\TemplateNode $templateNode): void -{ - (new NodeTraverser)->traverse( - $templateNode, - // Μπορούμε να χρησιμοποιήσουμε το 'enter', επειδή τροποποιούμε τον κόμβο απευθείας - // και δεν εξαρτόμαστε από τα παιδιά για αυτήν την απόφαση. - enter: function (Node $node) { - // Είναι ένα στοιχείο HTML με όνομα 'img'; - if ($node instanceof Html\ElementNode && $node->name === 'img') { - // Διασφαλίζουμε ότι ο κόμβος χαρακτηριστικών υπάρχει - $node->attributes ??= new Nodes\FragmentNode; - - // Ελέγχουμε αν υπάρχει ήδη το χαρακτηριστικό 'loading' (ανεξαρτήτως πεζών-κεφαλαίων) - foreach ($node->attributes->children as $attrNode) { - if ($attrNode instanceof Html\AttributeNode - && $attrNode->name instanceof Nodes\TextNode // Στατικό όνομα χαρακτηριστικού - && strtolower($attrNode->name->content) === 'loading' - ) { - return; // Βρέθηκε, δεν κάνουμε τίποτα - } - } - - // Προσθέτουμε ένα κενό εάν τα χαρακτηριστικά δεν είναι κενά - if ($node->attributes->children) { - $node->attributes->children[] = new Nodes\TextNode(' '); - } - - // Δημιουργούμε ένα νέο κόμβο χαρακτηριστικού: loading="lazy" - $node->attributes->children[] = new Html\AttributeNode( - name: new Nodes\TextNode('loading'), - value: new Nodes\TextNode('lazy'), - quote: '"', - ); - // Η αλλαγή εφαρμόζεται απευθείας στο αντικείμενο, δεν χρειάζεται να επιστρέψουμε τίποτα. - } - }, - ); -} -``` - -Εξήγηση: -- Ο `enter` visitor αναζητά κόμβους `Html\ElementNode` με όνομα `img`. -- Επαναλαμβάνει τα υπάρχοντα χαρακτηριστικά (`$node->attributes->children`) και ελέγχει αν το χαρακτηριστικό `loading` είναι ήδη παρόν. -- Αν δεν βρεθεί, δημιουργεί ένα νέο `Html\AttributeNode` που αντιπροσωπεύει το `loading="lazy"`. - - -Έλεγχος κλήσεων συναρτήσεων ---------------------------- - -Τα περάσματα μεταγλώττισης αποτελούν τη βάση του Latte Sandbox. Αν και το πραγματικό Sandbox είναι εξελιγμένο, μπορούμε να επιδείξουμε τη βασική αρχή του ελέγχου απαγορευμένων κλήσεων συναρτήσεων. - -**Στόχος:** Αποτροπή της χρήσης της δυνητικά επικίνδυνης συνάρτησης `shell_exec` εντός των εκφράσεων του προτύπου. - -```php -use Latte\Compiler\Node; -use Latte\Compiler\NodeTraverser; -use Latte\Compiler\Nodes; -use Latte\Compiler\Nodes\Php; -use Latte\SecurityViolationException; - -function checkForbiddenFunctions(Nodes\TemplateNode $templateNode): void -{ - $forbiddenFunctions = ['shell_exec' => true, 'exec' => true]; // Απλή λίστα - - $traverser = new NodeTraverser; - (new NodeTraverser)->traverse( - $templateNode, - enter: function (Node $node) use ($forbiddenFunctions) { - // Είναι ένας κόμβος άμεσης κλήσης συνάρτησης; - if ($node instanceof Php\Expression\FunctionCallNode - && $node->name instanceof Php\NameNode - && isset($forbiddenFunctions[strtolower((string) $node->name)]) - ) { - throw new SecurityViolationException( - "Η συνάρτηση {$node->name}() δεν επιτρέπεται.", - $node->position, - ); - } - }, - ); -} -``` - -Εξήγηση: -- Ορίζουμε μια λίστα απαγορευμένων ονομάτων συναρτήσεων. -- Ο `enter` visitor ελέγχει για `FunctionCallNode`. -- Αν το όνομα της συνάρτησης (`$node->name`) είναι στατικό `NameNode`, ελέγχουμε την αναπαράστασή του σε πεζά γράμματα έναντι της απαγορευμένης λίστας μας. -- Αν βρεθεί απαγορευμένη συνάρτηση, πετάμε `Latte\SecurityViolationException`, η οποία υποδεικνύει σαφώς την παραβίαση του κανόνα ασφαλείας και σταματά τη μεταγλώττιση. - -Αυτά τα παραδείγματα δείχνουν πώς τα περάσματα μεταγλώττισης με τη χρήση του `NodeTraverser` μπορούν να αξιοποιηθούν για ανάλυση, αυτόματες τροποποιήσεις και επιβολή περιορισμών ασφαλείας αλληλεπιδρώντας άμεσα με τη δομή AST του προτύπου. - - -Βέλτιστες πρακτικές -=================== - -Κατά τη συγγραφή περασμάτων μεταγλώττισης, λάβετε υπόψη αυτές τις οδηγίες για τη δημιουργία στιβαρών, συντηρήσιμων και αποτελεσματικών επεκτάσεων: - -- **Η σειρά έχει σημασία:** Έχετε υπόψη τη σειρά με την οποία εκτελούνται τα περάσματα. Αν το πέρασμά σας εξαρτάται από τη δομή AST που δημιουργήθηκε από άλλο πέρασμα (π.χ. βασικά περάσματα του Latte ή άλλο προσαρμοσμένο πέρασμα), ή αν άλλα περάσματα μπορεί να εξαρτώνται από τις τροποποιήσεις σας, χρησιμοποιήστε τον μηχανισμό ταξινόμησης που παρέχεται από το `Extension::getPasses()` για να ορίσετε εξαρτήσεις (`before`/`after`). Δείτε την τεκμηρίωση του [`Extension::getPasses()` |extending-latte#getPasses] για λεπτομέρειες. -- **Μία ευθύνη:** Επιδιώξτε περάσματα που εκτελούν μία καλά καθορισμένη εργασία. Για σύνθετους μετασχηματισμούς, εξετάστε το ενδεχόμενο διαχωρισμού της λογικής σε πολλαπλά περάσματα – ίσως ένα για ανάλυση και ένα άλλο για τροποποίηση βάσει των αποτελεσμάτων της ανάλυσης. Αυτό βελτιώνει τη σαφήνεια και τη δυνατότητα δοκιμής. -- **Απόδοση:** Να θυμάστε ότι τα περάσματα μεταγλώττισης προσθέτουν χρόνο στη μεταγλώττιση του προτύπου (αν και αυτό συνήθως συμβαίνει μόνο μία φορά, μέχρι να αλλάξει το πρότυπο). Αποφύγετε υπολογιστικά δαπανηρές λειτουργίες στα περάσματά σας, αν είναι δυνατόν. Αξιοποιήστε τις βελτιστοποιήσεις διάσχισης όπως `NodeTraverser::DontTraverseChildren` και `NodeTraverser::StopTraversal` όποτε γνωρίζετε ότι δεν χρειάζεται να επισκεφθείτε ορισμένα τμήματα του AST. -- **Χρησιμοποιήστε το `NodeHelpers`:** Για κοινές εργασίες όπως η εύρεση συγκεκριμένων κόμβων ή η στατική αξιολόγηση απλών εκφράσεων, ελέγξτε αν το `Latte\Compiler\NodeHelpers` προσφέρει μια κατάλληλη μέθοδο πριν γράψετε τη δική σας λογική `NodeTraverser`. Μπορεί να εξοικονομήσει χρόνο και να μειώσει την ποσότητα του προπαρασκευαστικού κώδικα. -- **Χειρισμός σφαλμάτων:** Αν το πέρασμά σας ανιχνεύσει ένα σφάλμα ή μια άκυρη κατάσταση στο AST του προτύπου, πετάξτε `Latte\CompileException` (ή `Latte\SecurityViolationException` για θέματα ασφαλείας) με ένα σαφές μήνυμα και το σχετικό αντικείμενο `Position` (συνήθως `$node->position`). Αυτό παρέχει χρήσιμη ανατροφοδότηση στον προγραμματιστή του προτύπου. -- **Idempotence (αν είναι δυνατόν):** Ιδανικά, η εκτέλεση του περάσματός σας πολλές φορές στο ίδιο AST θα πρέπει να παράγει το ίδιο αποτέλεσμα με την εκτέλεσή του μία φορά. Αυτό δεν είναι πάντα εφικτό, αλλά απλοποιεί τον εντοπισμό σφαλμάτων και τη συλλογιστική σχετικά με τις αλληλεπιδράσεις των περασμάτων, εάν επιτευχθεί. Για παράδειγμα, βεβαιωθείτε ότι το πέρασμα τροποποίησής σας ελέγχει αν η τροποποίηση έχει ήδη εφαρμοστεί πριν την εφαρμόσει ξανά. - -Ακολουθώντας αυτές τις πρακτικές, μπορείτε να αξιοποιήσετε αποτελεσματικά τα περάσματα μεταγλώττισης για να επεκτείνετε τις δυνατότητες του Latte με ισχυρό και αξιόπιστο τρόπο, συμβάλλοντας σε ασφαλέστερη, βελτιστοποιημένη ή πιο πλούσια σε λειτουργίες επεξεργασία προτύπων. diff --git a/latte/el/cookbook/@home.texy b/latte/el/cookbook/@home.texy deleted file mode 100644 index ca842613f6..0000000000 --- a/latte/el/cookbook/@home.texy +++ /dev/null @@ -1,13 +0,0 @@ -Οδηγοί και διαδικασίες -********************** - -.[perex] -Παραδείγματα κώδικα και συνταγές για την εκτέλεση κοινών εργασιών με το Latte. - -- [Διαδικασίες για developers |/develop] -- [Πέρασμα μεταβλητών μεταξύ templates |passing-variables] -- [Όλα όσα θέλατε να μάθετε για την ομαδοποίηση |grouping] -- [Πώς να γράψετε ερωτήματα SQL στο Latte; |how-to-write-sql-queries-in-latte] -- [Μετάβαση από PHP |migration-from-php] -- [Μετάβαση από το Twig |migration-from-twig] -- [Χρήση του Latte με το Slim 4 |slim-framework] diff --git a/latte/el/cookbook/@meta.texy b/latte/el/cookbook/@meta.texy deleted file mode 100644 index 77b5040a4a..0000000000 --- a/latte/el/cookbook/@meta.texy +++ /dev/null @@ -1,2 +0,0 @@ -{{sitename: Latte Τεκμηρίωση}} -{{leftbar: /@left-menu}} diff --git a/latte/el/cookbook/grouping.texy b/latte/el/cookbook/grouping.texy deleted file mode 100644 index ca1f2a72c3..0000000000 --- a/latte/el/cookbook/grouping.texy +++ /dev/null @@ -1,251 +0,0 @@ -Όλα όσα θέλατε ποτέ να μάθετε για την ομαδοποίηση -************************************************* - -.[perex] -Όταν εργάζεστε με δεδομένα σε πρότυπα, μπορεί συχνά να συναντήσετε την ανάγκη να τα ομαδοποιήσετε ή να τα εμφανίσετε συγκεκριμένα σύμφωνα με ορισμένα κριτήρια. Το Latte προσφέρει πολλά ισχυρά εργαλεία για αυτόν τον σκοπό. - -Το φίλτρο και η συνάρτηση `|group` επιτρέπουν την αποτελεσματική ομαδοποίηση δεδομένων σύμφωνα με ένα καθορισμένο κριτήριο, το φίλτρο `|batch` διευκολύνει τη διαίρεση δεδομένων σε σταθερές παρτίδες, και η ετικέτα `{iterateWhile}` παρέχει τη δυνατότητα πιο σύνθετου ελέγχου της ροής των βρόχων με συνθήκες. Κάθε μία από αυτές τις ετικέτες προσφέρει συγκεκριμένες δυνατότητες για εργασία με δεδομένα, καθιστώντας τα απαραίτητα εργαλεία για δυναμική και δομημένη εμφάνιση πληροφοριών στα πρότυπα του Latte. - - -Φίλτρο και συνάρτηση `group` .{data-version:3.0.16} -=================================================== - -Φανταστείτε έναν πίνακα βάσης δεδομένων `items` με στοιχεία χωρισμένα σε κατηγορίες: - -| id | categoryId | name -|------------------ -| 1 | 1 | Apple -| 2 | 1 | Banana -| 3 | 2 | PHP -| 4 | 3 | Green -| 5 | 3 | Red -| 6 | 3 | Blue - -Μια απλή λίστα όλων των στοιχείων χρησιμοποιώντας ένα πρότυπο Latte θα έμοιαζε ως εξής: - -```latte -
      -{foreach $items as $item} -
    • {$item->name}
    • -{/foreach} -
    -``` - -Ωστόσο, αν θέλαμε τα στοιχεία να οργανωθούν σε ομάδες ανά κατηγορία, θα χρειαζόταν να τα χωρίσουμε έτσι ώστε κάθε κατηγορία να έχει τη δική της λίστα. Το αποτέλεσμα θα έπρεπε τότε να μοιάζει ως εξής: - -```latte -
      -
    • Apple
    • -
    • Banana
    • -
    - -
      -
    • PHP
    • -
    - -
      -
    • Green
    • -
    • Red
    • -
    • Blue
    • -
    -``` - -Η εργασία μπορεί να λυθεί εύκολα και κομψά χρησιμοποιώντας το `|group`. Ως παράμετρο, καθορίζουμε το `categoryId`, που σημαίνει ότι τα στοιχεία θα χωριστούν σε μικρότερους πίνακες με βάση την τιμή του `$item->categoryId` (αν το `$item` ήταν πίνακας, θα χρησιμοποιούνταν το `$item['categoryId']`): - -```latte -{foreach ($items|group: categoryId) as $categoryId => $categoryItems} -
      - {foreach $categoryItems as $item} -
    • {$item->name}
    • - {/foreach} -
    -{/foreach} -``` - -Το φίλτρο μπορεί επίσης να χρησιμοποιηθεί ως συνάρτηση στο Latte, δίνοντάς μας μια εναλλακτική σύνταξη: `{foreach group($items, categoryId) ...}`. - -Αν θέλετε να ομαδοποιήσετε στοιχεία με βάση πιο σύνθετα κριτήρια, μπορείτε να χρησιμοποιήσετε μια συνάρτηση στην παράμετρο του φίλτρου. Για παράδειγμα, η ομαδοποίηση στοιχείων με βάση το μήκος του ονόματος θα έμοιαζε ως εξής: - -```latte -{foreach ($items|group: fn($item) => strlen($item->name)) as $items} - ... -{/foreach} -``` - -Είναι σημαντικό να σημειωθεί ότι το `$categoryItems` δεν είναι ένας συνηθισμένος πίνακας, αλλά ένα αντικείμενο που συμπεριφέρεται σαν επαναλήπτης. Για πρόσβαση στο πρώτο στοιχείο της ομάδας, μπορείτε να χρησιμοποιήσετε τη συνάρτηση [`first()` |latte:functions#first]. - -Αυτή η ευελιξία στην ομαδοποίηση δεδομένων καθιστά το `group` ένα εξαιρετικά χρήσιμο εργαλείο για την παρουσίαση δεδομένων στα πρότυπα του Latte. - - -Ένθετοι Βρόχοι --------------- - -Ας φανταστούμε ότι έχουμε έναν πίνακα βάσης δεδομένων με μια επιπλέον στήλη `subcategoryId`, η οποία ορίζει τις υποκατηγορίες των μεμονωμένων στοιχείων. Θέλουμε να εμφανίσουμε κάθε κύρια κατηγορία σε μια ξεχωριστή λίστα `